通过 JWT 生成用户登录唯一凭证
通过 JWT 生成用户唯一 Token 凭证,并提供反解析 Token 凭证位用户信息方法。应用于用户服务以及网关服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| @Slf4j public final class JWTUtil {
private static final long EXPIRATION = 86400L; public static final String TOKEN_PREFIX = "Bearer "; public static final String ISS = "zheng"; public static final String SECRET = "SecretKey039245678901232039487623456783092349288901402967890140939827";
public static String generateAccessToken(UserInfoDTO userInfo) { Map<String, Object> customerUserMap = new HashMap<>(); customerUserMap.put(USER_ID_KEY, userInfo.getUserId()); customerUserMap.put(user_KEY, userInfo.getUsername()); customerUserMap.put(REAL_NAME_KEY, userInfo.getRealName()); String jwtToken = Jwts.builder() .signWith(SignatureAlgorithm.HS512, SECRET) .setIssuedAt(new Date()) .setIssuer(ISS) .setSubject(JSON.toJSONString(customerUserMap)) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000)) .compact(); return TOKEN_PREFIX + jwtToken; }
public static UserInfoDTO parseJwtToken(String jwtToken) { if (StringUtils.hasText(jwtToken)) { String actualJwtToken = jwtToken.replace(TOKEN_PREFIX, ""); try { Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(actualJwtToken).getBody(); Date expiration = claims.getExpiration(); if (expiration.after(new Date())) { String subject = claims.getSubject(); return JSON.parseObject(subject, UserInfoDTO.class); } } catch (ExpiredJwtException ignored) { } catch (Exception ex) { log.error("JWT Token解析失败,请检查", ex); } } return null; } }
|
generateAccessToken(UserInfoDTO userInfo):这个方法用于生成一个新的JWT。它首先创建一个包含用户信息的Map,然后使用这个Map作为JWT的主体(subject)。然后,它设置JWT的签名算法(HS512),发行时间,发行人,以及过期时间。最后,它将生成的JWT添加到一个字符串前缀 “Bearer “ 并返回
parseJwtToken(String jwtToken):这个方法用于解析一个已存在的JWT。它首先检查输入的JWT是否存在,然后尝试解析JWT并获取其主体(subject)。如果JWT没有过期,那么它会将主题(subject)解析为一个 UserInfoDTO 对象并返回。如果JWT已经过期,那么它会捕获 ExpiredJwtException 异常并返回null。如果在解析过程中发生其他错误,那么它会记录一个错误消息并返回null
- StringUtils.hasText():Spring框架中的一个方法,它用于检查一个给定的字符串是否有文本。如果一个字符串为null,长度为0,或者只包含空白字符(比如空格、制表符或者换行符),那么
StringUtils.hasText()方法会返回false。否则,它会返回true
- 类中的
SECRET 应该被妥善保管,不应该在代码中硬编码,因为它是用于签名和验证JWT的关键信息。如果其他人获取了这个 SECRET,他们就可以伪造或篡改JWT。在实际的生产环境中,应该将这个 SECRET 存储在一个安全的地方,比如环境变量或者密钥管理系统中。
封装当前请求用户上下文
仅定义有且必须的,不限于下面的三个字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data @NoArgsConstructor @AllArgsConstructor @Builder public class UserInfoDTO {
private String userId;
private String username;
private String realName; }
|
定义用户参数上下文
这个类主要用于在同一线程中存储和检索用户信息。这是通过使用ThreadLocal实现的,ThreadLocal是Java中的一个类,它可以为每个线程提供一个独立的变量副本。在这个例子中,ThreadLocal变量USER_THREAD_LOCAL用于存储UserInfoDTO对象,这个对象包含了用户的详细信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
public final class UserContext {
private static final ThreadLocal<UserInfoDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void setUser(UserInfoDTO user) { USER_THREAD_LOCAL.set(user); }
public static String getUserId() { UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get(); return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserId).orElse(null); }
public static String getUsername() { UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get(); return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUsername).orElse(null); }
public static String getRealName() { UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get(); return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getRealName).orElse(null); }
public static void removeUser() { USER_THREAD_LOCAL.remove(); } }
|
setUser(UserInfoDTO user):这个方法接收一个UserInfoDTO对象作为参数,并将其存储在USER_THREAD_LOCAL中。这样,这个用户信息就可以在当前线程的后续执行中被检索和使用
getUserId(), getUsername(), getRealName():这些方法用于从USER_THREAD_LOCAL中检索用户信息,并返回用户的ID、用户名或真实姓名。如果USER_THREAD_LOCAL中没有用户信息,那么这些方法将返回null
removeUser():这个方法用于从USER_THREAD_LOCAL中移除用户信息。这通常在完成对用户信息的使用后调用,以避免不必要的内存占用
Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserId).orElse(null):这行代码使用了Java 8的Optional类和Stream API来处理可能为null的userInfoDTO对象
Optional类是Java 8引入的一个特性,主要用于解决空指针异常(NullPointerException)。它是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。这是一种更安全的处理可能为null的对象的方式,可以避免NullPointerException
- https://www.runoob.com/java/java8-optional-class.html)
Optional.ofNullable(userInfoDTO):这会创建一个Optional对象,它可能包含userInfoDTO(如果userInfoDTO不为null),或者不包含任何值(如果userInfoDTO为null)
.map(UserInfoDTO::getUserId):这是一个Stream操作,它会对Optional中的值应用getUserId方法。如果Optional中有userInfoDTO对象,那么它会返回一个新的Optional,这个Optional包含了getUserId方法的结果。如果Optional为空,那么它会返回一个空的Optional
.orElse(null):这会返回Optional中的值,如果Optional为空,那么它会返回null
关于TransmittableThreadLocal
TransmittableThreadLocal是阿里巴巴开源的一个类,它是Java的ThreadLocal类的一个扩展。它的主要作用是在多线程环境下,将某个变量的值从一个线程传递到另一个线程。与普通的ThreadLocal不同,TransmittableThreadLocal可以在线程切换时保持变量的值不变,从而实现线程间的值传递。这对于一些特定的应用场景,如线程池中的线程复用,非常有用。
引入依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.3</version> </dependency>
|
TransmittableThreadLocal是一个用于在多线程环境下传递值的工具类。它是InheritableThreadLocal的一个扩展,可以在线程之间传递值,并且支持线程池等场景。从线程安全的角度来看,TransmittableThreadLocal并不是完全线程安全的。尽管它提供了跨线程传递值的功能,但在某些情况下可能会出现线程安全问题。TransmittableThreadLocal在使用过程中需要特别注意以下几点:
- 对于普通的单线程应用,
TransmittableThreadLocal是线程安全的,因为每个线程都有自己独立的副本。
- 在多线程环境下,如果多个线程同时修改同一个
TransmittableThreadLocal实例的值,可能会导致数据混乱或错误的结果。
- 当使用线程池时,由于线程的重用,可能会导致
TransmittableThreadLocal值的泄漏或错乱。这是因为线程池中的线程在执行完任务后并不会被销毁,而是被放回线程池中等待下一次任务。如果没有正确清理TransmittableThreadLocal的值,那么下次使用该线程时可能会获取到上一次的残留值。
- 虽然
TransmittableThreadLocal提供了在多线程环境下传递值的功能,但在使用时需要注意线程安全性,并且合理管理和清理TransmittableThreadLocal的值,以避免潜在的线程安全问题。
用户上下文拦截器
定义了一个名为UserTransmitFilter的过滤器,它实现了jakarta.servlet.Filter接口。这个过滤器的主要作用是在HTTP请求处理过程中,从请求头中获取用户信息,并将这些信息存储在UserContext中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
public class UserTransmitFilter implements Filter {
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String userId = httpServletRequest.getHeader(UserConstant.USER_ID_KEY); if (StringUtils.hasText(userId)) { String userName = httpServletRequest.getHeader(UserConstant.user_KEY); String realMame = httpServletRequest.getHeader(UserConstant.REAL_NAME_KEY); if (StringUtils.hasText(userName)) { userName = URLDecoder.decode(userName, UTF_8); } if (StringUtils.hasText(realMame)) { realMame = URLDecoder.decode(realMame, UTF_8); } UserInfoDTO userInfoDTO = UserInfoDTO.builder() .userId(userId) .username(userName) .realName(realMame) .build(); UserContext.setUser(userInfoDTO); } try { filterChain.doFilter(servletRequest, servletResponse); } finally { UserContext.removeUser(); } } }
|
doFilter():这是Filter接口的核心方法,它在每次HTTP请求处理时都会被调用
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;:这行代码将ServletRequest转换为HttpServletRequest,以便能够使用HttpServletRequest提供的方法
filterChain.doFilter(servletRequest, servletResponse);:这行代码将请求传递给过滤器链中的下一个过滤器或目标资源(如一个Servlet或一个静态资源)。这是过滤器的典型用法,它允许过滤器在请求到达目标资源之前和之后执行一些处理
UserContext.removeUser();:这行代码在请求处理完成后清理UserContext中的用户信息。如果不清理用户信息,那么这些信息可能会在后续的请求中被错误地使用
参考资料
http://t.csdnimg.cn/DV3aF
http://t.csdnimg.cn/EfWcP
TransmittableThreadLocal