8. Spring Boot 认证与授权

4 阅读7分钟

一、认证与授权概述

1. 核心概念

概念说明示例
认证(Authentication)验证用户身份"你是谁?"(登录验证)
授权(Authorization)验证用户权限"你能做什么?"(访问控制)

2. 为什么需要认证与授权?

场景问题解决方案
用户登录防止未授权访问认证机制
资源保护防止越权操作授权机制
API 安全防止恶意调用Token 认证
数据安全防止数据泄露权限控制

二、JWT(JSON Web Token)认证

1. JWT 结构

JWT 由三部分组成:

Header.Payload.Signature

例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header(头部)

{
  "alg": "HS256",      // 签名算法
  "typ": "JWT"         // 令牌类型
}

2. Payload(载荷)

{
  "sub": "1234567890",    // 用户 ID(标准声明)
  "name": "John Doe",     // 用户名(自定义声明)
  "email": "john@example.com",
  "role": "ADMIN",        // 角色
  "iat": 1516239022,      // 签发时间
  "exp": 1516242622       // 过期时间
}

3. Signature(签名)

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

2. JWT 认证流程

1. 用户登录(POST /api/auth/login)
   前端 → 后端:{ username, password }

2. 后端验证用户名和密码
   - 验证成功:生成 JWT Token
   - 验证失败:返回 401 错误

3. 返回 JWT Token
   后端 → 前端:{ token: "eyJhbGci..." }

4. 前端保存 Token
   - LocalStorage
   - SessionStorage
   - Cookie

5. 访问受保护资源(GET /api/users)
   前端 → 后端:Header: Authorization: Bearer eyJhbGci...

6. 后端验证 Token
   - 验证成功:返回资源
   - 验证失败:返回 401 错误

7. Token 过期处理
   - 前端:跳转登录页
   - 后端:提示重新登录

3. JWT 配置

添加依赖

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

JWT 工具类

JwtTokenProvider.java:

package com.example.myapp.config;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {
    
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    
    @Value("${app.jwt.expiration-ms}")
    private long jwtExpirationMs;
    
    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }
    
    // 生成 Token
    public String generateToken(Long userId, String username, String role) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
        
        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .claim("username", username)
                .claim("role", role)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }
    
    // 从 Token 中获取用户 ID
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }
    
    // 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.get("username", String.class);
    }
    
    // 从 Token 中获取角色
    public String getRoleFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.get("role", String.class);
    }
    
    // 验证 Token
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

配置文件

application.properties:

# JWT 配置
app.jwt.secret=MySuperSecretKeyForJWTTokenGeneration123456789
app.jwt.expiration-ms=86400000  # 24 小时

4. Spring Security 配置

Security 配置类

SecurityConfig.java:

package com.example.myapp.config;

import com.example.myapp.security.JwtAuthenticationEntryPoint;
import com.example.myapp.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用 CSRF(JWT 不需要)
                .csrf(csrf -> csrf.disable())
                
                // 设置会话管理为无状态(JWT)
                .sessionManagement(session -> 
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                
                // 设置异常处理
                .exceptionHandling(exception -> 
                        exception.authenticationEntryPoint(unauthorizedHandler)
                )
                
                // 设置授权规则
                .authorizeHttpRequests(auth -> auth
                        // 公开接口:登录、注册
                        .requestMatchers("/api/auth/**").permitAll()
                        // 公开接口:Swagger、Actuator
                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll()
                        // 其他接口需要认证
                        .anyRequest().authenticated()
                );
        
        // 添加 JWT 过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

JWT 认证入口点

JwtAuthenticationEntryPoint.java:

package com.example.myapp.security;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException
    ) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(
                "{\"status\":401,\"message\":\"未授权访问,请先登录\"}"
        );
    }
}

JWT 认证过滤器

JwtAuthenticationFilter.java:

package com.example.myapp.security;

