目录
二、编写 jwt 工具类,实现生成 token 和解析 token
2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法
一、导入依赖
导入 jwt 的依赖
<!-- jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
二、编写 jwt 工具类,实现生成 token 和解析 token
jwt 工作流程
可以传入具体的用户信息,方便解析校验
// Jwt工具类
public class JwtUtil {
//private static long time = 1000*10; // token 有效期为10秒
private static long time = 1000*60*60*24; // token 有效期为一天
private static String signature = "admin";
// 生成token ,三个参数是我实体类的字段,可根据自身需求来传,一般只需要用户id即可
public static String createJwtToken(String operNo,String operName ,String organNo){
JwtBuilder builder = Jwts.builder();
String jwtToken = builder
// header
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// payload 载荷
.claim("operNo",operNo)
.claim("operName",operName)
.claim("organNo",organNo)
.claim("date",new Date())
.setSubject(operNo)
.setExpiration(new Date(System.currentTimeMillis()+time))
.setId(UUID.randomUUID().toString())
// signature 签名信息
.signWith(SignatureAlgorithm.HS256,signature)
// 用.拼接
.compact();
return jwtToken;
}
// 验证token是否还有效,返回具体内容
public static Claims checkToken(String token){
if(token == null){
return null;
}
JwtParser parser = Jwts.parser();
try {
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
System.out.println(claims.get("operNo"));
System.out.println(claims.get("operName"));
System.out.println(claims.get("organNo"));
System.out.println(claims.getId());
System.out.println(claims.getSubject()); // 签名
System.out.println(claims.getExpiration()); // 有效期
// 如果解析token正常,返回claims
return claims;
}catch (Exception e) {
// 如果解析token抛出异常,返回null
return null;
}
}
}
三、在登录请求中向redis中添加token信息
1、先注入redis的接口类
如果不知道怎么配置redis的可以去看这篇文章 => springboot整合redis并实现mybatis二级缓存
@Autowired
StringRedisTemplate redisTemplate;
2、在登录方法中生成token并插入redis,有效期一天
redis中key值使用字符串 "operToken" 加上用户 id 拼接而成,value 就是 token 的具体内容
也可以插入一个map,redis的键依旧为字符串 "operToken" 加上用户 id 拼接而成,map中的键为token版本号(可以更好的验证并发登录替换了token,不同的随机数即可),值为token的具体内容
// 插入JWT的token
String token = JwtUtil.createJwtToken(loginOper.getOperNo(),loginOper.getOperName(),loginOper.getOrganNo());
loginOper.setToken(token);
// 将JWT的token存入redis,有效期一天
redisTemplate.opsForValue().set("operToken"+loginOper.getOperNo(),token,1, TimeUnit.DAYS);
四、实现请求拦截器
三个请求拦截器
/**前置处理:在业务处理器处理请求之前被调用*/
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception;
/**中置处理:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView ,现在这个很少使用了*/
void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception;
/**后置处理:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等*/
void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception;
在请求处理之前,切面的给每个请求做一个校验,校验请求头中的token信息是否有效且信息正确
1、编写自定义的请求拦截器
先取出请求头中token的信息,然后判断这个token是否存在,如果存在再去校验这个token是否真实有效,如果有效再取出token中的用户信息,根据用户id来找出redis中的token,再判断redis中的token是否存在,不存在则说明过期了,如果存在则继续对比请求头中的token是否一致,不一致的话则说明token错误或者被别人并发登录,这里就没有办法判断具体是哪种情况,所以用map集合来存入redis就可以更大程度的判断出(先判断最新版本号的token是否和请求头中的相同,再判断过往版本号的token是否有相同,如果有则说明并发登录,如果没有则token错误。加入map之前需要判断map的大小是否超过一定数值,比如5,超过5则删除之前的数据;对map里键值对单独设置过期时也可以)当然,这还是不够精准
@Component // @Component注解一定要加上
public class Interceptor implements HandlerInterceptor {
// 注入redis
@Autowired
StringRedisTemplate redisTemplate;
// 处理请求之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 取出请求头中Authorization的信息,就是token内容,接下来就是各种判断
String requestToken = request.getHeader("Authorization");
if(!StringUtils.isEmpty(requestToken)){
Claims claims = JwtUtil.checkToken(request.getHeader("Authorization"));
if (claims != null) {
String token = redisTemplate.opsForValue().get("operToken"+claims.get("operNo"));
if(Boolean.TRUE.equals(redisTemplate.hasKey("operToken" + claims.get("operNo")))){
if(requestToken.equals(token)){
// token正确
return true;
}else {
// token错误,判为并发登录,挤下线
// 对应的修改响应头的状态,用于前端判断做出相应的策略
response.setStatus(411);
return false;
}
}else {
// token不存在于redis中,已过期
response.setStatus(410);
return false;
}
}
// 解析token中的用户信息claims为null
response.setStatus(409);
return false;
}
// requestToken为空
response.setStatus(409);
return false;
}
// 处理请求之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("处理请求之后执行");
}
}
2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法
@Component
public class InterceptorConfig implements WebMvcConfigurer {
// 注入自定义拦截器
@Autowired
private Interceptor interceptor;
// 重写添加拦截器方法
@Override
public void addInterceptors(InterceptorRegistry registry){ // InterceptorRegistry 为拦截器注册对象
registry.addInterceptor(interceptor) // 注册自定义拦截器
.addPathPatterns("/sys/basic-api/**")// 拦截的路径
.excludePathPatterns(); // 不拦截的路径
}
}
五、测试总结
1、请求拦截
给刚刚的自定义拦截器加上输出
然后进行测试
①正确 token
可以发现是先打印输出结果,再执行请求,创建SqlSession
②错误的token
因为是错误的token,token校验是通过不了的,因此返回的用户信息也为空,打印完直接结束,没有执行请求
③空token
打印完直接结束,没有执行请求
④从redis中删掉token
redis中查不到token,判定为token过期
2、阻止并发登录
因为每次生成的token中都有随机id,所以每次登录时生成的token肯定都不一样
所以并发登陆以后,之前登录的用户再一次发送请求就会被验证拦截,根据返回的411状态码来判断账号已在别处登录
3、总结
利用token来实现请求拦截校验是完全没有问题的,但是现实中可以尽可能的多加几层校验,确保足够的安全
token的存储方式还是有很大的改进空间,虽然也能勉强实现阻止多并发登录,但是不够完善,不能精准判断出具体的错误情况,所以由此引出 spring security