认证与授权#

认证方式#

系统支持两种认证方式:

  1. 邮箱密码认证
  2. Google OAuth 2.0 认证

JWT 认证流程#

sequenceDiagram
    participant C as Client
    participant API as Backend API
    participant DB as Database

    C->>API: POST /api/auth/login {email, password}
    API->>DB: 查询用户
    DB-->>API: 返回用户信息
    API->>API: 验证密码(BCrypt)
    API->>API: 生成 JWT Token
    API-->>C: 返回 {token, user}

    Note over C: 保存 Token 到 localStorage

    C->>API: GET /api/users/me<br/>Authorization: Bearer {token}
    API->>API: 验证 JWT Token
    API->>DB: 查询用户信息
    DB-->>API: 返回用户数据
    API-->>C: 返回用户信息

JWT Token 结构#

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user@example.com",
    "userId": 1,
    "iat": 1234567890,
    "exp": 1234654290
  },
  "signature": "..."
}

字段说明:

  • sub: 用户邮箱(Subject)
  • userId: 用户 ID
  • iat: 签发时间(Issued At)
  • exp: 过期时间(Expiration,24 小时后)

Google OAuth 2.0 流程#

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant G as Google
    participant B as Backend

    U->>F: 点击「使用 Google 登入」
    F->>G: 打开 Google OAuth 授权页面
    U->>G: 授权应用
    G-->>F: 返回 Access Token
    F->>B: POST /api/auth/google {token}
    B->>G: 验证 Token 并获取用户信息
    G-->>B: 返回用户资料
    B->>B: 创建/更新用户
    B->>B: 生成 JWT Token
    B-->>F: 返回 {token, user}
    F->>F: 保存 Token
    F-->>U: 登录成功,跳转

安全配置#

密码加密#

使用 BCrypt 加密密码:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

特点:

  • 自动加盐(Salt)
  • 计算成本可调
  • 防止彩虹表攻击

JWT Secret#

要求:

  • 长度 >= 32 字符
  • 使用强随机字符串
  • 通过环境变量配置
# application.properties
jwt.secret=${JWT_SECRET}
jwt.expiration=86400000  # 24 hours in milliseconds

生成安全的 Secret:

openssl rand -base64 32

CORS 配置#

@Configuration
public class SecurityConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(
            Arrays.asList(corsOrigins.split(","))
        );
        configuration.setAllowedMethods(
            Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")
        );
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

权限控制#

公开端点#

无需认证即可访问:

"/api/auth/**",           // 认证相关
"/api/leaderboard/**",    // 排行榜
"/api/courses/**",        // 课程列表(基础信息)
"/actuator/health"        // 健康检查

受保护端点#

需要 JWT Token:

"/api/users/**",          // 用户信息
"/api/enrollments/**",    // 课程报名
"/api/missions/*/progress", // 学习进度
"/api/missions/*/claim"   // 领取奖励

自定义权限注解#

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireEnrollment {
    // 检查用户是否已报名课程
}

前端集成#

保存 Token#

// 登录成功后
const response = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password })
});

const data = await response.json();

// 保存到 localStorage
localStorage.setItem('token', data.token);

// 更新 Zustand store
useUserStore.setState({
  user: data.user,
  isAuthenticated: true
});

携带 Token 发送请求#

const token = localStorage.getItem('token');

const response = await fetch('/api/users/me', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

自动注销(Token 过期)#

const response = await fetch('/api/users/me', {
  headers: { 'Authorization': `Bearer ${token}` }
});

if (response.status === 401) {
  // Token 无效或过期
  localStorage.removeItem('token');
  useUserStore.setState({
    user: null,
    isAuthenticated: false
  });
  window.location.href = '/sign-in';
}

安全最佳实践#

1. Token 存储#

推荐: localStorage(简单场景) ❌ 避免: Cookie(除非 HttpOnly + Secure)

2. HTTPS#

生产环境必须使用 HTTPS:

  • 防止 Token 被拦截
  • 防止中间人攻击

3. Token 刷新#

当前实现:固定过期时间(24 小时)

改进方案(可选):

  • Refresh Token 机制
  • 滑动过期时间

4. 防止 CSRF#

JWT 存储在 localStorage 时自动防止 CSRF(不使用 Cookie)。

5. 输入验证#

@Valid
public ResponseEntity<?> login(@RequestBody @Valid LoginRequest request) {
    // Spring Validation 自动验证
}

public class LoginRequest {
    @Email
    @NotBlank
    private String email;

    @NotBlank
    @Size(min = 6)
    private String password;
}

故障排查#

Token 无效#

症状: 401 Unauthorized

检查:

  1. Token 是否过期
  2. JWT_SECRET 是否一致
  3. Token 格式是否正确(Bearer {token}

CORS 错误#

症状: Access-Control-Allow-Origin 错误

解决:

  1. 确认 CORS_ORIGINS 包含前端域名
  2. 检查请求方法是否允许
  3. 确认 Access-Control-Allow-Credentials 设置

Google OAuth 失败#

检查:

  1. GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET 是否正确
  2. 回调 URL 是否在 Google Console 中配置
  3. Google Token 是否有效