import com.example.myapp.config.JwtTokenProvider;
import com.example.myapp.model.User;
import com.example.myapp.repository.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            // 从请求中获取 Token
            String jwt = getJwtFromRequest(request);
            
            // 验证 Token
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                // 从 Token 中获取用户 ID
                Long userId = tokenProvider.getUserIdFromToken(jwt);
                
                // 从数据库获取用户
                User user = userRepository.findById(userId).orElse(null);
                
                if (user != null) {
                    // 创建认证对象
                    UsernamePasswordAuthenticationToken authentication = 
                            new UsernamePasswordAuthenticationToken(
                                    user,
                                    null,
                                    new ArrayList<>()
                            );
                    authentication.setDetails(
                            new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    
                    // 设置认证信息到 Security Context
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

5. 认证 API 实现

AuthController(认证控制器)

AuthController.java:

package com.example.myapp.controller;

import com.example.myapp.dto.LoginDTO;
import com.example.myapp.dto.RegisterDTO;
import com.example.myapp.model.User;
import com.example.myapp.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    private final AuthService authService;
    
    public AuthController(AuthService authService) {
        this.authService = authService;
    }
    
    // 登录
    @PostMapping("/login")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDTO loginDTO) {
        return ResponseEntity.ok(authService.login(loginDTO));
    }
    
    // 注册
    @PostMapping("/register")
    public ResponseEntity<?> register(@Valid @RequestBody RegisterDTO registerDTO) {
        return ResponseEntity.ok(authService.register(registerDTO));
    }
}

AuthDTO(认证 DTO)

LoginDTO.java:

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class LoginDTO {
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    private String password;
}

RegisterDTO.java:

package com.example.myapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class RegisterDTO {
    
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
    private String name;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
    private String password;
    
    @Size(max = 20, message = "电话号码长度不能超过 20 个字符")
    private String phone;
}

AuthService(认证服务)

AuthService.java:

package com.example.myapp.service;

import com.example.myapp.config.JwtTokenProvider;
import com.example.myapp.dto.LoginDTO;
import com.example.myapp.dto.RegisterDTO;
import com.example.myapp.model.User;
import com.example.myapp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class AuthService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    // 登录
    public Map<String, Object> login(LoginDTO loginDTO) {
        // 根据邮箱查找用户
        User user = userRepository.findByEmail(loginDTO.getEmail())
                .orElseThrow(() -> new RuntimeException("邮箱或密码错误"));
        
        // 验证密码
        if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
            throw new RuntimeException("邮箱或密码错误");
        }
        
        // 生成 JWT Token
        String token = tokenProvider.generateToken(
                user.getId(),
                user.getEmail(),
                user.getRole() != null ? user.getRole().name() : "USER"
        );
        
        // 返回 Token 和用户信息
        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("user", user);
        
        return result;
    }
    
    // 注册
    public Map<String, Object> register(RegisterDTO registerDTO) {
        // 检查邮箱是否已存在
        if (userRepository.existsByEmail(registerDTO.getEmail())) {
            throw new RuntimeException("邮箱已被使用");
        }
        
        // 创建用户
        User user = new User();
        user.setName(registerDTO.getName());
        user.setEmail(registerDTO.getEmail());
        user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
        user.setPhone(registerDTO.getPhone());
        user.setActive(true);
        
        // 保存用户
        User savedUser = userRepository.save(user);
        
        // 生成 JWT Token
        String token = tokenProvider.generateToken(
                savedUser.getId(),
                savedUser.getEmail(),
                "USER"
        );
        
        // 返回 Token 和用户信息
        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("user", savedUser);
        
        return result;
    }
}

6. 前端对接示例

React 示例

AuthService.js:

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api/auth';

export default {
    // 登录
    async login(email, password) {
        const response = await axios.post(`${API_BASE_URL}/login`, {
            email,
            password
        });
        return response.data;
    },
    
    // 注册
    async register(userData) {
        const response = await axios.post(`${API_BASE_URL}/register`, userData);
        return response.data;
    }
};

Login.js:

import { useState } from 'react';
import authService from './AuthService';
import { useNavigate } from 'react-router-dom';

