这里写目录标题
一、JWT
导入依赖后创建JWT工具类创建JWT对象
重点是工具类的createJWT和parseJWT方法如何实现
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();
}
代码的解释
-
SignatureAlgorithm 签名算法选择:
在这里,签名算法被设置为 HS256,表示 HMAC-SHA256 算法,这是一种对称加密算法,它使用相同的密钥来加密和解密数据。 -
生成 JWT 的过期时间:
ttlMillis
参数用来指定 JWT 的有效期(以毫秒为单位)。有效期是从当前时间开始计算的,并在有效期过后 JWT 将失效。在代码中,通过System.currentTimeMillis()
获取当前时间,并将其与ttlMillis
相加,得到 JWT 的过期时间。 -
设置 JWT 的 body(Claims):
在 JWT 的载荷部分,可以设置一些自定义的声明(Claims),比如用户 ID、角色、权限等信息。这些声明被存储在claims
参数中,通过.setClaims(claims)
方法设置到 JWT 中。 -
设置签名和秘钥:
使用.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
方法设置签名算法和秘钥。这里将使用指定的秘钥对 JWT 进行签名,以确保 JWT 的完整性和真实性。 -
设置过期时间:
最后,通过.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;
}
-
得到 DefaultJwtParser:
通过Jwts.parser()
获取一个 JWT 解析器,默认情况下它会使用标准的解析规则。 -
设置签名的秘钥:
使用.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
方法设置签名的秘钥。这个秘钥必须和创建 JWT 时使用的秘钥一致,否则解析将会失败。 -
解析 JWT:
使用.parseClaimsJws(token)
方法解析传入的 JWT。这个方法会解析 JWT 并验证其签名的有效性。如果 JWT 的签名有效,就可以通过.getBody()
方法获取 JWT 的载荷部分,即声明信息。 -
返回声明信息:
将解析得到的声明信息返回给调用者。
拦截器
在JwtTokenAdminInterceptor类(自定义的)需要实现HandlerInterceptor接口然后实现其中preHandle方法
HandlerInterceptor接口有三个方法待实现分别是preHandle,postHandle,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()方法