通过JWT实生成Token

通过 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 {

// Token的过期时间(秒)
private static final long EXPIRATION = 86400L;
// Token的前缀
public static final String TOKEN_PREFIX = "Bearer ";
// Token的发行人
public static final String ISS = "zheng";
// 用于签名Token的密钥
public static final String SECRET = "SecretKey039245678901232039487623456783092349288901402967890140939827";

/**
* 生成用户 Token
* @param userInfo 用户信息
* @return 用户访问 Token
*/
public static String generateAccessToken(UserInfoDTO userInfo) {
// 创建一个Map来存储用户信息
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());
// 使用用户信息Map作为主题来创建JWT
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();
// 返回带有前缀的JWT
return TOKEN_PREFIX + jwtToken;
}

/**
* 解析用户 Token
* @param jwtToken 用户访问 Token
* @return 用户信息
*/
public static UserInfoDTO parseJwtToken(String jwtToken) {
// 检查JWT是否存在
if (StringUtils.hasText(jwtToken)) {
// 去掉JWT的前缀
String actualJwtToken = jwtToken.replace(TOKEN_PREFIX, "");
try {
// 解析JWT并获取主题
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(actualJwtToken).getBody();
Date expiration = claims.getExpiration();
// 检查JWT是否过期
if (expiration.after(new Date())) {
String subject = claims.getSubject();
// 将主题解析为UserInfoDTO对象
return JSON.parseObject(subject, UserInfoDTO.class);
}
} catch (ExpiredJwtException ignored) {
// 如果JWT已过期,忽略异常
} catch (Exception ex) {
// 如果在解析过程中发生其他错误,记录错误消息
log.error("JWT Token解析失败,请检查", ex);
}
}
// 如果JWT不存在或解析失败,返回null
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 {

/**
* 用户 ID
*/
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 {

// 使用TransmittableThreadLocal来存储用户信息,这样可以在同一线程中的不同方法间共享数据
private static final ThreadLocal<UserInfoDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();

/**
* 设置用户至上下文
* @param user 用户详情信息
*/
public static void setUser(UserInfoDTO user) {
// 将用户信息存储在ThreadLocal变量中
USER_THREAD_LOCAL.set(user);
}

/**
* 获取上下文中用户 ID
* @return 用户 ID
*/
public static String getUserId() {
// 从ThreadLocal变量中获取用户信息
UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
// 如果用户信息存在,返回用户ID,否则返回null
return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUserId).orElse(null);
}

/**
* 获取上下文中用户名称
* @return 用户名称
*/
public static String getUsername() {
// 从ThreadLocal变量中获取用户信息
UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
// 如果用户信息存在,返回用户名,否则返回null
return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getUsername).orElse(null);
}

/**
* 获取上下文中用户真实姓名
* @return 用户真实姓名
*/
public static String getRealName() {
// 从ThreadLocal变量中获取用户信息
UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
// 如果用户信息存在,返回用户真实姓名,否则返回null
return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getRealName).orElse(null);
}

/**
* 清理用户上下文
*/
public static void removeUser() {
// 从ThreadLocal变量中移除用户信息
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 {
// 将ServletRequest转换为HttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
// 从请求头中获取用户ID
String userId = httpServletRequest.getHeader(UserConstant.USER_ID_KEY);
// 检查用户ID是否存在
if (StringUtils.hasText(userId)) {
// 从请求头中获取用户名和真实姓名
String userName = httpServletRequest.getHeader(UserConstant.user_KEY);
String realMame = httpServletRequest.getHeader(UserConstant.REAL_NAME_KEY);
// 如果用户名和真实姓名存在,进行URL解码
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中
UserContext.setUser(userInfoDTO);
}
try {
// 将请求传递给过滤器链中的下一个过滤器或目标资源
filterChain.doFilter(servletRequest, servletResponse);
} finally {
// 请求处理完成后,清理UserContext中的用户信息
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


通过JWT实生成Token
https://lzhengjy.github.io/2024/06/20/通过JWT实生成Token/
作者
Zheng
发布于
2024年6月20日
许可协议