Spring:登录后逻辑:JWT、拦截器与ThreadLocal

spring:登录后逻辑:JWT、拦截器与ThreadLocal

JWT

做好网页登录了?为什么一刷新就要重新登录?如何才能像各大平台一样登录后一段时间之内不用重复登录?
也许你需要JWT令牌这个东西。

JWT 全名是 JSON Web Token,是一个用于在用户和服务器之间安全传递信息的东西,尤其常用于“登录后的身份认证”。

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJ4eXoifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

它其实分三部分,用 . 分隔:

  1. Header(头部):算法和类型

  2. Payload(载荷):真正传递的用户信息,比如 userId、权限等级、过期时间等

  3. Signature(签名):用来验证这个 token 是不是被伪造的

为什么说需要JWT呢?

我们为了让你的网站登录之后短期内不需要登录,我们可以把用户的信息浓缩进一个“卡片“内,就跟现在的NFC卡片一样,只要向机器上刷卡,机器就知道你是谁了。
JWT也同理,后端通过用户信息+过期时间+签名然后使用一些算法就可生成一段加密的文字。
当前端用户登录成功之后,后端把这个加密文字交给前端,前端执行某些操作:比如获取个人信息,点赞,评论的时候,把这一串加密的文字交给后端,后端拿着签名进行解密就获取了请求者的个人信息,于是就知道是谁发起请求的。

用法

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

工具类

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
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {
private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24; // 1天
private static final Key KEY = Keys.hmacShaKeyFor("1234567890abcdef1234567890abcdef".getBytes());

// 创建 Token
public static String createToken(String userId) {
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
.signWith(KEY, SignatureAlgorithm.HS256)
.compact();
}

// 解析 Token
public static String getUserId(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}

// 判断 Token 是否过期
public static boolean isExpired(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
}

发送令牌

1
2
3
4
5
6
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
// 假设验证用户名密码成功
String token = JwtUtil.createToken(dto.getUserId());
return Result.success(token);
}

到这里,令牌已经交到前端的手里了,除了登录注册以外,用户一系列的行为:点赞,评论,收藏等等都需要带上他的令牌与后端交互,难道每一个接口都要检测一下用户有没有令牌吗?
显然不是,在Spring中,有一个名为拦截器的东西。

拦截器

拦截器,顾名思义,就是用来拦截的。

创建拦截器

1
2
3
4
5
6
7
8
9
10
11
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token == null || JwtUtil.isExpired(token)) {
response.setStatus(401); // 未授权
return false;
}
return true;
}
}

形参

1. HttpServletRequest request 表示当前的请求

获取请求头、参数、URL、方法名:

1
2
3
4
request.getHeader("Authorization")
request.getRequestURI()
request.getMethod()
request.getParameter("username")

拿登录信息、IP、Session、Cookie 等等:

1
2
3
request.getRemoteAddr()
request.getSession()
request.getCookies()
2. HttpServletResponse response

表示 当前响应,你可以用它:

  • 修改响应状态码:
1
response.setStatus(401) // 未授权
  • 设置返回数据(例如拦截后自己返回错误信息):
1
2
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"未登录\"}");

一般配合 return false 使用,用来终止请求继续向下执行

3. Object handler

写这篇文章的时候我也不太清楚,查了一下

  • 判断被访问的方法是哪个 Controller 的哪个方法
  • 拿到方法上的注解(比如自定义注解做权限控制)
1
2
3
4
5
6
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
String methodName = method.getName();
MyAuth auth = method.getAnnotation(MyAuth.class);
}

返回值

返回值是boolean,想必你已经知道答案了,为true的时候放行,false拦截。

配置拦截器

讲了这么多,是不是觉得少了点什么,拦截器该拦截什么请求,放行什么请求呢?
我们得给这个拦截器加一个配置才行。

1
2
3
4
5
6
7
8
9
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register"); // 放行登录注册接口
}
}

代码示例

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
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.method.HandlerMethod;

public class JwtInterceptor implements HandlerInterceptor {

// 预处理逻辑:在 Controller 方法执行前执行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {

// 只拦截方法(排除静态资源等)
if (!(handler instanceof HandlerMethod)) {
return true;
}

String token = request.getHeader("Authorization");

// 没带 token,拒绝访问
if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401, \"msg\":\"请先登录\"}");
return false;
}

// 校验 token 合法性
try {
String userId = JwtUtil.getUserId(token); // 自己实现的工具类
// 可选:将 userId 存到 request 供后续 Controller 使用
request.setAttribute("userId", userId);
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401, \"msg\":\"token 无效或已过期\"}");
return false;
}
}
}

拿到密钥了,密钥正确,要让后端知道用户是谁,要把密钥解开
String userId = JwtUtil.getUserId(token);
上文已经解开了
用户ID什么时候才有用呢?
当然是和数据库做交互的时候,如果你的发表评论的逻辑是:
后端接收到令牌和评论信息,解读令牌,通过后在评论数据表中插入 用户ID + 评论数据
那么,Controller层需要拿到你的用户ID吗?
我们可以直接在Service层把用户ID+评论数据放到一个comment对象中,而不是通过request从Controller一层层往下传。
这时候,我们就需要用到

线程本地变量(ThreadLocal

Spring MVC 每个请求都会用一个线程处理,你可以用 ThreadLocal整个请求生命周期中共享变量:

  • 登录拦截器里解析完 token,把用户 ID 存进去
  • 后续任何地方(Service、Controller)都可以直接拿到当前用户 ID
    你可以理解为:每一个请求都相当于一个平行宇宙,每一个平行宇宙中都有单独的ThreadLocal

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserContext {
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
//设置
public static void setUserId(String userId) {
USER_ID_HOLDER.set(userId);
}
//获取
public static String getUserId() {
return USER_ID_HOLDER.get();
}
//删除
public static void clear() {
USER_ID_HOLDER.remove();
}
}

用法

存储用户ID

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
public class JwtInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");

if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401, \"msg\":\"请先登录\"}");
return false;
}

try {
String userId = JwtUtil.getUserId(token);
UserContext.setUserId(userId); // 存入当前线程
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401, \"msg\":\"token 无效或已过期\"}");
return false;
}
}

// 在请求结束后自动清理 ThreadLocal,防止内存泄露
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
UserContext.clear();
}
}

拿取用户ID

1
2
3
4
5
6
7
8
@Service
public class UserService {

public void getProfile() {
String userId = UserContext.getUserId(); // 拿到当前登录用户 ID
// 继续查询用户数据
}
}

像不像Terraria里的虚空袋,Minecraft里的末影箱

以上,你完成了登录模块的一系列功能,你能够进行登录拦截,能够持久化登录,能够让当前请求的用户ID像放在末影箱里一样随取随用。


Spring:登录后逻辑:JWT、拦截器与ThreadLocal
https://www.zheep.top/posts/1587478215/
作者
西行寺岩羊
发布于
2025年5月9日
许可协议