jwt与token+redis,哪种方案更好用?
最直观的:token需要查库验证token 是否有效,而JWT不用查库或者少查库,直接在服务端进行校验,并且不用查库。因为用户的信息及加密信息在第二部分payload和第三部分签证中已经生成,只要在服务端进行校验就行,并且校验也是JWT自己实现的。
TOKEN
概念: 令牌, 是访问资源的凭证。
Token的认证流程:
1. 用户输入用户名和密码,发送给服务器。
2. 服务器验证用户名和密码,正确的话就返回一个签名过的token(token 可以认为就是个长长的字符串),浏览器客户端拿到这个token。
3. 后续每次请求中,浏览器会把token作为http header发送给服务器,服务器验证签名是否有效,如果有效那么认证就成功,可以返回客户端需要的数据。
特点:
这种方式的特点就是客户端的token中自己保留有大量信息,服务器没有存储这些信息。
JWT
概念:
JWT是json web token缩写。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。
组成:
WT包含三个部分: Header头部,Payload负载和Signature签名。由三部分生成token,三部分之间用“.”号做分割。 列如 : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header 在Header中通常包含了两部分:type:代表token的类型,这里使用的是JWT类型。 alg:使用的Hash算法,例如HMAC SHA256或RSA.
{ "alg": "HS256", "typ": "JWT" } 这会被经过base64Url编码形成第一部分
2. Payload token的第二个部分是荷载信息,它包含一些声明Claim(实体的描述,通常是一个User信息,还包括一些其他的元数据) 声明分三类: 1)Reserved Claims,这是一套预定义的声明,并不是必须的,这是一套易于使用、操作性强的声明。包括:iss(issuer)、exp(expiration time)、sub(subject)、aud(audience)等 2)Plubic Claims, 3)Private Claims,交换信息的双方自定义的声明 { "sub": "1234567890", "name": "John Doe", "admin": true } 同样经过Base64Url编码后形成第二部分
3. signature 使用header中指定的算法将编码后的header、编码后的payload、一个secret进行加密。 例如使用的是HMAC SHA256算法,大致流程类似于: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 这个signature字段被用来确认JWT信息的发送者是谁,并保证信息没有被修改 。
例如下面这个例子:
现在有一个接口/viptest只能是vip用户访问,我们看看服务端如何根据Token判断用户是否有效。
普通token版:
1. 查库判断是否过期
2. 查库判断时候是VIP
JWT 版本:
假如payload部分如下:
{
"exp": 1518624000,
"isVip": true,
"uid":1
}
1. 解析JWT
2. 判断签名是否正确,根据生成签名时使用的密钥和加密算法,只要这一步过了就说明是payload是可信的
3. 判断JWT token是否过期,根据exp,判断是否是VIP,根据isVip
JWT版是没有查库的,他所需要的基础信息可以直接放到JWT里,服务端只要判断签名是否正确就可以判断出该用户是否可以访问该接口,当然JWT里的内容也不是无限多的,其他更多的信息我们就可以通过id去查数据库
了解了基本的区别和优缺点后,我们重点说说JWT的使用,目前用的比较多
JAVA中使用JWT
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
这里使用了一个叫JJWT(Java JWT)的库。
生成JWT
public String generateToken(String payload) {
return Jwts.builder()
.setSubject(payload)
.setExpiration(new Date(System.currentTimeMillis() + 10000))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
- 这里设置过期时间为10秒,因此生成的JWT只在10秒内能通过验证。
- 需要提供一个自定义的秘钥。
解码JWT
public String parseToken(String jwt) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(jwt)
.getBody()
.getSubject();
}
- 解码时会检查JWT的签名,因此需要提供秘钥。
验证JWT
public boolean isTokenValid(String jwt) {
try {
parseToken(jwt);
} catch (Throwable e) {
return false;
}
return true;
}
- JJWT并没有提供判断JWT是否合法的方法,但是在解码非法JWT时会抛出异常,因此可以通过捕获异常的方式来判断是否合法。
注册/登录
@GetMapping("/registration")
public String register(@RequestParam String username, HttpServletResponse response) {
String jwt = jwtService.generateToken(username);
response.setHeader(JWT_HEADER_NAME, jwt);
return String.format("JWT for %s :n%s", username, jwt);
}
- 需要为还没有获取到JWT的用户提供一个这样的注册或者登录入口,来获取JWT。
- 获取到响应里的JWT后,要在后续的请求里包含JWT,这里放在请求的Authorization头里。
验证JWT
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String jwt = httpServletRequest.getHeader(JWT_HEADER_NAME);
if (WHITE_LIST.contains(httpServletRequest.getRequestURI())) {
chain.doFilter(request, response);
} else if (isTokenValid(jwt)) {
updateToken(httpServletResponse, jwt);
chain.doFilter(request, response);
} else {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
private void updateToken(HttpServletResponse httpServletResponse, String jwt) {
String payload = jwtService.parseToken(jwt);
String newToken = jwtService.generateToken(payload);
httpServletResponse.setHeader(JWT_HEADER_NAME, newToken);
}
- 将验证操作放在Filter里,这样除了登录入口,其它的业务代码将感觉不到JWT的存在。
- 将登录入口放在WHITE_LIST里,跳过对这些入口的验证。
- 需要刷新JWT。如果JWT是合法的,那么应该用同样的Payload来生成一个新的JWT,这样新的JWT就会有新的过期时间,用此操作来刷新JWT,以防过期。
- 如果使用Filter,那么刷新的操作要在调用doFilter()之前,因为调用之后就无法再修改response了。
API
private final static String JWT_HEADER_NAME = "Authorization";
@GetMapping("/api")
public String testApi(HttpServletRequest request, HttpServletResponse response) {
String oldJwt = request.getHeader(JWT_HEADER_NAME);
String newJwt = response.getHeader(JWT_HEADER_NAME);
return String.format("Your old JWT is:n%s nYour new JWT is:n%sn", oldJwt, newJwt);
}
这时候API就处于JWT的保护下了。API可以完全不用感知到JWT的存在,同时也可以主动获取JWT并解码,以得到JWT里的信息。如上所示。