背景与问题
在 Java Web 项目里,认证和授权几乎是绕不过去的基础设施。很多团队一开始会用 Session 做登录态管理,开发快、上手也直接,但只要系统进入下面这些场景,问题就会开始出现:
- 前后端分离,客户端不只浏览器,还有 App、小程序、第三方调用
- 服务开始横向扩容,登录态共享变复杂
- 需要把“谁能访问什么接口”讲清楚,并且可审计
- 需要支持无状态、便于网关转发和微服务拆分
这时候,Spring Boot + JWT 往往会成为一个很自然的选择。
不过我见过不少项目,JWT 虽然用了,但其实只是“把用户 ID 塞进 token”。结果上线后会遇到一串问题:
- token 能验证,但权限判断很粗糙
- 登出后 token 仍然有效
- 接口明明加了权限注解,却被绕过
- 刷新 token 设计混乱,前端不断被踢下线
- 安全过滤器顺序不对,导致匿名访问失效或者 401/403 混乱
所以这篇文章不只讲“JWT 怎么生成”,而是站在架构设计 + 工程落地的角度,把一套可运行、可扩展、能排查问题的方案串起来。
先讲清楚:我们到底要解决什么
在权限认证设计里,通常有三层目标:
-
认证 Authentication
证明“你是谁”,比如用户名密码登录成功后签发 token。 -
授权 Authorization
判断“你能做什么”,比如管理员才能删除用户,普通用户只能查看自己的资料。 -
接口安全 API Security
防止接口被伪造调用、暴力破解、越权访问、token 泄漏后长期滥用等。
如果把 Spring Boot + JWT 用好,它适合解决的是:
- 无状态登录态校验
- 跨服务传递身份信息
- 基于角色/权限的接口访问控制
- 与 Spring Security 平滑整合
但它不天然解决这些问题:
- token 主动失效
- 细粒度动态权限实时变更
- 防重放、防盗用、防撞库
- 复杂会话控制(如同账号多端互斥)
也就是说,JWT 是“基础积木”,不是“安全万能药”。
方案全景与取舍分析
1. Session 与 JWT 的对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Session + Cookie | 开发简单、服务端可控、天然可失效 | 集群要共享 Session,跨端支持一般 | 传统后台、单体系统 |
| JWT | 无状态、易扩展、适合前后端分离和网关 | 主动失效复杂、token 泄漏风险更高 | API 化系统、微服务、移动端 |
| JWT + Redis | 保留 JWT 无状态优势,同时可做黑名单/刷新控制 | 架构复杂度略高,需要额外存储 | 中大型业务系统 |
在真实项目里,我更推荐的不是“纯 JWT”,而是:
Access Token 用 JWT,Refresh Token/黑名单/会话控制放 Redis。
这样可以在可扩展和可控之间取得一个比较稳的平衡。
2. 推荐的权限模型
中级项目里,建议至少采用下面这套模型:
- 用户
User - 角色
Role - 权限
Permission - 关系:
- 用户-角色:多对多
- 角色-权限:多对多
接口控制层面:
- 粗粒度:角色控制,如
ROLE_ADMIN - 细粒度:权限控制,如
user:delete、order:read
这样后期不会因为“一个管理员角色太大”而改得很痛苦。
核心原理
1. JWT 的结构
JWT 由三部分组成:
- Header:声明算法、类型
- Payload:载荷,保存用户标识、角色、过期时间等
- Signature:签名,防篡改
结构如下:
xxxxx.yyyyy.zzzzz
其中 Payload 里的数据只是 Base64Url 编码,不是加密。
这点特别重要:不要把密码、手机号明文、银行卡号等敏感信息放进 JWT。
2. 认证流程
下面先看一个典型流程图。
flowchart LR
A[用户登录<br/>用户名/密码] --> B[Spring Boot 认证]
B --> C{认证成功?}
C -- 否 --> D[返回 401]
C -- 是 --> E[生成 JWT Access Token]
E --> F[客户端保存 Token]
F --> G[请求接口时携带 Authorization: Bearer Token]
G --> H[JWT 过滤器校验签名/过期时间]
H --> I{合法?}
I -- 否 --> J[返回 401]
I -- 是 --> K[构造 Authentication 放入 SecurityContext]
K --> L[进入权限判断]
L --> M{拥有权限?}
M -- 否 --> N[返回 403]
M -- 是 --> O[返回业务数据]
3. Spring Security 在这里扮演什么角色
很多人会把 JWT 和权限框架混在一起理解。其实:
- JWT 负责“携带身份声明”
- Spring Security 负责“认证上下文 + 访问控制”
落地时通常是这样:
- 用户登录,校验账号密码
- 服务端生成 JWT
- 请求进来后,JWT 过滤器解析 token
- 如果合法,就创建
Authentication - 放入
SecurityContextHolder - 后续交给 Spring Security 的授权机制处理,如:
requestMatchers().hasRole(...)@PreAuthorize("hasAuthority('user:read')")
这一步是整个架构的关键:
JWT 只是把“用户是谁、有什么权限”带进来,真正控制访问的是 Spring Security。
4. 认证与授权时序图
sequenceDiagram
participant Client as 客户端
participant Auth as 认证接口
participant JWT as JWT工具
participant Filter as JWT过滤器
participant Security as Spring Security
participant API as 业务接口
Client->>Auth: POST /login 用户名密码
Auth->>Auth: 校验账号密码
Auth->>JWT: 生成Access Token
JWT-->>Auth: token
Auth-->>Client: 返回token
Client->>Filter: GET /api/users\nAuthorization: Bearer xxx
Filter->>JWT: 解析并校验token
JWT-->>Filter: 用户信息/权限
Filter->>Security: 写入Authentication
Security->>API: 执行权限判断
API-->>Client: 返回结果或403
设计一套可落地的权限认证架构
为了让后面的代码更完整,我们约定一个简单架构:
/auth/login:登录接口,返回 JWT/api/admin/**:管理员角色访问/api/user/**:登录用户访问- 使用
Spring Security 6 - 使用
jjwt - 权限从“内存假数据”加载,方便示例可运行
权限模型简图
classDiagram
class User {
+Long id
+String username
+String password
+List~String~ roles
+List~String~ authorities
}
class JwtAuthenticationFilter
class JwtTokenProvider
class CustomUserDetailsService
class SecurityConfig
CustomUserDetailsService --> User
JwtAuthenticationFilter --> JwtTokenProvider
SecurityConfig --> JwtAuthenticationFilter
实战代码(可运行)
下面给一套精简但能跑通核心流程的代码。你可以直接放进 Spring Boot 项目里验证。
1. Maven 依赖
<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>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>
</dependencies>
2. 启动类
package com.example.jwtsecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JwtSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(JwtSecurityApplication.class, args);
}
}
3. 用户对象与登录请求
package com.example.jwtsecurity.model;
import java.util.List;
public class LoginUser {
private Long id;
private String username;
private String password;
private List<String> roles;
private List<String> authorities;
public LoginUser() {
}
public LoginUser(Long id, String username, String password, List<String> roles, List<String> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
this.authorities = authorities;
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public List<String> getRoles() {
return roles;
}
public List<String> getAuthorities() {
return authorities;
}
}
package com.example.jwtsecurity.dto;
public class LoginRequest {
private String username;
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;
}
}
4. 一个简单的用户服务
为了让示例最小化,这里先用内存数据模拟数据库。
package com.example.jwtsecurity.service;
import com.example.jwtsecurity.model.LoginUser;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@Service
public class CustomUserService {
private static final Map<String, LoginUser> USER_STORE = new HashMap<>();
static {
USER_STORE.put("admin", new LoginUser(
1L,
"admin",
"{noop}123456",
Arrays.asList("ROLE_ADMIN"),
Arrays.asList("user:read", "user:delete")
));
USER_STORE.put("tom", new LoginUser(
2L,
"tom",
"{noop}123456",
Arrays.asList("ROLE_USER"),
Arrays.asList("user:read")
));
}
public LoginUser findByUsername(String username) {
return USER_STORE.get(username);
}
}
5. JWT 工具类
package com.example.jwtsecurity.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class JwtTokenProvider {
private static final String SECRET = "my-super-secret-key-my-super-secret-key-123456";
private static final long EXPIRE_TIME = 1000 * 60 * 30;
private final SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
public String generateToken(UserDetails userDetails) {
List<String> authorities = userDetails.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("authorities", authorities)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public String getUsernameFromToken(String token) {
return parseClaims(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (ExpiredJwtException e) {
System.out.println("Token 已过期");
} catch (UnsupportedJwtException e) {
System.out.println("Token 格式不支持");
} catch (MalformedJwtException e) {
System.out.println("Token 格式错误");
} catch (SecurityException | IllegalArgumentException e) {
System.out.println("Token 签名无效");
}
return false;
}
private Jws<Claims> parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
}
}
6. UserDetailsService
这里把业务用户映射为 Spring Security 可识别的 UserDetails。
package com.example.jwtsecurity.service;
import com.example.jwtsecurity.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private CustomUserService customUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LoginUser loginUser = customUserService.findByUsername(username);
if (loginUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
loginUser.getRoles().forEach(role -> authorityList.add(new SimpleGrantedAuthority(role)));
loginUser.getAuthorities().forEach(auth -> authorityList.add(new SimpleGrantedAuthority(auth)));
return User.builder()
.username(loginUser.getUsername())
.password(loginUser.getPassword())
.authorities(authorityList)
.build();
}
}
7. JWT 过滤器
package com.example.jwtsecurity.security;
import com.example.jwtsecurity.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
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 {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
8. Security 配置
这是整套方案的核心配置。
package com.example.jwtsecurity.config;
import com.example.jwtsecurity.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.security.authentication.AuthenticationManager;
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.password.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").authenticated()
.anyRequest().denyAll()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
示例里用了
NoOpPasswordEncoder,只是为了演示方便。
生产环境必须换成BCryptPasswordEncoder。
9. 登录接口
package com.example.jwtsecurity.controller;
import com.example.jwtsecurity.dto.LoginRequest;
import com.example.jwtsecurity.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@PostMapping("/login")
public Map<String, Object> login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenProvider.generateToken(userDetails);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("tokenType", "Bearer");
return result;
}
}
10. 业务接口
package com.example.jwtsecurity.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/user/profile")
public String profile() {
return "当前用户资料";
}
@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "管理员面板";
}
@PreAuthorize("hasAuthority('user:read')")
@GetMapping("/user/query")
public String queryUser() {
return "查询用户成功";
}
@PreAuthorize("hasAuthority('user:delete')")
@DeleteMapping("/admin/delete")
public String deleteUser() {
return "删除用户成功";
}
}
如何验证这套代码
1. 登录获取 token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
返回结果类似:
{
"tokenType": "Bearer",
"token": "eyJhbGciOiJIUzI1NiJ9..."
}
2. 访问普通登录接口
curl http://localhost:8080/api/user/profile \
-H "Authorization: Bearer 你的token"
3. 访问管理员接口
curl http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer 你的token"
4. 验证权限差异
用 tom / 123456 登录后:
- 可以访问
/api/user/profile - 可以访问
/api/user/query - 不可以访问
/api/admin/dashboard - 不可以访问
/api/admin/delete
这时候你会看到两种常见返回:
401 Unauthorized:通常是没登录、token 无效、token 过期403 Forbidden:已经登录,但权限不够
这两个状态码别混了,很多前后端对接问题就出在这里。
常见坑与排查
这一节我尽量讲“项目里真会踩到的坑”。
1. hasRole("ADMIN") 与 ROLE_ADMIN 对不上
在 Spring Security 里:
.hasRole("ADMIN")
本质上会去匹配权限:
ROLE_ADMIN
所以如果你数据库里只存了 ADMIN,那就会匹配失败。
建议
- 角色统一存成
ROLE_XXX - 细粒度权限统一存成
module:action
例如:
ROLE_ADMINROLE_USERuser:readuser:delete
2. 过滤器明明执行了,但接口还是匿名
常见原因:
- 没有把
Authentication放进SecurityContextHolder - 放入前后线程切换导致上下文丢失
- 过滤器顺序不对
正确姿势通常是:
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
如果顺序错了,Spring Security 后面的授权判断拿不到认证信息。
3. token 合法,但权限没有更新
这是 JWT 的天然特点之一。
例如:
- 用户登录,token 里带着
user:delete - 管理员后来把这个权限收回
- 用户手里的旧 token 在过期前仍可能可用
解决思路
- 缩短 Access Token 生命周期,比如 15~30 分钟
- 使用 Refresh Token 刷新
- 对高风险接口额外查数据库/缓存实时权限
- 用户权限变更后,将旧 token 加入黑名单
如果你的系统是后台管理系统,权限变更要尽快生效,我会更倾向于:
Access Token 短时有效 + Redis 黑名单 + 关键接口二次校验
4. 登出后 token 还可用
很多人第一次接触 JWT 都会问:“我都点退出了,怎么 token 还能请求接口?”
因为 JWT 是无状态的。只要签名合法、没过期,服务端并不知道“你刚刚已经退出”。
处理方式
- 前端删除本地 token:这是最基础的
- 服务端维护黑名单:更稳妥
- 使用 token version:用户登出或修改密码时版本号递增
如果你的系统对安全要求高,不能只依赖前端删 token。
5. token 放在哪里最合适
这个问题没有绝对标准,但有边界条件。
常见方案
放 LocalStorage
- 优点:简单
- 缺点:容易受 XSS 影响
放 HttpOnly Cookie
- 优点:JS 不能直接读取,能降低 token 被脚本窃取的风险
- 缺点:需要处理 CSRF,跨域配置更复杂
我的建议
- 前后端分离后台系统:可考虑
HttpOnly Cookie + CSRF 防护 - 移动端/App:通常走 Authorization Header 更自然
- 如果用 LocalStorage,一定加强 XSS 防护
安全/性能最佳实践
这一节更偏“架构落地的取舍”。
1. 不要在 JWT 里放敏感信息
JWT Payload 不是加密存储。
所以不要放:
- 用户密码
- 手机号明文
- 身份证号
- 银行卡信息
- 过多业务上下文
推荐只放最小必要信息:
sub:用户名或用户 IDiat:签发时间exp:过期时间authorities:权限列表(按需)jti:token 唯一编号(便于黑名单)
2. Access Token 短效,Refresh Token 长效
推荐思路:
Access Token:15~30 分钟Refresh Token:7~30 天- Refresh Token 存 Redis 或数据库,支持吊销
状态转换可以抽象成这样:
stateDiagram-v2
[*] --> 未登录
未登录 --> 已登录: 用户名密码认证成功
已登录 --> AccessToken有效: 签发Access Token
AccessToken有效 --> AccessToken过期: 超过exp
AccessToken过期 --> 刷新成功: Refresh Token有效
刷新成功 --> AccessToken有效: 重新签发
AccessToken过期 --> 重新登录: Refresh Token失效/被吊销
已登录 --> 已登出: 主动退出/加入黑名单
已登出 --> 未登录
这样做的好处是:
- 接口请求轻量
- 权限/会话失控时间可接受
- 支持用户无感刷新登录态
3. 对高风险接口做额外保护
JWT 校验通过,不等于这个请求就绝对安全。
对于这些接口,建议增加二次防护:
- 删除、导出、转账、重置密码
- 管理员批量操作
- 涉及金额、隐私、主数据修改
可选措施:
- 二次密码确认
- 图形验证码/短信验证码
- 操作审计日志
- 幂等 token
- IP/设备指纹校验
- 限流和风控规则
4. 密钥管理要正规
JWT 的签名密钥别写死在代码里,这是很常见但也很危险的做法。示例为了可运行这么写,生产环境不要照搬。
正确建议
- 放在环境变量或配置中心
- 定期轮换密钥
- 不同环境使用不同密钥
- 高等级场景可使用非对称加密(RSA/ECDSA)
如果有多服务校验 token:
- 对称密钥:简单,但密钥分发要谨慎
- 非对称密钥:签发方持私钥,校验方持公钥,更适合分布式
5. 接口限流与登录防爆破
认证系统最容易被打的地方之一就是登录接口。
建议至少做:
- 按 IP 限流
- 按用户名限流
- 连续失败锁定一段时间
- 验证码触发机制
- 登录日志与告警
否则 JWT 做得再漂亮,登录入口被撞库,系统一样危险。
6. 容量与性能上的简单估算
JWT 方案常被说“性能好”,这句话一半对,一半不完整。
优势
- 请求校验不必查 Session
- 横向扩容更简单
- 网关和服务间传递身份轻量
成本
- token 体积比 session id 大
- 每次请求都要做签名校验
- 如果 authority 很多,HTTP Header 会膨胀
举个实际点的建议:
- token 尽量控制在 1~2KB 内
- 权限非常多时,不要把全部细粒度权限都塞进 token
- 可以只放用户标识与版本号,再配合缓存查权限
否则你会发现: “为了少查一次 Redis,结果每个请求的 Header 都变大了,网关日志也更重了。”
这就是典型的架构取舍问题。
进一步演进:从示例到生产
上面的代码已经能跑,但离生产还有几步要补齐。
建议补强点
1. 密码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2. 自定义 401/403 返回体
这样前端更容易统一处理。
3. 引入 Refresh Token
不要让用户频繁重新登录。
4. Redis 黑名单
支持登出、踢人、密码修改后失效。
5. 权限数据从数据库/缓存加载
不要一直停留在内存写死阶段。
6. 审计日志
记录:
- 谁
- 什么时间
- 调用了什么接口
- 是否成功
- IP、UA、traceId
常见接口安全清单
如果你打算把这套方案真正用于项目,我建议上线前至少对照下面的清单过一遍:
- 所有受保护接口都经过 Spring Security
- 不存在 controller 忘记加限制的“漏网接口”
- Access Token 生命周期合理
- 支持 Refresh Token
- 支持登出后失效控制
- 使用 BCrypt 加密密码
- 登录接口有限流/防爆破
- 401/403 响应语义清晰
- 敏感操作有审计日志
- 不在 JWT 里放敏感数据
- 密钥不硬编码在仓库里
- 权限模型区分角色和具体权限
总结
Spring Boot + JWT 不是难在“代码怎么写”,而是难在边界怎么定、取舍怎么做。
如果只想快速登录鉴权,最小方案是:
- 登录签发 JWT
- 过滤器校验 token
- Spring Security 做接口授权
如果想真正落地到中型项目,我更建议这样设计:
- Access Token:JWT,短效
- Refresh Token:Redis/数据库,长效
- 角色 + 权限双层模型
- 关键接口额外做风控与审计
- 黑名单或版本号机制解决主动失效
最后给一个比较务实的建议:
如果你的系统只是内部后台、用户量不大、权限变化频率高,不要为了“时髦”而强上纯 JWT。
如果你的系统是前后端分离、多终端、服务扩展明显,那么 JWT 值得用,但一定配套刷新、吊销和审计机制。
真正靠谱的认证系统,从来不是“用了 JWT”就结束了,而是你能不能把认证、授权、会话控制、风险防护这几件事连成一套。这样系统上线后,才不会一到权限问题就只能临时打补丁。