第 43 课:Spring Boot安全
本课目标:掌握Spring Security基础配置,理解JWT认证流程,实现基于角色的访问控制。
一、概念讲解
1.1 Spring Security核心架构
请求过滤链:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 客户端 │───>│ Filter1 │───>│ Filter2 │───>│ FilterN │───> Controller
│ │<───│ (认证) │<───│ (授权) │<───│ (安全) │<───
└──────────┘ └──────────┘ └──────────┘ └──────────┘
1.2 认证 vs 授权
认证(Authentication):你是谁? → 登录验证身份
授权(Authorization):你能干什么? → 权限控制
┌─────────────────────────────────────────────────┐
│ 用户登录 │
│ ┌─────────┐ ┌────────────┐ ┌──────────┐ │
│ │ 用户名 │───>│ Security │───>│ JWT Token │ │
│ │ 密码 │ │ 过滤器链 │ │ (返回) │ │
│ └─────────┘ └────────────┘ └──────────┘ │
│ │
│ 访问资源 │
│ ┌─────────┐ ┌────────────┐ ┌──────────┐ │
│ │ 请求+ │───>│ JWT过滤器 │───>│ Controller│ │
│ │ Token │ │ 验证Token │ │ (返回) │ │
│ └─────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
1.3 JWT结构
JWT由三部分组成:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9 ← Header (算法类型)
.
eyJzdWIiOiIxMjM0NTY3ODkwIn0 ← Payload (载荷/用户信息)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (签名)
二、语法格式
2.1 Security配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // 禁用CSRF(REST API)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 公开接口
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // 其他需认证
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
2.2 JWT相关方法
// 生成Token
String token = jwtUtils.generateToken(userId, username, role);
// 解析Token
Claims claims = jwtUtils.parseToken(token);
// 验证Token
boolean valid = jwtUtils.validateToken(token);
三、代码案例
3.1 添加依赖
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 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>
3.2 配置文件
# application.yml
jwt:
secret: mySecretKeyForJwtTokenGenerationMustBeLongEnough2024!
expiration: 86400000 # 24小时(毫秒)
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
3.3 JWT工具类
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
// 生成Token
public String generateToken(Long userId, String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
return Jwts.builder()
.claims(claims)
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
// 解析Token
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 验证Token是否有效
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
return !claims.getExpiration().before(new Date());
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
// 从Token中获取用户ID
public Long getUserId(String token) {
Claims claims = parseToken(token);
return ((Number) claims.get("userId")).longValue();
}
// 从Token中获取用户名
public String getUsername(String token) {
return parseToken(token).getSubject();
}
// 从Token中获取角色
public String getRole(String token) {
Claims claims = parseToken(token);
return (String) claims.get("role");
}
}
3.4 JWT认证过滤器
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
public JwtFilter(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsername(token);
String role = jwtUtils.getRole(token);
SimpleGrantedAuthority authority =
new SimpleGrantedAuthority("ROLE_" + role);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username, null, Collections.singletonList(authority));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3.5 Security配置类
package com.example.demo.config;
import com.example.demo.security.JwtFilter;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
3.6 用户实体(数据库)
package com.example.demo.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false, length = 20)
private String role; // ROLE_USER, ROLE_ADMIN
@Column(columnDefinition = "INT DEFAULT 1")
private int status;
// 构造方法
public User() {}
public User(String username, String email, String password, String role) {
this.username = username;
this.email = email;
this.password = password;
this.role = role;
this.status = 1;
}
// getter/setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
}
3.7 认证Controller(登录/注册)
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.security.JwtUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtils jwtUtils;
public AuthController(UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtUtils jwtUtils) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtils = jwtUtils;
}
// 注册
@PostMapping("/register")
public Result<User> register(@RequestBody User user) {
if (userRepository.existsByUsername(user.getUsername())) {
return Result.error(400, "用户名已存在");
}
if (userRepository.existsByEmail(user.getEmail())) {
return Result.error(400, "邮箱已被注册");
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRole("USER");
user.setStatus(1);
User saved = userRepository.save(user);
saved.setPassword(null); // 不返回密码
return Result.success("注册成功", saved);
}
// 登录
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody User loginUser) {
User user = userRepository.findByUsername(loginUser.getUsername())
.orElse(null);
if (user == null || !passwordEncoder.matches(
loginUser.getPassword(), user.getPassword())) {
return Result.error(401, "用户名或密码错误");
}
if (user.getStatus() == 0) {
return Result.error(403, "账号已被禁用");
}
String token = jwtUtils.generateToken(
user.getId(), user.getUsername(), user.getRole());
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("username", user.getUsername());
result.put("role", user.getRole());
return Result.success("登录成功", result);
}
// 获取当前用户信息
@GetMapping("/info")
public Result<Map<String, Object>> getUserInfo(
@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
String username = jwtUtils.getUsername(token);
String role = jwtUtils.getRole(token);
Map<String, Object> info = new HashMap<>();
info.put("username", username);
info.put("role", role);
return Result.success(info);
}
}
3.8 受保护的业务接口
package com.example.demo.controller;
import com.example.demo.common.Result;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
@PreAuthorize("hasRole('ADMIN')")
public Result<Map<String, Object>> dashboard() {
Map<String, Object> data = Map.of(
"totalUsers", 100,
"activeToday", 25,
"revenue", 50000
);
return Result.success(data);
}
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public Result<String> manageUsers() {
return Result.success("管理员可以查看所有用户");
}
@GetMapping("/settings")
@PreAuthorize("hasRole('ADMIN')")
public Result<String> settings() {
return Result.success("管理员可以管理系统设置");
}
}
3.9 需认证的普通接口
package com.example.demo.controller;
import com.example.demo.common.Result;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserCenterController {
@GetMapping("/profile")
public Result<Map<String, Object>> profile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
Map<String, Object> profile = Map.of(
"username", username,
"message", "这是你的个人信息页面"
);
return Result.success(profile);
}
@GetMapping("/settings")
public Result<String> settings() {
return Result.success("用户可以修改自己的设置");
}
}
四、常见错误
4.1 403 Forbidden
原因:未携带Token或Token无效。
解决:请求头添加Authorization: Bearer <token>。
4.2 Bad credentials
原因:密码不匹配,或未使用BCrypt编码。
解决:注册时必须用passwordEncoder.encode()加密密码。
4.3 Method not allowed
原因:Spring Security默认禁止CSRF,但表单提交需要。
解决:REST API禁用CSRF,表单场景启用CSRF并携带Token。
4.4 Token expired
原因:JWT过期。
解决:实现Token刷新机制,或在前端检测过期后重新登录。
五、课后练习
- 实现密码修改接口,需要验证旧密码后才能设置新密码
- 添加角色
ADMIN,测试@PreAuthorize的权限控制 - 实现简单的Token刷新接口:当Token剩余有效期小于1小时时,返回新Token
- 拦截未认证请求,返回统一的JSON错误信息(而非跳转登录页)
六、本课小结
| 知识点 | 说明 |
|---|---|
| Spring Security | Spring生态中的安全框架,提供认证和授权能力 |
| JWT | 无状态Token机制,适合REST API认证 |
| BCrypt | 密码加密算法,Spring Security默认推荐 |
| @PreAuthorize | 方法级别权限控制,支持SpEL表达式 |
| Stateless | 无状态会话,每次请求携带Token,不依赖Session |
| JwtFilter | 自定义过滤器,拦截请求验证Token并设置认证信息 |
本课程持续更新中,欢迎关注!