JWT认证登陆方式
在学习前后端分离的过程中往往遇到的第一个问题就是登陆验证问题,以前的我们将Cookie、Session、Token,在我的学习过程中认识了JWT验证方案,纯属自己理解,有错还望大佬指出。
JWT 全称是
JSON Web Token
,是目前非常流行的跨域认证解决方案.
前情回顾
早期的Cookie和Session的认证方式,之前没有进行前后端分离的模式大部分使用的是Cookie-Session的认证方式,现在的未分离的项目基本也还是这种模式。
认证的大致流程就是:
- 用户在前端输入用户名和密码进行登陆系统。
- 服务端进行用户名和密码的验证,创建一个Session对象,存入内存中,并将此Session的ID返回给前端
- 前端存储返回的SessionID,每次请求都要带着SessionID,服务端通过SessionID进行校验。
JWT
JWT 就是一种Cookie-Session改造版的具体实现,让你省去自己造轮子的时间,JWT 还有个好处,那就是你可以不用在服务端存储认证信息(比如 token),完全由客户端提供,服务端只要根据 JWT 自身提供的解密算法就可以验证用户合法性,而且这个过程是安全的。
如果你是刚接触 JWT,最有疑问的一点可能就是: JWT 为什么可以完全依靠客户端(比如浏览器端)就能实现认证功能,认证信息全都存在客户端,怎么保证安全性?
JWT的构成
JWT 是由三段字符串和两个.
组成,每个字符串和字符串之间没有换行(类似于这样:xxxxxx.yyyyyy.zzzzzz),每个字符串代表了不同的功能
- JWT头部
- 有效载荷
- 哈希签名
我们将这三个字符串的功能按顺序列出来并讲解:
1. JWT 头
JWT 头描述了 JWT 元数据,是一个 JSON 对象,它的格式如下:
{"alg":"HS256","typ":"JWT"}
这里的 alg 属性表示签名所使用的算法,JWT 签名默认的算法为 HMAC SHA256 , alg 属性值 HS256 就是 HMAC SHA256 算法。typ 属性表示令牌类型,这里就是 JWT。
2. 有效载荷
有效载荷是 JWT 的主体,同样也是个 JSON 对象。有效载荷包含三个部分:
-
标准注册声明标准注册声明不是强制使用是的,但是我建议使用。它一般包括以下内容:
-
- iss:jwt的签发者/发行人;
-
- sub:主题;
-
- aud:接收方;
-
- exp:jwt过期时间;
-
- nbf:jwt生效时间;
-
- iat:签发时间
-
- jti:jwt唯一身份标识,可以避免重放攻击
-
公共声明:可以在公共声明添加任何信息,我们一般会在里面添加用户信息和业务信息,但是不建议添加敏感信息,因为公共声明部分可以在客户端解密。
-
私有声明:私有声明是服务器和客户端共同定义的声明,同样这里不建议添加敏感信息。
下面这个代码段就是定义了一个有效载荷:
{"exp":"201909181230","role":"admin","isShow":false}
3. 哈希签名
哈希签名的算法主要是确保数据不会被篡改。它主要是对前面所讲的两个部分进行签名,通过 JWT 头定义的算法生成哈希。哈希签名的过程如下:
-
指定密码,密码保存在服务器中,不能向客户端公开;
-
使用 JWT 头指定的算法进行签名,进行签名前需要对 JWT 头和有效载荷进行 Base64URL 编码,JWT 头和有效载荷编码后的结果之间需要用 . 来连接。
简单示例如下:
HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)
最终结果如下:
base64UrlEncode(JWT 头)+"."+base64UrlEncode(有效载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)
使用方式
- 用户在前端输入用户名和密码进行登陆系统。
- 服务器对账号密码进行验证,计算出该用户的JWT字符串,并且返回给客户端
- 客户端将后端返回的JWT字符串存储到Cookie或者是LocalStorage
- 客户端在进行请求的时候需要带上JWT
- 服务端拿到这个JWT字符串后,使用 base64的头部和 base64 的载荷部分,通过
HMACSHA256
算法计算签名部分,比较计算结果和传来的签名部分是否一致,如果一致,说明此次请求没有问题,如果不一致,说明请求过期或者是非法请求。
Java实现
-
依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency>
-
实现
package com.example.jwtmdeo.utils; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; /** * JWT处理类 */ public class JwtHelper { private static final String SECRET = "9a96349e2345385785e804e0f4254dee"; //加密字符串 随便定义 private static String ISSUER = "sys_user"; //签发人 /** * 生成JWT * @param claims 有效载荷 * @param expireDatePoint 过期时间点 * @return */ public static String createJWT(Map<String, String> claims, Date expireDatePoint){ try { //使用HMAC256进行加密 Algorithm algorithm = Algorithm.HMAC256(SECRET); //创建jwt JWTCreator.Builder builder = JWT.create(). withIssuer(ISSUER). //发行人 withExpiresAt(expireDatePoint); //过期时间点 //添加有效载荷 claims.forEach((key,value)-> { builder.withClaim(key, value); }); //签名加密 return builder.sign(algorithm); } catch (IllegalArgumentException | UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * 解密jwt * @param token * @return * @throws RuntimeException */ public static Map<String,String> verifyToken(String token) throws RuntimeException{ Algorithm algorithm = null; try { //使用HMAC256进行加密 algorithm = Algorithm.HMAC256(SECRET); } catch (IllegalArgumentException | UnsupportedEncodingException e) { throw new RuntimeException(e); } //解密 JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build(); DecodedJWT jwt = verifier.verify(token); Map<String, Claim> map = jwt.getClaims(); Map<String, String> resultMap = new HashMap<>(); map.forEach((k,v) -> resultMap.put(k, v.asString())); return resultMap; } }
注意事项
在使用 JWT 时需要注意以下事项:
-
JWT 默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密;
-
JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改;
-
JWT 包含认证信息,如果泄露了,任何人都可以获得令牌所有的权限;因此 JWT 有效期不能太长,对于重要操作每次请求都必须进行身份验证。
-
一旦颁发一个 JWT 令牌,服务端就没办法废弃掉它,除非等到它自身过期。有很多应用默认只允许最新登录的一个客户端正常使用,不允许多端登录,JWT 就没办法做到,因为颁发了新令牌,但是老的令牌在过期前仍然可用。这种情况下,就需要服务端增加相应的逻辑。