基于JWT+拦截器+ThreadLocal的登陆拦截实现

一、JWT

在这里插入图片描述
导入依赖后创建JWT工具类创建JWT对象
重点是工具类的createJWTparseJWT方法如何实现

createJWT

public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);
        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);
        return builder.compact();
    }

代码的解释

  1. SignatureAlgorithm 签名算法选择:
    在这里,签名算法被设置为 HS256,表示 HMAC-SHA256 算法,这是一种对称加密算法,它使用相同的密钥来加密和解密数据。

  2. 生成 JWT 的过期时间:
    ttlMillis 参数用来指定 JWT 的有效期(以毫秒为单位)。有效期是从当前时间开始计算的,并在有效期过后 JWT 将失效。在代码中,通过 System.currentTimeMillis() 获取当前时间,并将其与 ttlMillis 相加,得到 JWT 的过期时间。

  3. 设置 JWT 的 body(Claims):
    在 JWT 的载荷部分,可以设置一些自定义的声明(Claims),比如用户 ID、角色、权限等信息。这些声明被存储在 claims 参数中,通过 .setClaims(claims) 方法设置到 JWT 中。

  4. 设置签名和秘钥:
    使用 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) 方法设置签名算法和秘钥。这里将使用指定的秘钥对 JWT 进行签名,以确保 JWT 的完整性和真实性。

  5. 设置过期时间:
    最后,通过 .setExpiration(exp) 方法设置 JWT 的过期时间。一旦 JWT 过了指定的过期时间,将不再被认为是有效的。

调用该方法
可以看到SecretKey是通过调用jwtProperties.getAdminSecretKey(),所以我们使用前也是需要自动装配这个对象的

String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

parseJWT方法

 public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }
  1. 得到 DefaultJwtParser:
    通过 Jwts.parser() 获取一个 JWT 解析器,默认情况下它会使用标准的解析规则。

  2. 设置签名的秘钥:
    使用 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) 方法设置签名的秘钥。这个秘钥必须和创建 JWT 时使用的秘钥一致,否则解析将会失败。

  3. 解析 JWT:
    使用 .parseClaimsJws(token) 方法解析传入的 JWT。这个方法会解析 JWT 并验证其签名的有效性。如果 JWT 的签名有效,就可以通过 .getBody() 方法获取 JWT 的载荷部分,即声明信息。

  4. 返回声明信息:
    将解析得到的声明信息返回给调用者。

拦截器

JwtTokenAdminInterceptor类(自定义的)需要实现HandlerInterceptor接口然后实现其中preHandle方法
HandlerInterceptor接口有三个方法待实现分别是preHandlepostHandle,afterCompletion

preHandle:
这个方法在请求处理之前被调用,也就是在 Controller 方法调用之前。它允许对请求进行预处理,比如进行身份验证、日志记录、性能监控等。如果在这个方法中返回 true,则继续执行后续的拦截器和请求处理器;如果返回 false,则结束请求的执行,不会进入后续的拦截器和请求处理器,且请求将被中止。

postHandle:
这个方法在请求处理之后、视图渲染之前被调用。它允许对请求进行后处理,但是并不能改变请求的结果。这个方法中可以对响应内容进行修改,比如添加一些公共的数据等。

afterCompletion:
这个方法在整个请求处理完成后,也就是在视图渲染完成后被调用。它允许进行资源清理工作,比如释放资源、记录日志等。它的调用时机是在请求处理完成后,包括 postHandle 方法和视图的渲染都已经完成了。这个方法可以用于进行一些清理操作,无论请求处理过程中是否发生了异常,都会被调用。

preHandle方法逻辑

通过parseJWT方法解析token
判断解析后的id和ThreadLocal是否有对应id来判断相应的权限,比如可以是登录拦截器,在这个方法里面实现检查有没有登录的逻辑

public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("当前线程的id"+Thread.currentThread().getId());
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

WebMvcConfigurer配置类

拦截器方法实现后就需要在WebMvcConfiguration(继承WebMvcConfigurationSupport类)注册自定义拦截器
下面分别配置了LoginInterceptor()拦截器和RefreshTokenInterceptor拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/voucher/**",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

ThreadLocal

想实现每一个线程都有自己的专属本地变量该如何解决呢?
JDK 中自带的ThreadLocal类正是为了解决这样的问题

访问ThreadLocal变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal是一个类

也可以传入一个对象 对象里面可以包括用户id 权限信息等

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, ThreadLocal!");
String value = threadLocal.get();

ThreadLocal原理

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。
inheritableThreadLocals 则适用于父子线程之间需要传递值的场景。
ThreadLocal类的set()方法

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,
而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
由此可知一个线程可以有多个ThreadLocal对象

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值