Spring Boot 3 实战:基于 JWT 与 Spring Security 6 构建可扩展的 Java Web 登录鉴权体系
在 Java Web 项目里,“登录鉴权”几乎是绕不过去的一步。很多团队刚开始做的时候,会先写一个能跑的版本:用户名密码登录成功后,把用户信息塞进 Session,后面请求靠 Cookie 续命。这个方式并不是错,但一旦项目开始走向前后端分离、微服务化,或者准备做多端接入,传统 Session 方案很快就会遇到扩展性和部署复杂度的问题。
这篇文章我换一个更偏“落地”的角度来讲:不是只讲 JWT 是什么,而是带你在 Spring Boot 3 + Spring Security 6 里,真正搭一个能跑、能扩展、便于后续演进的登录鉴权体系。我们会从问题出发,讲清楚核心链路,再给出一套完整代码骨架,并穿插我自己常见的踩坑点。
背景与问题
先看几个典型场景:
- 前后端分离,前端通过
Authorization: Bearer <token>调接口 - 后端服务需要无状态部署,避免 Session 粘连
- 需要支持角色权限,比如普通用户、管理员
- 后续可能接入 Redis 黑名单、Refresh Token、多端登录控制
这时,很多人会自然想到 JWT。它的核心优势是:
- 无状态:服务端不强依赖 Session 存储
- 易扩展:适合 API 网关、微服务、移动端
- 跨服务传播方便:标准化的 token 格式
但 JWT 也不是“上了就安全”,常见问题包括:
- token 过期策略混乱
- 过滤器顺序不对,导致明明带了 token 仍然 403
- claim 设计随意,后续兼容性差
- 把敏感信息直接塞进 token
- 刷新机制和登出机制没规划
所以这篇文章的目标不是“演示登录成功”,而是构建一套结构清晰、方便维护、适合继续演进的方案。
前置知识与环境准备
建议你至少具备以下基础:
- Java 17+
- Maven 基本使用
- Spring Boot 基本开发经验
- 了解 HTTP 请求头、状态码、JSON
本文环境:
- JDK 17
- Spring Boot 3.x
- Spring Security 6.x
- jjwt 0.12.x
- Maven
整体设计思路
我们先定一个简单、实用的架构:
- 用户通过用户名密码调用
/api/auth/login - 后端校验账号密码
- 校验成功后签发 JWT
- 前端后续请求在
Authorization头里带上Bearer token - JWT 过滤器解析 token,并将用户身份放入 Spring Security 上下文
- 控制器或方法通过权限规则进行访问控制
为什么用这种结构?
因为它有几个好处:
- 登录逻辑和鉴权逻辑分离
- token 解析集中在过滤器中
- 业务代码只关心“当前用户是谁”
- 后续接入数据库、Redis、网关都比较自然
核心原理
1. Spring Security 6 的基本思路
Spring Security 6 不再推荐继承 WebSecurityConfigurerAdapter,而是通过声明式的 SecurityFilterChain 来配置。
你可以把它理解成:
- 请求先经过一串过滤器
- 其中某个过滤器负责从 JWT 中识别用户
- 如果识别成功,就把认证信息放进
SecurityContext - 后面的授权规则根据这个上下文决定是否放行
2. JWT 的组成
JWT 由三部分组成:
- Header
- Payload
- Signature
其中 Payload 常见字段:
sub:用户标识iat:签发时间exp:过期时间- 自定义字段,如
roles
要注意:JWT 是可解码的,不是加密的。所以不要把密码、身份证号这类敏感信息放进去。
3. 认证与授权分开理解
很多初学者容易把这两个混在一起:
- 认证 Authentication:你是谁
- 授权 Authorization:你能访问什么
在本文方案里:
- 登录接口负责认证
- 请求携带 token 后,过滤器恢复用户身份
- Spring Security 规则负责授权
请求链路图
flowchart TD
A[客户端发起登录请求] --> B[AuthController /login]
B --> C[AuthenticationManager 校验用户名密码]
C -->|成功| D[JwtService 签发 JWT]
D --> E[返回 access token]
E --> F[客户端后续请求携带 Bearer Token]
F --> G[JwtAuthenticationFilter]
G --> H[解析 JWT 并构建 Authentication]
H --> I[放入 SecurityContext]
I --> J[控制器/方法鉴权]
认证时序图
sequenceDiagram
participant Client as 客户端
participant Controller as AuthController
participant Manager as AuthenticationManager
participant UDS as UserDetailsService
participant JWT as JwtService
Client->>Controller: POST /api/auth/login
Controller->>Manager: authenticate(username, password)
Manager->>UDS: loadUserByUsername()
UDS-->>Manager: UserDetails
Manager-->>Controller: 认证成功
Controller->>JWT: generateToken(user)
JWT-->>Controller: accessToken
Controller-->>Client: 200 + token
项目结构建议
我个人更推荐下面这种结构,后面扩展 Refresh Token、黑名单、租户等能力时不会太乱:
src/main/java/com/example/demo
├── config
│ └── SecurityConfig.java
├── controller
│ ├── AuthController.java
│ └── UserController.java
├── dto
│ ├── LoginRequest.java
│ └── LoginResponse.java
├── security
│ ├── JwtAuthenticationFilter.java
│ ├── JwtService.java
│ └── CustomUserDetailsService.java
├── service
│ └── InMemoryUserService.java
└── DemoApplication.java
实战代码(可运行)
下面给出一个最小可运行版本。为了聚焦 JWT 与 Security 主线,我先用内存用户,避免文章被数据库细节冲散。你后续接数据库时,只需要替换 UserDetailsService 即可。
1. Maven 依赖
<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>springboot3-jwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<jjwt.version>0.12.5</jjwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2. 配置文件
# src/main/resources/application.yml
server:
port: 8080
jwt:
secret: 12345678901234567890123456789012
expiration: 3600000
这里的
secret为了演示直接明文写在配置里,生产环境不要这么干,后面会讲。
3. 启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4. DTO
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.example.demo.dto;
public class LoginResponse {
private String token;
private String tokenType = "Bearer";
public LoginResponse(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public String getTokenType() {
return tokenType;
}
}
5. JWT 服务
package com.example.demo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String generateToken(UserDetails userDetails) {
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
Date now = new Date();
Date expireAt = new Date(now.getTime() + expiration);
return Jwts.builder()
.subject(userDetails.getUsername())
.claim("roles", roles)
.issuedAt(now)
.expiration(expireAt)
.signWith(getSignKey())
.compact();
}
public String extractUsername(String token) {
return parseClaims(token).getSubject();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
public boolean isTokenExpired(String token) {
return parseClaims(token).getExpiration().before(new Date());
}
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(
java.util.Base64.getEncoder().encodeToString(secret.getBytes())
);
return Keys.hmacShaKeyFor(keyBytes);
}
}
6. 自定义 UserDetailsService
package com.example.demo.security;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return User.withUsername("admin")
.password("$2a$10$N9qo8uLOickgx2ZMRZo5i.ej5j8xQ1OtSWT8gJ5istyhXCTITVE4G")
.roles("ADMIN")
.build();
}
if ("user".equals(username)) {
return User.withUsername("user")
.password("$2a$10$N9qo8uLOickgx2ZMRZo5i.ej5j8xQ1OtSWT8gJ5istyhXCTITVE4G")
.roles("USER")
.build();
}
throw new UsernameNotFoundException("用户不存在");
}
}
上面的 BCrypt 密文对应明文密码:
123456
7. JWT 认证过滤器
这个类是整套方案最关键的部分之一。它做的事情很纯粹:
- 从请求头取 token
- 解析用户名
- 加载用户
- 验证 token
- 构建认证对象并放入上下文
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception ex) {
// 这里先吞掉异常,让后续统一走未认证处理
}
filterChain.doFilter(request, response);
}
}
8. Security 配置
package com.example.demo.config;
import com.example.demo.security.CustomUserDetailsService;
import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
CustomUserDetailsService userDetailsService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
9. 登录接口
package com.example.demo.controller;
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.LoginResponse;
import com.example.demo.security.JwtService;
import jakarta.validation.Valid;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
public AuthController(AuthenticationManager authenticationManager, JwtService jwtService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@PostMapping("/login")
public LoginResponse login(@RequestBody @Valid LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
String token = jwtService.generateToken(
(org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal()
);
return new LoginResponse(token);
}
}
10. 受保护接口
package com.example.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/me")
public Map<String, Object> me(Authentication authentication) {
return Map.of(
"username", authentication.getName(),
"authorities", authentication.getAuthorities()
);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/dashboard")
public Map<String, Object> adminDashboard() {
return Map.of("message", "欢迎访问管理员面板");
}
}
逐步验证清单
建议你按这个顺序测试,排查效率会高很多。
1. 获取 token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"123456"}'
预期返回:
{
"token": "xxxxx",
"tokenType": "Bearer"
}
2. 访问当前用户接口
curl http://localhost:8080/api/me \
-H "Authorization: Bearer 你的token"
3. 普通用户访问管理员接口
curl http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer 普通用户token"
预期是 403 Forbidden。
4. 管理员登录并访问管理员接口
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
然后用返回 token 访问:
curl http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer 管理员token"
权限模型图
classDiagram
class UserDetails {
+String username
+String password
+Collection authorities
}
class JwtService {
+generateToken(userDetails)
+extractUsername(token)
+isTokenValid(token, userDetails)
}
class JwtAuthenticationFilter {
+doFilterInternal(request, response, chain)
}
class SecurityContextHolder {
<<static>>
}
JwtAuthenticationFilter --> JwtService
JwtAuthenticationFilter --> UserDetails
JwtAuthenticationFilter --> SecurityContextHolder
常见坑与排查
这部分非常重要。很多人代码“看起来都对”,但就是登录后访问不了接口,基本都栽在这些地方。
坑 1:过滤器加错位置
如果你没有把 JWT 过滤器放在 UsernamePasswordAuthenticationFilter 之前,Spring Security 的认证流程可能不会按预期工作。
正确写法:
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
坑 2:权限前缀混乱
Spring Security 的 roles("ADMIN") 实际会自动加上前缀 ROLE_。所以:
- 写
roles("ADMIN") - 判断时用
hasRole("ADMIN")
不要混着写成:
- authority 是
ADMIN - 判断却是
hasRole("ADMIN")
否则很容易 403。我当时第一次接权限判断时,就在这个点上绕了半小时。
坑 3:密码编码器不一致
如果你数据库里存的是 BCrypt 密文,那认证时也必须用 BCrypt。
常见现象是:
- 用户明明存在
- 密码也确认没错
- 但一直报
Bad credentials
检查点:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
坑 4:JWT 密钥长度不合适
如果你用 HMAC 签名算法,密钥长度要满足要求。太短会报错,太随意也不安全。
实际项目建议:
- 使用足够长度的随机密钥
- 放在环境变量、配置中心或密钥管理系统中
坑 5:请求头没带 Bearer 前缀
后端通常约定:
Authorization: Bearer eyJhbGci...
如果你只传 token 本体,不带 Bearer ,过滤器大概率直接跳过。
坑 6:登录接口被拦截
如果忘了放行登录接口:
.requestMatchers("/api/auth/login").permitAll()
那么你会发现一个很尴尬的现象:连登录接口都要求先登录。
坑 7:异常被吞掉后难排查
上面的示例里,为了保证最小实现,我们在过滤器中简单吞掉了异常。但真实项目里更推荐记录日志,例如:
catch (Exception ex) {
logger.warn("JWT 解析失败: {}", ex.getMessage());
}
否则线上 token 失效、签名错误、格式异常时,你只能靠猜。
安全/性能最佳实践
这里给的是更贴近生产环境的建议,不一定每项都要立刻做,但建议你在设计时预留位置。
1. Access Token 短时效,别无限期
推荐思路:
- Access Token:15 分钟 ~ 2 小时
- Refresh Token:7 天 ~ 30 天
原因很简单:token 一旦泄露,生命周期越长,风险越大。
2. 不要把敏感信息放进 JWT
JWT 常适合放:
- 用户 ID
- 用户名
- 角色
- 租户 ID(按需)
不要放:
- 密码
- 手机号明文
- 身份证号
- 银行卡号
- 大块业务数据
3. 登出不是“删前端 token”就完了
很多文章讲到这里就结束了,但实际业务常常要求“立即失效”。
JWT 天生无状态,所以如果你要支持服务端主动失效,可以考虑:
- Redis 黑名单
- token 版本号
- 用户状态校验
- 刷新 token 轮换
尤其是后台管理系统,我更建议加黑名单或版本机制。
4. 用户权限变更后的失效策略要提前想好
比如:
- 某用户刚被取消管理员权限
- 但他手里的旧 token 还没过期
这时系统可能会出现“权限撤销不即时”的问题。常见做法:
- token 过期时间缩短
- token 中带权限版本号
- 服务端按需二次查库校验关键操作
5. 方法级鉴权比 URL 鉴权更稳
URL 规则适合做粗粒度控制,但业务复杂后,更推荐在关键方法上加:
@PreAuthorize("hasRole('ADMIN')")
这样代码可读性更高,也更不容易因为路径调整导致权限漏配。
6. 控制 JWT 体积
token 不是越全越好。体积过大会带来:
- 请求头膨胀
- 网络开销增加
- 网关/代理兼容性问题
所以 claim 要克制,只放必要字段。
7. 为鉴权链路补齐日志与监控
至少要监控这些数据:
- 登录成功/失败次数
- token 解析失败次数
- 403/401 比例
- 高频用户异常访问
- 管理员接口访问日志
生产环境里,没有日志和指标的安全系统,定位问题会非常痛苦。
可扩展演进方向
如果你准备把这个 demo 演进到正式项目,我建议按下面顺序升级:
阶段 1:接入数据库用户体系
替换 CustomUserDetailsService:
- 从数据库查用户
- 查角色、权限
- 判断账号状态:禁用、锁定、过期
阶段 2:加入 Refresh Token
适用场景:
- 用户希望保持较长登录态
- Access Token 需要缩短生命周期
阶段 3:支持登出与强制失效
推荐:
- Redis 存储 token 黑名单
- 或按用户维度维护 token version
阶段 4:支持多终端登录控制
例如:
- 同账号最多同时登录 3 台设备
- 管理员可踢掉历史登录
阶段 5:网关统一鉴权
如果系统走向微服务,可将 JWT 校验前移到网关,但核心服务仍应保留关键权限校验,避免单点失守。
一些实现边界条件
这套方案不是银弹,下面这些情况需要单独评估:
1. 高安全后台
如果是金融、政企、核心管理后台,仅仅 JWT + 密码登录还不够,建议增加:
- MFA 多因子认证
- IP 白名单
- 设备指纹
- 操作二次确认
2. 强一致的会话控制
如果你强依赖:
- 单点登出
- 秒级强制失效
- 精确在线人数
那纯 JWT 无状态方案会变得“理论优雅,工程补丁很多”。这时可以考虑:
- JWT + Redis
- 或基于 Session / OAuth2 授权中心的集中式方案
3. 超复杂权限模型
如果权限细到菜单、按钮、数据行级别,那么角色 claim 往往不够。建议:
- token 放基础身份
- 细粒度权限由服务端动态判定
总结
如果你要在 Spring Boot 3 里做一套现代化、适合前后端分离的登录鉴权体系,Spring Security 6 + JWT 依然是非常主流且实用的选择。
这篇文章我们完成了几件关键事情:
- 搞清了 JWT 与 Spring Security 6 的协作方式
- 用
SecurityFilterChain替代旧式配置 - 实现了登录、token 签发、请求鉴权、角色控制
- 梳理了常见踩坑点和生产实践建议
如果你现在正准备在项目里落地,我给你一个很实用的执行建议:
- 先跑通本文最小版本
- 再替换成数据库用户体系
- 上线前补齐异常处理、日志、过期策略
- 根据业务要求决定是否引入 Refresh Token 和黑名单
- 关键接口一定保留方法级鉴权
最后再强调一句:JWT 解决的是“身份携带”问题,不会自动解决所有安全问题。真正可用的鉴权体系,靠的是合理的 token 生命周期、清晰的权限模型、完备的日志审计,以及对边界场景的预案。
如果你是中级开发者,我建议你别只停留在“会写 demo”,而是从现在开始,把“可扩展、可排查、可演进”当成安全设计的一部分。这样你做出来的登录体系,才能真的扛住后续业务增长。