JWT结构详解与JWT设置
1. 什么是token
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务端生成一个token并返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
2. 为什么要使用token
大家可能会想到,用服务器的session_id存储到cookies中也能做到,为什么非要用token呢?
个人觉得,开发web应用的话用哪种都行。但如果是开发api接口,前后端分离,最好使用token,因为 session + cookies 是基于web的。但是针对 api接口,需要考虑到移动端,app是没有cookies和session的。
3. 什么是JWT
JWT是 JSON Web Tokens 的简称,从单词可以看出它也是一种 token,其实可以理解为一种生成token的框架或规范。
4. JWT的格式
4.1 header
非常简单,typ顾名思义就是type的意思,例如上面这里就指明是JWT的类型。alg顾名思义是algorithm的意思,指代一个加密算法,例如上面指代HS256(HMAC-SHA256),这个算法会在生成第三部分signature的时候用到。
4.2 payload
payload 用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为 claims 。
payload 的一个“属性值对”其实就是一个claim(要求),每一个 claim 都代表特定的含义和作用。
根据JWT的标准,这些 claims 可以分为以下三种类型:
-
Reserved claims(保留)
它属于JWT标准里面规定的一些claim,就像是编程语言的保留字一样。JWT标准里面定义好的claim有:iss(Issuser):代表这个JWT的签发主体;
sub(Subject):代表这个JWT的主体,即它的所有人;
aud(Audience):代表这个JWT的接收对象;
exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
jti(JWT ID):是JWT的唯一标识。 -
Public claims,略(不重要)
-
Private claims(私有)
这个指的就是自定义的claim。这些claim跟JWT标准规定的claim区别在于:- JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(即JWT的每个实现库都会参照这个描述来提供JWT的验证实现);
- 而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行;
- 按照JWT标准的说明:保留的claims都是可选的,在生成payload不强制用上面的那些claim;
4.3 signature
signature顾名思义就是签名,签名一般就是用一些算法生成一个能够认证身份的字符串,具体算法就是上面表示的,也比较简单,唯一说明的一点是上面 hash 方法用到了一个 secret,这个东西需要 客户端 和 服务端 双方都知道,相当于约好了同一把验证的钥匙,最终才好做认证。
最后,每个部分都做一个 base64url encoded 的转换,然后按照 header.payload.signature 这个格式串起来就行了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
注意:JWT不保证数据不泄露,因为JWT的设计目的就不是数据加密和保护,而是为了认证来源。
5. JWT校验流程
最后再解释一下服务端 如何认证用户发来的JWT是否合法。
首先服务端和客户端必须要有个约定,例如双方同时知道加密用的 secret(这里假设用的就是简单的对称加密算法),那么在服务端 收到这个JWT时,就可以利用 JWT 前两段数据作为输入,用同一套 hash 算法和同一个 secret 自己计算一个签名值,然后把计算出来的签名值和收到的 JWT 第三段比较,如果相同则认证通过,如果不相同,则认证不通过。
就这么简单,当然,上面是假设了这个hash算法是对称加密算法,其实如果用非对称加密算法也是可以的,比方说我就用非对称的算法,对应的密钥就是一对,而非一个,那么一对公钥+私钥可以这样分配:私钥由客户端保存,公钥由服务端保存,服务端验证的时候,用公钥解密收到的 signature,这样就得到了 header 和 payload 的拼接值,用这个拼接值跟前两段比较,相同就验证通过。
6. JWT使用案例
6.1 token的创建
/**
* 初始化生成token的参数
* @param userId
* @return String
*/
public String generateToken(String userId) {
Map<String, Object> claims = new HashMap<>(1);
claims.put("sub", userId);
return generateToken(claims);
}
/**
* 生成token
* @param claims
* @return String
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(this.generateExpirationDate())
.setIssuedAt(this.generateCurrentDate())
.signWith(SignatureAlgorithm.HS512, this.secret)
.compact();
}
6.2 判断token是否可以刷新
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(this.secret)
.parseClaimsJws(token)
.getBody();
final Date iat = claims.getIssuedAt();
final Date exp = claims.getExpiration();
if (iat.before(lastPasswordReset) || exp.before(generateCurrentDate())) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
6.3 刷新token
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = Jwts.parser()
.setSigningKey(this.secret)
.parseClaimsJws(token)
.getBody();
refreshedToken = this.generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
6.4 token的校验
public String verifyToken(String token) {
String result = "";
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(this.secret)
.parseClaimsJws(token)
.getBody();
result = TokenStatus.TOKEN_VALID;
} catch (Exception e) {
result = TokenStatus.TOKEN_INVALID;
}
return result;
}
6.5 用户验证流程
用户登录
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public JSONResponse login(@RequestBody Map<String, String> map) {
String loginName = map.get("loginName");
String password = map.get("password");
User user1 = new User();
user1.setName(loginName);
user1.setPassword(password);
//身份验证是否成功
boolean isSuccess = userService.checkUser(user1);
if (isSuccess) {
User user = userService.getUserByLoginName(loginName);
if (user != null) {
//生成token,返回给客户端
String token = jwtUtil.generateToken(user.getId());
if (token != null) {
return JSONResponse.ok(token);
}
}
}
//返回登陆失败消息
return JSONResponse.info("登陆失败");
}
拦截器验证
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = request.getHeader("access_token");
//token是否存在
if (null != token) {
//验证token是否正确
String result = jwtUtil.verifyToken(token);
if(result.equals(TokenStatus.TOKEN_INVALID)){//无效
outputStream(servletResponse,"token令牌无效...");
}else{//有效令牌,需要重新刷新token,再将token传回客户端,客户端会拿着新的token进行访问
String refreshedToken = jwtUtil.refreshToken(token);
System.out.println("refreshedToken:"+refreshedToken);
// Access-Control-Allow-Origin就是我们需要设置的域名
// Access-Control-Allow-Headers跨域允许包含的头。
// Access-Control-Allow-Methods是允许的请求方式
// response.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名
// response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
// response.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type, Accept");
// 允许请求头Token
// httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With, Content-Type, Accept, Token");
// 允许客户端,发一个新的请求头jwt
// response.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, jwt");
// 允许客户端,处理一个新的响应头jwt
// response.setHeader("Access-Control-Expose-Headers", "jwt");
response.setHeader("access_token", refreshedToken);
}
filterChain.doFilter(request, response);
return;
}
outputStream(servletResponse,"无token令牌...");
}
/**
* @description: 向客户端返回响应信息(json格式)
* @author wangdong
* @date 2019/10/8 16:46
*/
private void outputStream(ServletResponse servletResponse,String message){
try{
String string = JSON.toJSONString(JSONResponse.info(message));
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.getOutputStream().write(string.getBytes("UTF-8"));
servletResponse.getOutputStream().close();
}catch (Exception e){
e.printStackTrace();
}
}
--------over