一、认证与授权概述
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 | 基于角色的访问控制 |