使用JWT、拦截器与ThreadLocal实现在任意位置获取Token中的信息,并结合自定义注解实现对方法的鉴权
1. 简介
1.1 JWT
JWT,即JSON Web Token,是一种用于在网络上传递声明的开放标准(RFC 7519)。JWT 可以在用户和服务器之间传递安全可靠的信息,通常用于身份验证和信息交换。
- 声明(Claims): JWT 包含一组称为声明的信息,声明描述了一些数据。有三种类型的声明:
- 注册声明(Registered Claims):这是一些预定义的声明,包括标准的声明,例如"iss"(签发者)、"sub"(主题)和"exp"(过期时间)等。
- 公共声明(Public Claims):这些声明是由使用 JWT 的双方定义的,并且必须遵守一定的规定,以防止冲突。
- 私有声明(Private Claims):这些是自定义的声明,用于在双方之间共享信息。
- 编码结构: JWT 由三部分组成,使用点号(.)分隔开:
- Header(头部):包含了令牌的元数据,例如算法和令牌类型。
- Payload(载荷):包含了声明,即实际传输的数据。
- Signature(签名):用于验证发送方的身份以及确保消息的完整性。签名由前两部分的内容和密钥组成,以防篡改。
- 编码方法: JWT 可以使用不同的编码方法,包括:
- Base64 URL encoding:用于编码头部和载荷。
- HMACSHA256:用于生成签名,以确保消息完整性。
- 使用场景:
- 身份验证:用户登录后,服务器生成一个包含用户信息的JWT,并将其发送给客户端。客户端在后续请求中将JWT包含在请求头中,服务器验证JWT以确保请求的合法性。
- 信息交换:JWT还可以包含其他信息,例如用户的角色、访问权限等,这些信息可以在不需要查询数据库的情况下进行快速访问。
1.2 拦截器
拦截器(Interceptor)是一种用于处理请求的机制,可以让你在请求的处理过程中进行预处理和后处理。拦截器类似于过滤器(Filter),但相比过滤器,拦截器更加专注于处理控制器层面的请求,可以对处理器的执行过程进行更加细粒度的控制。
使用场景:
- 身份验证和授权:拦截器可以用于检查用户是否已经登录,是否具有足够的权限访问某个资源。
- 日志记录:拦截器可以用于记录请求和响应的日志信息,方便调试和监控。
- 性能监控:通过拦截器,你可以记录请求的处理时间,帮助进行性能监控。
- 执行顺序:拦截器可以配置多个,它们的执行顺序由配置时的顺序决定。
- 异步请求:拦截器也能处理异步请求,需要实现
AsyncHandlerInterceptor
接口。
1.3 ThreadLocal
ThreadLocal
是 Java 中的一个类,主要用于提供了线程本地变量。 ThreadLocal
创建的变量只能被当前线程访问,其他线程无法直接访问或修改它。ThreadLocal
主要用于保持线程封闭性,即每个线程都拥有自己独立的变量副本,不同线程之间不会相互干扰。
应用场景:
- 线程安全的数据传递:在多线程环境中,通过
ThreadLocal
可以轻松地将数据在方法调用间传递,而无需将数据作为参数传递。 - 数据库连接管理:在数据库连接的管理中,可以使用
ThreadLocal
来存储每个线程的数据库连接,确保每个线程使用的是自己的连接。 - 事务管理:
ThreadLocal
可以用于事务管理,确保事务的一致性。
注意:
- ThreadLocal可以在虚拟线程环境下使用
ThreadLocal
应该谨慎使用,避免滥用。过多的使用可能导致代码难以理解和维护。- 避免在
ThreadLocal
中存储大对象,以防止内存泄漏。- 在使用线程池时,需要注意清理
ThreadLocal
,以防止线程复用时出现数据污染。
2. 代码实战
2.1 引入依赖
<!-- java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>${jwt.version}</version> </dependency>
2.2 获取token的函数
推荐使用@Value
的方式从application.yml
中获取密钥和过期时间
// refresh-token密钥 @Value("${refresh-token.secret}") private String REFRESH_TOKEN_SECRET; // refresh-token过期时间 @Value("${refresh-token.expire-time}") private int REFRESH_TOKEN_EXPIRE_TIME; // refresh-token密钥 @Value("${access-token.secret}") private String ACCESS_TOKEN_SECRET; // refresh-token过期时间 @Value("${access-token.expire-time}") private int ACCESS_TOKEN_EXPIRE_TIME; /** * 获取access_token * @param userId * @param userType * @return */ private String getAccessToken(int userId, int userType){ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR, ACCESS_TOKEN_EXPIRE_TIME); return JWT.create() .withClaim("userId", userId) .withClaim("userType", userType) .withExpiresAt(calendar.getTime()) .sign(Algorithm.HMAC512(ACCESS_TOKEN_SECRET)); } /** * 获取refresh_token * @param userId * @param userType * @return */ private String getRefreshToken(int userId, int userType){ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR, REFRESH_TOKEN_EXPIRE_TIME); return JWT.create() .withClaim("userId", userId) .withClaim("userType", userType) .withExpiresAt(calendar.getTime()) .sign(Algorithm.HMAC512(REFRESH_TOKEN_SECRET)); }
2.3 ThreadLocal工具类
/** * <p> * 用户SessionVO * </p> * * @author jonil * @since 2023/11/10 16:03 */ @Data public class UserSessionVO { // 用户id Integer userId; // 用户类型 Integer userType; }
/** * <p> * 用户session上下文 * </p> * * @author jonil * @since 2023/11/10 16:02 */ public class UserSessionContext { private static ThreadLocal<UserSessionVO> userSessionVOThreadLocal = new ThreadLocal<>(); public static void set(UserSessionVO userSessionVO) { userSessionVOThreadLocal.set(userSessionVO); } public static UserSessionVO get() { return userSessionVOThreadLocal.get(); } public static void remove() { userSessionVOThreadLocal.remove(); } }
2.4 拦截器
此处拦截器为处理HTTP请求前
中
后
各阶段需要做的操作。HTTP请求进来的时候将JWT解析的数据放进ThreadLocal,出去的时候需要将数据从ThreadLocal移除,否则会造成内存泄漏。
/** * <p> * JWT拦截器 * </p> * * @author jonil * @since 2023/11/10 15:55 */ public class JWTInterceptor implements HandlerInterceptor { // refresh-token密钥 @Value("${access-token.secret}") private String ACCESS_TOKEN_SECRET; /** * 将用户基本信息添加到ThreadLocal * @param request * @param response * @param handler * @return */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String accessToken = request.getHeader("Authorization"); JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(ACCESS_TOKEN_SECRET)).build(); DecodedJWT decodedJWT = jwtVerifier.verify(accessToken); Integer userId = decodedJWT.getClaim("userId").asInt(); Integer userType = decodedJWT.getClaim("userType").asInt(); UserSessionVO userSessionVO = new UserSessionVO(); userSessionVO.setUserId(userId); userSessionVO.setUserType(userType); UserSessionContext.set(userSessionVO); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) { } /** * 将用户的基本信息从ThreadLocal移除 * @param request * @param response * @param handler * @param ex */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { UserSessionContext.remove(); } }
2.5 使用
在需要用到JWT内容的地方,使用以下代码即可获取对应的内容
UserSessionVO userSessionVO = UserSessionContext.get(); Integer userId = userSessionVO.getUserId();
3. 进阶
3.1 结合自定义注解实现对方法的鉴权
角色枚举类
public enum UserType { systemAdmin(0), userAdmin(1), maintenancePersonnel(2), vipUser(3), user(4), none(5); UserType(int code) { this.code = code; } private int code; public int getCode() { return code; } }
自定义注解类
/** * <p> * 自定义校验注解 * </p> * * @author jonil * @since 2023/11/13 20:05 */ @Documented @Target({ ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {PermissionValidator.class}) public @interface Permission { UserType type() default UserType.none; /** * 是否强制校验 * @return */ boolean required() default true; /** * 校验不通过时的报错信息 * @return */ String message() default "权限不足!"; /** * 分组 * @return */ Class<?>[] groups() default {}; /** * bean的负载 * @return */ Class<? extends Payload>[] payload() default {}; }
自定义注解实现类
/** * <p> * 自定义注解实现 * </p> * * @author jonil * @since 2023/11/13 20:05 */ public class PermissionValidator implements ConstraintValidator<Permission, UserType> { @Override public void initialize(Permission constraintAnnotation) { ConstraintValidator.super.initialize(constraintAnnotation); } /** * 判断当前是否有权限 * @param userType object to validate * @param context context in which the constraint is evaluated * * @return */ @Override public boolean isValid(UserType userType, ConstraintValidatorContext context) { return UserSessionContext.get().getUserType() <= userType.getCode(); } }
使用
/** * 获取信息接口 */ @Permission(type = UserType.user) @GetMapping("/info") public R getInfo() { return R.success(service.getInfo()); }
注解会从ThreadLocal获取当前用户的类型,然后进行比对,当然你的判断逻辑可以比这个更加复杂,只需要符合业务实现就好了。
4. 注意
倘若你是在微服务环境或分布式环境下使用这一套逻辑,需要注意在网关(流量网关、业务网关)和OpenFeign中携带原生的Header,否则获取不到该用户的信息。
本文作者:护发师兄
本文链接:https://www.cnblogs.com/jonil/p/17831566.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步