一、引言
上面这张图是传统的用户身份认证流程,一般过程如下:
1、用户向服务器发送用户名和密码;
2、 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中;
3、 服务器向用户返回session_id,session信息都会写入到用户的Cookie;
4、 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器;
5、 服务器收到session_id并对比之前保存的数据,确认用户的身份;
对于分布式架构设计,这种传统模式是存在问题的,它不能有效的支持横向扩展。
二、JWT
1、什么是JWT?
官网介绍:JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/专用密钥对对JWT进行签名。
尽管可以对JWT进行加密以在各方之间提供保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
简单总结一句话:JWT是一个JSON信息传输的开放标准,它可以使用密钥对信息进行数字签名,以确保信息是可验证和可信任的。
2、JWT的结构
JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。 这三部分紧密排列,并由“.“分隔,因此,JWT通常如下所示。
xxxxx.yyyyy.zzzzz
其中,第一部分为 header
header通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA等等。例如:
{
"alg": "HS256",
"typ": "JWT"
}
alg
属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)
;typ
属性表示令牌的类型,JWT
令牌统一写为JWT
。最后,会使用Base64 URL
算法将上面JSON对象
转换为字符串保存,也就是xxxxx.yyyyy.zzzzz
中的xxxxxx
。
其中,第二部分为 Payload
Payload
也叫有效载荷部分,是JWT
的主体内容部分,它也是一个JSON对象
,包含需要传递的数据。 JWT指定七个默认字段供选择
iss: 该JWT的签发者
exp(expires): 什么时候过期,这里是一个Unix时间戳
sub: 该JWT所面向的用户
aud: 接收该JWT的一方
nbf:在此之前不可用
iat(issued at): 在什么时候签发的
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
举个例子:
{
"iss": "www.baidu.com",
"sub": "you",
"aud": "me",
"name": "456",
"admin": true,
"iat": 1584091337,
"exp": 1784091337,
}
这部分同样会被Base64
编码,然后形成JWT
的第二部分,也就是xxxxx.yyyyy.zzzzz
中的yyyyyy。
其中,第三部分为 Signature
这是JWT的第三部分,叫做签名,此部分用于防止JWT内容被篡改。将上面的两个编码后的字符串都用英文句号“.”连接在一起(头部在前),就形成了
xxxxxx.yyyyyy
然后再使用header
中声明签名算法进行签名,以确保数据不会被篡改,如果要使用HMAC SHA256
算法,则将通过以下方式创建签名:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
当然,在加密的时候,我们还需要提供一个密钥(secret),该密钥仅仅为保存在服务器中,并且不能向用户公开,这样就形成了JWT
的第三部分,也就是xxxxx.yyyyy.zzzzz
中的zzzzzz
。
最后,我们把这三个部分拼在一起,就形成了一个完整的JWT对象。
3、Base64URL算法
如前所述,JWT
头和有效载荷序列化的算法都用到了Base64URL
。该算法和常见Base64
算法类
似,但稍有差别。
作为令牌的JWT
可以放在URL
中(例如api.example/?token=xxx
)。 Base64
中用的三个字符 是"+"
,"/"
和"="
,由于这三个字符在URL
中有特殊含义,因此Base64URL
中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL
算法。
4、JWT的原则
JWT
的原则是在服务器身份验证之后,将生成一个JSON对象
并将其发送回用户,之后,当用户与服务器通信时,客户在请求中携带这个JSON对象
。服务器仅依赖于这个JSON对象
来标识用户,为了防止用户篡改数据,服务器将在生成对象时添加签名。服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
5、JWT的用法
客户端接收服务器返回的JWT
,将其存储在Cookie
或localStorage
中。此后,客户端将在与服务器交互中都会带JWT
。如果将它存储在Cookie
中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP
请求的Header Authorization
字段中。当跨域时,也可以将JWT
被放置于POST
请求的数据主体中。
6、JWT问题和趋势
(1)、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
(2)、生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
(3)、存储在客户端,不占用服务端的内存资源
(4)、JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。
(5)、当JWT未加密时,一些私密数据无法通过JWT传输。
(6)、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权 限。也就是说,一旦JWT签发,在有效期内将会一直有效。
(7)、JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象 不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用, JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
(8)、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进 行传输。
7、JWT认证流程
三、整合代码
1、在需要的模块中添加jwt工具依赖
<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
2、创建JWT工具类
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
public static final String APP_SECRET =
"ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥
//生成token字符串的方法
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("common-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id) //设置token主体部分 ,存储用户信息
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(APP_SECRET)
.parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}