第 64 章:Spring Security 入门
学习目标
- 理解认证(Authentication)与授权(Authorization)的本质
- 掌握 Spring Security 6 的核心过滤器链
- 学会自定义登录、密码加密、权限校验
一、为什么需要安全框架?
Spring Security 是 Spring 全家桶里最强大的安全框架,处理:
- 认证(Authentication):登录、Session、Token
- 授权(Authorization):谁能访问什么资源
- 防护:CSRF、XSS、Session 固定攻击、Clickjacking
二、核心架构:过滤器链
核心机制:每个请求要穿过一串过滤器,过滤器链是 Spring Security 的核心。
三、快速集成
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>java
// 启动类加 @EnableWebSecurity
@SpringBootApplication
@EnableWebSecurity
public class TaskflowApplication { }yaml
# 默认配置下,所有接口都需要认证
# 浏览器访问 → 自动跳到默认登录页(账号 user,密码控制台随机生成)四、自定义安全配置(Spring Security 6 新写法)
java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// ① 禁用 CSRF(前后端分离 + JWT 方案不需要)
.csrf(AbstractHttpConfigurer::disable)
// ② 配置 CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ③ 禁用 Session(无状态)
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// ④ 配置路径权限
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册放行
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
.anyRequest().authenticated() // 其他需要登录
)
// ⑤ 配置自定义认证
.authenticationProvider(authenticationProvider())
// ⑥ 加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
// ⑦ 自定义登录失败 / 权限不足的响应
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, resp, e) -> {
resp.setStatus(401);
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(JSON.toJSONString(
Result.fail(401, "未登录或登录已过期")));
})
.accessDeniedHandler((req, resp, e) -> {
resp.setStatus(403);
resp.getWriter().write(JSON.toJSONString(
Result.fail(403, "权限不足")));
})
)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.addAllowedOriginPattern("*");
cfg.addAllowedHeader("*");
cfg.addAllowedMethod("*");
cfg.setAllowCredentials(true);
cfg.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", cfg);
return source;
}
}五、UserDetailsService:加载用户
java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ① 查用户
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// ② 查角色和权限
List<Role> roles = roleMapper.selectByUserId(user.getId());
List<GrantedAuthority> authorities = roles.stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r.getCode()))
.collect(Collectors.toList());
// ③ 查细粒度权限(可选)
List<String> perms = permissionMapper.selectByUserId(user.getId());
perms.forEach(p -> authorities.add(new SimpleGrantedAuthority(p)));
// ④ 返回 Spring Security 用户对象
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.disabled(user.getStatus() != 1)
.accountLocked(user.getStatus() == 2)
.build();
}
}六、密码加密
java
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // ① 推荐
}
}
@Service
public class UserService {
private final PasswordEncoder encoder;
public void register(UserDTO dto) {
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(encoder.encode(dto.getPassword())); // ② 加密
userMapper.insert(user);
}
public boolean login(String username, String password) {
User user = userMapper.selectByUsername(username);
return encoder.matches(password, user.getPassword()); // ③ 校验
}
}BCrypt 原理
java
// BCrypt 每次加密结果都不同(自带 salt)
encoder.encode("123456"); // $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
encoder.encode("123456"); // $2a$10$WQu9wYAgPvuK3i5Yj8yM.eIjZAgcfl7p92ldGxad68LJZdL17lhWy (不一样)
// 长度 60 字符,前 22 位是 salt + cost
// ❌ 错误:用 MD5 + 固定 salt(不安全,容易被彩虹表攻击)
String md5 = DigestUtils.md5Hex(password + "salt");
// ❌ 错误:明文存储
user.setPassword(password);
// ❌ 错误:用可解密的加密
String encrypted = AES.encrypt(password); // 能解密就有泄露风险| 算法 | 安全性 | 推荐度 |
|---|---|---|
| MD5 / SHA1 | ❌ 已破解 | ❌ |
| SHA256 + salt | ⚠️ 算力攻击 | ⚠️ |
| BCrypt | ✅ 自带 salt,可调 cost | ✅ 推荐 |
| Argon2 | ✅ 最强 | ✅ 但 Spring Boot 默认不支持 |
| SCrypt | ✅ 强 | ⚠️ |
七、JWT 集成(前后端分离主流方案)
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private Long expire; // 秒
// ① 生成 token
public String generate(Long userId, String username, List<String> roles) {
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("username", username)
.claim("roles", roles)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expire * 1000))
.signWith(getKey(), Jwts.SIG.HS256)
.compact();
}
// ② 解析 token
public Claims parse(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
}yaml
jwt:
secret: ${JWT_SECRET} # 至少 256 位(32 字节)
expire: 7200 # 2 小时JWT 过滤器
java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse resp,
FilterChain chain)
throws ServletException, IOException {
// ① 提取 token
String token = extractToken(req);
if (token == null) {
chain.doFilter(req, resp); // 放行(后面的过滤器决定)
return;
}
try {
// ② 解析 token
Claims claims = jwtUtil.parse(token);
String username = claims.get("username", String.class);
// ③ 设置认证上下文
if (username != null && SecurityContextHolder.getContext()
.getAuthentication() == null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (ExpiredJwtException e) {
log.warn("Token 已过期");
SecurityContextHolder.clearContext();
} catch (JwtException e) {
log.warn("Token 非法: {}", e.getMessage());
SecurityContextHolder.clearContext();
}
chain.doFilter(req, resp);
}
private String extractToken(HttpServletRequest req) {
String bearer = req.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}登录接口
java
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody LoginDTO dto) {
// ① 认证(自动调用 UserDetailsService + PasswordEncoder)
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
dto.getUsername(), dto.getPassword()));
// ② 生成 token
UserDetails user = (UserDetails) auth.getPrincipal();
String token = jwtUtil.generate(
extractUserId(user), user.getUsername(),
user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList());
return Result.ok(new LoginVO(token, user.getUsername()));
}
}八、方法级权限控制
java
@Service
public class UserService {
// ① 拥有 ROLE_ADMIN 角色才能调用
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// ② 拥有 user:create 权限才能调用
@PreAuthorize("hasAuthority('user:create')")
public void createUser(UserDTO dto) {
userMapper.insert(toUser(dto));
}
// ③ 同时满足两个条件
@PreAuthorize("hasRole('ADMIN') and #dto.userId == authentication.principal.id")
public void updateProfile(@RequestBody UserDTO dto) {
// ...
}
// ④ 表达式语法(SpEL)
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
@PostAuthorize("returnObject.owner == authentication.name") // 返回后校验
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
}启动类加 @EnableMethodSecurity:
java
@EnableMethodSecurity(prePostEnabled = true)
public class TaskflowApplication { }九、CSRF 与 CORS
CSRF(默认开启,前后端分离要关)
java
// ❌ 前后端分离 + JWT:禁用 CSRF
http.csrf(AbstractHttpConfigurer::disable);
// ✅ 传统 Session 方案:保留 CSRF
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalseHandler()));CORS
java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.addAllowedOriginPattern("https://*.taskflow.com");
cfg.addAllowedHeader("*");
cfg.addAllowedMethod("*");
cfg.setAllowCredentials(true);
cfg.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", cfg);
return source;
}十、常见安全配置
java
// ① 同一账号最多一个会话
http.sessionManagement(s -> s
.maximumSessions(1)
.maxSessionsPreventsLogin(true));
// ② Session 过期处理
http.sessionManagement(s -> s
.invalidSessionStrategy((req, resp, ex) ->
resp.sendError(401, "Session 已过期")));
// ③ 限制登录失败次数
http.formLogin(form -> form
.failureHandler((req, resp, ex) -> {
// 记录失败次数
if (failureCount > 5) lockAccount(username);
}));
// ④ 登录成功处理
http.formLogin(form -> form
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(JSON.toJSONString(Result.ok("登录成功")));
}));十一、本章小结
| 要点 | 关键 |
|---|---|
| 核心 | 过滤器链(Filter Chain) |
| 认证流程 | 用户名/密码 → UserDetailsService → PasswordEncoder → AuthenticationManager |
| 密码加密 | BCrypt(自带 salt,不可逆) |
| JWT | 无状态,前后端分离主流 |
| 路径权限 | authorizeHttpRequests().requestMatchers(...).hasRole(...) |
| 方法权限 | @PreAuthorize("hasAuthority('user:create')") |
| CSRF | 前后端分离 + JWT 时关闭 |
| 入口拦截 | authenticationEntryPoint 返回 401 |
| 权限不足 | accessDeniedHandler 返回 403 |
动手练习
练习 1:基础题
实现用户登录接口:用户名密码验证 + BCrypt 校验 + 返回 JWT token。
练习 2:进阶题
实现 JWT 过滤器:从 Header 提取 token → 解析 → 设置 SecurityContext → 放行到 Controller。
练习 3:思考题
你的系统有三种角色:超管、普通管理员、普通用户。如何设计权限模型?菜单权限和数据权限怎么实现?
下一章:第 65 章:JWT 深入 →