跳到主要内容

认证授权详解

本文深入讲解基于 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 UnauthorizedToken 无效、过期或未提供重新登录获取新 Token
403 ForbiddenToken 有效但权限不足联系管理员分配权限
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

  1. 访问 https://jwt.io/
  2. 粘贴你的 Token
  3. 查看 Payload 内容

JWT Debugger

日志调试

@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

📚 相关文档