JWT令牌验证
[toc] 老师博客
验证流程
- 用户使用账号和面发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器.
结构
JWT包含了三部分:
- Header 头部(标题包含了令牌的元数据, 并且包含签名和/或加密算法的类型)
- Payload 负载 (类似于飞机上承载的物品)
- Signature 签名/签证
依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
<!-- 这个不是必需,但是可以简化操作,在下文会用到 -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
工具类
用于生成和验证的工具类, 这个类复制好以后就直接调用就好了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// src\main\java\com\neuedu\utils\JwtUtils.java
package com.neuedu.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
@Slf4j
public class JwtUtils {
// 过期时间5分钟
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* 1. 校验token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
// 加密算法(在此规定用户名是唯一的)
// secret 加盐
Algorithm algorithm = Algorithm.HMAC256(secret);
// 通过定义的算法产生 jwt校验对象
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
// 解析jwt
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 2. 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 3. 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
}
登录校验过程
主要是为围绕对登录的Controller的修改, 修改的时候会用到拦截器和令牌生成的算法.
拦截器
拦截器都放在 neuedu\interceptor
下.
主要是对登录的前置拦截器进行操作. 主要操作如下
- 如果当前是登录的链接,那么就不做检查,直接通行
- 从header中取出token,如果没有,就返回false
- 如果token不合法(利用JwtUtils.verify进行校验),返回结果
注意正确的时候直接返回true即可, 在出错的时候还是要对response进行输入, 然后返回报错信息.
令牌一般放在请求头中, 因为不是所有请求都有请求体. 这也意味着在Postman中进行测试时, token要加到Header里面.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// src\main\java\com\neuedu\interceptor\LoginInterceptor.java
package com.neuedu.interceptor;
import com.alibaba.fastjson.JSON;
import com.neuedu.constrant.GlobalConstrant;
import com.neuedu.constrant.ResultConstrant;
import com.neuedu.core.Result;
import com.neuedu.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* PROJECT_NAME: neuedu-his
* Created by DawnK on 2020/7/8
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
// 前置拦截器,在访问控制器之前,进行令牌的校验
// 参数是servlet对象
public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler) throws java.lang.Exception {
// 获取路径(getServletPath 或者 getRequestURI),因为有的需要用token,有的不需要
String path = request.getServletPath();
log.info("getpath" + path);
if (StringUtils.isNotEmpty(path) && path.contains("/user/login")) {
return true;
}
// 1. 是否有令牌
//
String token = request.getHeader("token");
log.info("findtoken ", token);
// 看是否为空
if (StringUtils.isEmpty(token)) {
Result result = Result.fail(ResultConstrant.TOKEN_ERROR, "token error");
response.getWriter().write(JSON.toJSONString(result));
return false;
} else {
// 解析令牌,查看其是否合法
String username = JwtUtils.getUsername(token);
if (JwtUtils.verify(token, username, GlobalConstrant.TOKEN_SECRET)) {
return true;
} else {
Result result = Result.fail(ResultConstrant.TOKEN_ERROR, "token invalid");
response.getWriter().write(JSON.toJSONString(result));
return false;
}
}
}
public void postHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler, @org.springframework.lang.Nullable org.springframework.web.servlet.ModelAndView modelAndView) throws java.lang.Exception {
log.info("响应拦截器");
}
public void afterCompletion(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, java.lang.Object handler, @org.springframework.lang.Nullable java.lang.Exception ex) throws java.lang.Exception {
log.info("后置拦截器");
}
}
配置类
配置所有的请求, 都要经过拦截器
1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns() 就是配置哪些请求路过拦截器
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
}
}
TokenVO封装
这一层封装没有太看出作用. 还要深入研究, 可能是让token的类型不要如此单薄.
1
2
3
4
5
// src\main\java\com\neuedu\entity\vo\TokenVO.java
@Data
public class TokenVO {
private String token;
}
控制类
在之前的设计中, 登录成功之后, 就返回Result.success()就结束了, 但是在加入令牌之后, 必须在登录成功的时候生成token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 截取了一部分,主要是登录
// src\main\java\com\neuedu\controller\UserController.java
@PostMapping("/login")
public Result login(@RequestBody LoginParams loginParams) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("username", loginParams.getUsername());
queryWrapper.eq("password", loginParams.getPassword());
List<User> list = userMapper.selectList(queryWrapper);
if (list.size() > 0) {
// 登录成功返回token
String token = JwtUtils.sign(loginParams.getUsername(), GlobalConstrant.TOKEN_SECRET);
TokenVO tokenVO = new TokenVO();
tokenVO.setToken(token);
return Result.success(tokenVO);
} else {
return Result.fail("登录失败");
}
}
测试方法
这个需要使用postman进行测试, 在没有令牌的情况下尝试访问非登录界面(理论上应该会拒绝访问). 然后登录, 登录成功后获得token, 然后使用这个token(切记要在postman上加到Header上)尝试访问其他的界面(此时应该成功).
至此后端的令牌验证已经结束了, 下面还需要前端的验证.