function Login() {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [error, setError] = useState('');
    const navigate = useNavigate();
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const data = await authService.login(email, password);
            // 保存 Token 到 LocalStorage
            localStorage.setItem('token', data.token);
            localStorage.setItem('user', JSON.stringify(data.user));
            // 跳转到首页
            navigate('/');
        } catch (err) {
            setError('登录失败:' + err.response?.data?.message || err.message);
        }
    };
    
    return (
        <div>
            <h2>登录</h2>
            {error && <div style={{color: 'red'}}>{error}</div>}
            <form onSubmit={handleSubmit}>
                <div>
                    <label>邮箱:</label>
                    <input 
                        type="email"
                        value={email}
                        onChange={(e) => setEmail(e.target.value)}
                        required
                    />
                </div>
                <div>
                    <label>密码:</label>
                    <input 
                        type="password"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                        required
                    />
                </div>
                <button type="submit">登录</button>
            </form>
        </div>
    );
}

export default Login;

API 请求拦截器

axios.js:

import axios from 'axios';

const api = axios.create({
    baseURL: 'http://localhost:8080/api'
});

// 请求拦截器:添加 Token
api.interceptors.request.use(
    (config) => {
        const token = localStorage.getItem('token');
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`;
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

// 响应拦截器:处理 401 错误
api.interceptors.response.use(
    (response) => response,
    (error) => {
        if (error.response?.status === 401) {
            // 清除 Token
            localStorage.removeItem('token');
            localStorage.removeItem('user');
            // 跳转到登录页
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
);

export default api;

7. 测试 API

登录

请求:

POST http://localhost:8080/api/auth/login
Content-Type: application/json

{
  "email": "zhangsan@example.com",
  "password": "123456"
}

响应:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "name": "张三",
    "email": "zhangsan@example.com",
    "phone": "13800138000",
    "active": true,
    "createdAt": "2024-03-26T15:30:00",
    "updatedAt": "2024-03-26T15:30:00"
  }
}

注册

请求:

POST http://localhost:8080/api/auth/register
Content-Type: application/json

{
  "name": "李四",
  "email": "lisi@example.com",
  "password": "123456",
  "phone": "13900139000"
}

响应:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 2,
    "name": "李四",
    "email": "lisi@example.com",
    "phone": "13900139000",
    "active": true,
    "createdAt": "2024-03-26T16:00:00",
    "updatedAt": "2024-03-26T16:00:00"
  }
}

访问受保护资源

请求:

GET http://localhost:8080/api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

响应:

{
  "content": [...],
  "pageable": {...},
  "totalPages": 1,
  "totalElements": 2
}

三、Spring Security 授权

1. 角色定义

public enum Role {
    ADMIN,   // 管理员
    USER     // 普通用户
}

2. 实体类添加角色字段

User.java:

@Entity
public class User {
    // ... 其他字段
    
    @Enumerated(EnumType.STRING)
    private Role role = Role.USER;  // 默认角色为普通用户
}

3. 配置授权规则

SecurityConfig.java:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> 
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(auth -> auth
                        // 公开接口
                        .requestMatchers("/api/auth/**").permitAll()
                        
                        // 管理员接口
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        
                        // 普通用户接口
                        .requestMatchers("/api/users/**").hasAnyRole("ADMIN", "USER")
                        
                        // 其他接口需要认证
                        .anyRequest().authenticated()
                );
        
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

4. 方法级授权

AdminController.java:

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    
    // 只有 ADMIN 角色可以访问
    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        // ...
    }
    
    // 只有 ADMIN 角色可以删除用户
    @DeleteMapping("/users/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        // ...
    }
    
    // 用户可以查看自己的信息
    @GetMapping("/profile")
    @PreAuthorize("#id == authentication.principal.id")
    public ResponseEntity<UserDTO> getProfile(@PathVariable Long id) {
        // ...
    }
}

四、总结

概念说明
认证验证用户身份(登录)
授权验证用户权限(访问控制)
JWT无状态 Token 认证
Spring Security安全框架
RBAC基于角色的访问控制