springboot 整合 JWT 和请求拦截,实现利用 token 做请求安全拦截校验,且实现阻止并发登录

目录

一、导入依赖

二、编写 jwt 工具类,实现生成 token 和解析 token

三、在登录请求中向redis中添加token信息

1、先注入redis的接口类

2、在登录方法中生成token并插入redis,有效期一天

四、实现请求拦截器

1、编写自定义的请求拦截器

2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法

五、测试总结

1、请求拦截

①正确 token

②错误的token

③空token

④从redis中删掉token

2、阻止并发登录

3、总结


一、导入依赖

导入 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

传送门  ==> 前后端分离项目整合spring security,自定义登录验证接口,并精准有效阻止并发登录

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值