惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
A
About on SuperTechFans
博客园 - Franky
Engineering at Meta
Engineering at Meta
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Martin Fowler
Martin Fowler
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
博客园 - 叶小钗
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
V
V2EX
G
Google Developers Blog

云端的解构者 | 西行寺岩羊的博客

苏州 记录:傻逼win11美化插件导致资源管理器崩溃 炸鸡 武汉 最近在干嘛 当善良成为廉价品:为什么你的帮助换不来感激? Minecraft跨平台服务器搭建记录 防弹笔记法(草稿 CSS纸张阴影二三事 Docker:网络 Docker:DockerFile Docker:容器数据卷 Docker:镜像讲解 Docker:基本命令 Spring:Redis
Spring:登录后逻辑:JWT、拦截器与ThreadLocal
西行寺岩羊 · 2025-05-09 · via 云端的解构者 | 西行寺岩羊的博客

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像放在末影箱里一样随取随用。