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)不要“什么都塞”,遵循最小化原则:

  • 必须userIduserTypeexp(过期时间)
  • 可选:角色列表 roles(不频繁变更才适合放 Token)
  • 不要放:手机号、邮箱、身份证等敏感信息

原因很简单:JWT 默认只是签名,并不是加密;即使加密也不建议把敏感信息塞进去。

5. 过期策略:体验与安全的平衡

你通常会配置两种 Token:

  • Access Token(短期):例如 30 分钟~2 小时
  • Refresh Token(长期):例如 7 天~30 天(建议存服务端,便于撤销)

本项目里提到了把 refresh token 存 Redis,这是一个更“可控”的做法:

  • 用户主动退出:可以删除 refresh token
  • 风险账号:可以撤销 refresh token,让它重新登录

6. 请求鉴权:拦截器里要做什么(别堆代码,抓住要点)

一个合格的 JWT 拦截器,核心就是这几步:

  1. 跳过白名单:登录、注册、静态资源、Swagger 等
  2. 读取 Token:通常从 Authorization: Bearer 中取
  3. 校验 Token
    • 是否能解析
    • 是否过期
    • 签名是否正确
  4. 构建上下文:把 userId/userType/roles 放入 UserContext
  5. 放行:进入 Controller
  6. 请求结束清理上下文:防止线程复用造成串号

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 记得清理
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