认证授权详解
本文深入讲解基于 JWT (JSON Web Token) 的认证授权机制,让你理解从登录到访问受保护资源的完整流程。
🎯 什么是认证和授权?
认证 (Authentication)
验证你是谁
用户: "我是 admin"
系统: "请证明你是 admin"
用户: 提供密码
系统: "验证通过,这是你的通行证(Token)"
授权 (Authorization)
验证你能做什么
用户: 访问 /api/admin/users
系统: "检查你的通行证... 你有 ADMIN 角色吗?"
用户: Token 里包含 ROLE_ADMIN
系统: "验证通过,允许访问"
🔑 JWT Token 详解
Token 结构
JWT Token 由三部分组成,用 . 分隔:
eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6MX0.signature
│ │ │
└─ Header (头部) └─ Payload (载荷) └─ Signature (签名)
1. Header (头部)
{
"alg": "HS384", // 签名算法:HMAC-SHA384
"typ": "JWT" // Token 类型
}
2. Payload (载荷) - 存储用户信息
{
"sub": "admin", // Subject: 用户名
"userId": 1, // 自定义:用户 ID
"roles": ["ADMIN", "USER"], // 自定义:用户角色
"iat": 1702300000, // Issued At: 签发时间
"exp": 1702307200 // Expiration: 过期时间(2小时后)
}
3. Signature (签名) - 防篡改
HMACSHA384(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret // 密钥(存储在服务器,不能泄露)
)
为什么需要签名?
- ✅ 防止 Token 被篡改(修改 payload 后签名会失效)
- ✅ 验证 Token 确实是服务器签发的
🔐 完整认证流程
登录流程(获取 Token)
sequenceDiagram
participant Browser as 浏览器
participant Frontend as 前端
participant Backend as 后端 API
participant Redis as Redis 缓存
participant DB as MySQL 数据库
Browser->>Frontend: 用户输入用户名密码
Frontend->>Backend: POST /api/v1/auth/login
Note over Frontend,Backend: {username:"admin", password:"123456"}
Backend->>DB: 1. 查询用户
Note over Backend,DB: SELECT * FROM sys_user WHERE username='admin'
DB-->>Backend: 返回用户信息
Note over Backend: {id:1, password:"$2a$10$...", status:1}
Backend->>Backend: 2. 验证密码
Note over Backend: BCrypt.matches(input, dbPassword)
Backend->>Redis: 3. 查询角色(缓存)
Redis-->>Backend: ["ADMIN", "USER"]
Backend->>Backend: 4. 生成 JWT Token
Note over Backend: 包含 userId, username, roles
Backend-->>Frontend: 返回 Token
Note over Backend,Frontend: {token:"eyJhbGci...", user:{...}}
Frontend->>Browser: 5. 保存 Token
Note over Frontend,Browser: localStorage.setItem('token', token)
访问受保护资源(使用 Token)
sequenceDiagram
participant Browser as 浏览器
participant Frontend as 前端
participant Filter as JwtAuthFilter
participant Security as Spring Security
participant Controller as UserController
Browser->>Frontend: 点击"我的资料"
Frontend->>Filter: GET /api/v1/users/me
Note over Frontend,Filter: Header: Authorization: Bearer {token}
Filter->>Filter: 1. 提取 Token
Note over Filter: 从 "Bearer xxx" 中提取
Filter->>Filter: 2. 验证 Token
Note over Filter: 签名是否有效?是否过期?
alt Token 无效或过期
Filter-->>Frontend: 401 Unauthorized
Frontend->>Browser: 跳转到登录页
end
Filter->>Security: 3. 解析用户信息
Note over Filter,Security: 从 payload 提取 userId, roles
Security->>Security: 4. 设置 SecurityContext
Note over Security: 存储当前用户信息
Filter->>Controller: 5. 放行请求
Controller->>Controller: 6. 获取当前用户
Note over Controller: SecurityUtils.getCurrentUserId()
Controller-->>Frontend: 返回用户信息
Frontend->>Browser: 显示用户资料
🛡️ Spring Security 配置
安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 禁用 CSRF(JWT 无状态,不需要)
.csrf(AbstractHttpConfigurer::disable)
// 2. 禁用 Session(无状态认证)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 3. 配置请求授权
.authorizeHttpRequests(auth -> auth
// 公开接口(无需认证)
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
// 其他接口需要认证
.anyRequest().authenticated())
// 4. 添加 JWT 过滤器
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// ===== 1. 提取 Token =====
String token = extractToken(request);
if (token == null) {
filterChain.doFilter(request, response); // 放行,后续会被拦截
return;
}
try {
// ===== 2. 验证 Token =====
if (!jwtTokenProvider.validateToken(token)) {
filterChain.doFilter(request, response);
return;
}
// ===== 3. 提取用户信息 =====
String username = jwtTokenProvider.getUsernameFromToken(token);
Long userId = jwtTokenProvider.getUserIdFromToken(token);
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
// ===== 4. 构建 Authentication =====
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId, // Principal(主体)
null, // Credentials(凭证)
authorities // Authorities(权限)
);
// ===== 5. 设置到 SecurityContext =====
SecurityContextHolder.getContext()
.setAuthentication(authentication);
} catch (Exception e) {
log.error("JWT 认证失败", e);
}
// ===== 6. 继续过滤器链 =====
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7); // 去掉 "Bearer " 前缀
}
return null;
}
}
🔒 权限控制
方法级权限注解
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
// ===== 1. 允许所有已认证用户 =====
@GetMapping("/me")
public Result<UserVO> getCurrentUser() {
// 只要登录即可访问
}
// ===== 2. 需要特定角色 =====
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')") // ⭐ 只有 ADMIN 角色能访问
public Result<UserVO> getUserById(@PathVariable Long id) {
// 检查当前用户是否有 ROLE_ADMIN
}
// ===== 3. 需要任意角色 =====
@PostMapping
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public Result<Void> createUser(@RequestBody UserDTO dto) {
// ADMIN 或 MANAGER 都可以访问
}
// ===== 4. 自定义权限表达式 =====
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == principal")
public Result<Void> updateUser(@PathVariable Long id) {
// ADMIN 可以修改任何人,普通用户只能修改自己
// principal 是当前登录用户的 ID
}
}
获取当前用户信息
// ===== 方式 1: 使用工具类(推荐)=====
Long userId = SecurityUtils.getCurrentUserId();
String username = SecurityUtils.getCurrentUsername();
List<String> roles = SecurityUtils.getCurrentUserRoles();
// ===== 方式 2: 直接从 SecurityContext =====
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = (Long) auth.getPrincipal();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
🚨 错误处理
常见认证错误
| HTTP 状态码 | 错误场景 | 解决方案 |
|---|---|---|
| 401 Unauthorized | Token 无效、过期或未提供 | 重新登录获取新 Token |
| 403 Forbidden | Token 有效但权限不足 | 联系管理员分配权限 |
| 400 Bad Request | 登录凭证格式错误 | 检查用户名密码是否正确 |
前端错误处理示例
// Axios 拦截器
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// Token 失效,跳转登录
localStorage.removeItem('token');
router.push('/login');
} else if (error.response.status === 403) {
// 权限不足
alert('您没有权限执行此操作');
}
return Promise.reject(error);
}
);
⚙️ 配置参数
application.yaml
app:
security:
# JWT 密钥(生产环境必须修改!)
jwt-secret: your-secret-key-change-this-in-production
# JWT 过期时间(毫秒)
jwt-expiration: 7200000 # 2 小时
# 公开 URL(无需认证)
permit-all-urls:
- /api/v1/auth/**
- /actuator/health
- /swagger-ui/**
- /v3/api-docs/**
重要提醒:
- ⚠️
jwt-secret必须足够复杂(建议 64 字符以上) - ⚠️ 生产环境使用环境变量或密钥管理服务
- ⚠️ 不要提交到 Git仓库
🔍 调试技巧
使用 jwt.io 解析 Token
- 访问 https://jwt.io/
- 粘贴你的 Token
- 查看 Payload 内容
日志调试
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(...) {
log.debug("请求路径: {}", request.getRequestURI());
log.debug("Authorization Header: {}", request.getHeader("Authorization"));
if (token != null) {
log.debug("提取到 Token: {}", token.substring(0, 20) + "...");
log.debug("Token 有效性: {}", jwtTokenProvider.validateToken(token));
}
}
}
💡 最佳实践
1. Token 存储
// ✅ 推荐:存储在 localStorage
localStorage.setItem('token', token);
// ❌ 不推荐:存储在 Cookie(容易受到 XSS 攻击)
document.cookie = `token=${token}`;
2. Token 刷新
Token 过期后:
1. 前端检测到 401 错误
2. 自动跳转到登录页
3. 用户重新登录获取新 Token
更好的方案(TODO):
1. 后端返回 accessToken + refreshToken
2. accessToken 过期后用 refreshToken 换取新的
3. refreshToken 也过期才需要重新登录
3. 安全建议
- ✅ 使用 HTTPS 传输 Token
- ✅ Token 有效期不要太长(2 小时推荐)
- ✅ 敏感操作需要二次验证(如修改密码)
- ❌ 不要在 URL 中传递 Token
- ❌ 不要在控制台打印完整 Token
📚 相关文档
- 用户管理详解 - 注册登录流程
- 角色权限管理 - 角色体系
- Security 配置 - 详细配置