JWT 认证:让登录、鉴权、权限控制“可控且可维护”
·
1. 为什么要用 JWT
在前后端分离的系统里,最常见的问题是:
- 用户登录后,后端怎么识别“这次请求是谁发的”?
- 用户权限怎么控制?学生能不能调用教师接口?
- 多台服务器部署时,怎么共享登录状态?
传统 Session 模式需要服务端保存会话状态(内存或 Redis),而 JWT 的核心价值是:
- 无状态:服务端不必保存会话;扩容更容易
- 跨域友好:前端可以把 Token 放在 Header
- 携带声明:把必要的用户信息(userId、角色、过期时间)写进 Token
一句话:JWT 让“谁在访问、能访问什么”变得清晰。
2. JWT 的基本结构(知道就行,不要背)
JWT 是三段字符串:
Header:声明算法与类型Payload:存业务声明(claims)Signature:签名(防篡改)
你在项目里要关心的只有两点:
- 签名密钥要保密(不要提交到仓库)
- 过期时间要合理(太长风险高,太短影响体验)
3. 本项目的认证链路(看这张流程图就够)
┌──────────────┐ 登录 ┌──────────────┐
│ 前端 Vue │────────▶│ /user/login │
│ 输入账号密码 │ │ 校验成功返回 │
└──────────────┘ │ accessToken │
└───────┬──────┘
│
│ 后续请求都带 Authorization: Bearer <token>
▼
┌────────────────┐
│ JwtInterceptor │
│ 解析/校验Token │
└───────┬────────┘
│
▼
┌────────────────┐
│ Controller/Service
│ 根据角色/权限执行业务 │
└────────────────┘
这里的核心组件就是:
- Token 生成/解析工具(JwtUtil)
- 请求拦截器(JwtInterceptor)
- 用户上下文(UserContext:把 userId/userType 放进 ThreadLocal)
4. Token 里到底应该放什么
Token 的 Payload(claims)不要“什么都塞”,遵循最小化原则:
- 必须:
userId、userType、exp(过期时间) - 可选:角色列表
roles(不频繁变更才适合放 Token) - 不要放:手机号、邮箱、身份证等敏感信息
原因很简单:JWT 默认只是签名,并不是加密;即使加密也不建议把敏感信息塞进去。
5. 过期策略:体验与安全的平衡
你通常会配置两种 Token:
- Access Token(短期):例如 30 分钟~2 小时
- Refresh Token(长期):例如 7 天~30 天(建议存服务端,便于撤销)
本项目里提到了把 refresh token 存 Redis,这是一个更“可控”的做法:
- 用户主动退出:可以删除 refresh token
- 风险账号:可以撤销 refresh token,让它重新登录
6. 请求鉴权:拦截器里要做什么(别堆代码,抓住要点)
一个合格的 JWT 拦截器,核心就是这几步:
- 跳过白名单:登录、注册、静态资源、Swagger 等
- 读取 Token:通常从
Authorization: Bearer中取 - 校验 Token:
- 是否能解析
- 是否过期
- 签名是否正确
- 构建上下文:把
userId/userType/roles放入 UserContext - 放行:进入 Controller
- 请求结束清理上下文:防止线程复用造成串号
7. 权限控制:别只靠“前端隐藏按钮”
权限控制必须在后端。
常见的权限粒度:
- 角色级:教师、学生、管理员
- 功能级:创建课程、发布考试、查看统计
- 资源级:只能操作自己创建的课程/考试
在文章里可以这样描述权限策略(更像博客):
- 角色级:通过
userType决定基础访问范围 - 功能级:通过
roles/authorities决定是否能调用某些接口 - 资源级:业务层再做二次校验(是否是课程创建者/是否属于该班级)
8. 最少代码展示(3 段以内)
8.1 前端请求携带 Token(关键点)
Authorization: Bearer <accessToken>
8.2 拦截器伪代码(核心逻辑)
if path in whitelist: return true
token = read Authorization header
if token missing: return 401
claims = parse token
if expired/invalid: return 401
UserContext.set(userId, userType, roles)
return true
8.3 资源级权限的例子(思想)
删除课程:
1) 必须是 teacher
2) 必须是该课程创建者
3) 满足后才允许删除
8.4 关键代码片段(可直接复制粘贴)
1) JwtUtil 最小可用版本(生成 + 解析)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
// 建议放配置文件,长度至少 32 字符(HS256)
private final String secret;
private final long expireMs;
public JwtUtil(String secret, long expireMs) {
this.secret = secret;
this.expireMs = expireMs;
}
public String generateToken(Integer userId, String userType, String username) {
Date now = new Date();
Date exp = new Date(now.getTime() + expireMs);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(exp)
.addClaims(Map.of("userId", userId, "userType", userType))
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.compact();
}
public Claims parse(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token)
.getBody();
}
}
2) 拦截器里“取 Token + 写入上下文 + 清理”(示意)
String auth = request.getHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
response.setStatus(401);
return false;
}
String token = auth.substring(7);
Claims claims = jwtUtil.parse(token);
Integer userId = claims.get("userId", Integer.class);
String userType = claims.get("userType", String.class);
UserContext.setUserId(userId);
UserContext.setUserType(userType);
// afterCompletion 必须清理
UserContext.clear();
3) 前端 Axios 统一注入 Token(直接可用)
import axios from 'axios'
const http = axios.create({
baseURL: '/api',
timeout: 15000
})
http.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
http.interceptors.response.use(res => res, err => {
if (err.response && err.response.status === 401) {
// 统一处理:跳转登录/刷新token
}
return Promise.reject(err)
})
export default http
9. 常见坑与修复建议
-
Token 过期后前端一直 401
- 建议配 refresh token 或统一跳转登录
-
ThreadLocal 串号
- 一定要在
afterCompletion清理 UserContext
- 一定要在
-
角色变更不生效
- 如果 roles 写进 JWT,角色变更后旧 token 仍有效
- 解决:缩短 token 过期时间,或把权限查询放到服务端(带来查询成本)
-
把敏感信息塞进 JWT
- 不建议;最小化 claims
10. 总结
JWT 在这个项目里的定位是:
- 让请求身份识别清晰(你是谁)
- 让权限控制可维护(你能做什么)
- 让系统易扩容(无状态)
把握住三条原则就不会写崩:
- Token 短期 + refresh 可撤销
- 后端必须鉴权,资源级再校验
- UserContext 记得清理
更多推荐




所有评论(0)