Spring Security

技术栈
后端框架
javaspring安全认证授权OAuth2

概览

Spring Security

Spring Security 是 Spring 生态中负责认证(Authentication)和授权(Authorization)的安全框架。

是什么

基于 Filter Chain 的声明式安全框架,为 Java 应用提供全面的安全保护。支持表单登录、HTTP Basic、OAuth2/OIDC、JWT、SAML 等多种认证方式,以及基于角色/权限的访问控制。

解决什么问题

  • 认证:确认用户身份(你是谁)
  • 授权:控制访问权限(你能做什么)
  • 攻击防护:CSRF、Session Fixation、Clickjacking 等内置防护
  • 密码安全:BCrypt/SCrypt/PBKDF2 密码编码器

关键特性

  • Security Filter Chain:可插拔的安全过滤器链
  • 认证管理器:AuthenticationManager + 多个 AuthenticationProvider
  • 方法级别安全:@PreAuthorize / @PostAuthorize / @Secured
  • OAuth2 支持:资源服务器 + 客户端
  • CORS/CSRF:跨域和跨站请求伪造防护
  • Session 管理:并发会话控制、会话固定防护

安装

Spring Security 安装指南

1. 环境准备

前置条件

  • JDK 8+(Spring Security 6.x 需 JDK 17+)
  • Spring Boot 2.x 或 3.x 项目

构建工具

  • Maven 3.6+ 或 Gradle 7+

2. 安装步骤

Maven

<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>

<!-- OAuth2 资源服务器(可选) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

3. 常见安装问题

Q: 引入后所有接口 401?

Spring Security 默认保护所有端点。需配置 SecurityFilterChain 放开不需要认证的路径:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/public/**").permitAll()
        .anyRequest().authenticated()
    );
    return http.build();
}

Q: 默认密码在哪?

启动日志中会打印临时密码:

Using generated security password: 8f2c-xxxx-xxxx-xxxx

可通过配置覆盖:

spring.security.user.name=admin
spring.security.user.password=secret

Q: CSRF 导致 POST 请求 403?

在 REST API 中通常禁用 CSRF:

http.csrf(csrf -> csrf.disable());

但需确保使用 JWT 等无状态认证机制。

示例

Spring Security JWT 认证完整实现

目标

实现基于 JWT 的无状态认证:登录签发 Token、请求验证 Token、角色权限控制。

完整代码

1. JWT 工具类

package com.example.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtUtils {

    private final SecretKey key = Keys.hmacShaKeyFor(
        "my-256-bit-secret-key-my-256-bit-secret-key"
            .getBytes(StandardCharsets.UTF_8));
    private final long expirationMs = 86400000; // 24小时

    public String generateToken(String username, String role) {
        return Jwts.builder()
            .subject(username)
            .claim("role", role)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expirationMs))
            .signWith(key)
            .compact();
    }

    public String getUsernameFromToken(String token) {
        return parseClaims(token).getSubject();
    }

    public boolean validateToken(String token) {
        try {
            parseClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    private Claims parseClaims(String token) {
        return Jwts.parser()
            .verifyWith(key)
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}

2. JWT 认证过滤器

package com.example.security;

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.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);

            if (jwtUtils.validateToken(token)) {
                String username = jwtUtils.getUsernameFromToken(token);

                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                        username, null,
                        List.of(new SimpleGrantedAuthority("ROLE_USER"))
                    );
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }

        chain.doFilter(request, response);
    }
}

3. Security 配置

package com.example.config;

import com.example.security.JwtAuthFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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
@EnableMethodSecurity  // 启用 @PreAuthorize
public class SecurityConfig {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter,
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

    @Bean
    public AuthenticationManager authManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

4. 认证控制器

package com.example.controller;

import com.example.security.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authManager;

    @Autowired
    private JwtUtils jwtUtils;

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        Authentication auth = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.username(), request.password())
        );
        String token = jwtUtils.generateToken(
            auth.getName(), auth.getAuthorities().toString());
        return Map.of("token", token);
    }

    record LoginRequest(String username, String password) {}
}

5. 测试 API

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/public/hello")
    public String publicHello() {
        return "公开接口,无需认证";
    }

    @GetMapping("/user/profile")
    @PreAuthorize("isAuthenticated()")
    public String profile() {
        return "需要认证的接口";
    }

    @GetMapping("/admin/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminDashboard() {
        return "管理员专属接口";
    }
}

运行测试

# 1. 获取 Token
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"password"}'

# 2. 携带 Token 访问
curl http://localhost:8080/api/user/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."

# 3. 公开接口无需 Token
curl http://localhost:8080/api/public/hello

教程

Spring Security 安全架构与最佳实践教程

第一章:Security Filter Chain 架构

1.1 过滤器链执行顺序

请求 → ChannelProcessingFilter (HTTPS)
     → SecurityContextPersistenceFilter (加载 SecurityContext)
     → ConcurrentSessionFilter (会话并发)
     → UsernamePasswordAuthenticationFilter (表单登录)
     → BasicAuthenticationFilter (HTTP Basic)
     → CsrfFilter (CSRF 防护)
     → ExceptionTranslationFilter (异常转换)
     → FilterSecurityInterceptor (授权决策)
     → 业务 Controller

1.2 自定义过滤器插入位置

http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(myFilter, CsrfFilter.class);
http.addFilterAt(myFilter, UsernamePasswordAuthenticationFilter.class);

第二章:认证(Authentication)

2.1 认证流程

UsernamePasswordAuthenticationToken (未认证)
  → AuthenticationManager
    → AuthenticationProvider (多个)
      → UserDetailsService.loadUserByUsername()
      → PasswordEncoder.matches()
    → 返回已认证的 Authentication
  → SecurityContextHolder 保存

2.2 自定义 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() ->
                new UsernameNotFoundException("用户不存在: " + username));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .accountLocked(!user.isActive())
            .build();
    }
}

2.3 多种认证方式组合

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Login(oauth -> oauth
            .loginPage("/login")
            .defaultSuccessUrl("/dashboard"))
        .formLogin(form -> form
            .loginPage("/login"))
        .httpBasic(Customizer.withDefaults());

    return http.build();
}
// 同时支持:OAuth2 登录 + 表单登录 + Basic Auth

第三章:授权(Authorization)

3.1 基于角色的访问控制

// URL 级别
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
    .requestMatchers(HttpMethod.POST, "/api/products").hasRole("EDITOR")
);

// 方法级别
@PreAuthorize("hasRole('ADMIN')")
public void adminOnly() { }

@PreAuthorize("hasRole('USER') and #userId == authentication.name")
public void updateOwnProfile(Long userId) { }

@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) { }

3.2 自定义权限评估器

@Component("authz")
public class CustomAuthorization {
    public boolean isOwner(Authentication auth, Long resourceId) {
        // 自定义逻辑
        return true;
    }
}

// 使用
@PreAuthorize("@authz.isOwner(authentication, #id)")
public void updateResource(Long id) { }

第四章:常见安全漏洞与防护

4.1 CSRF

// REST API(无状态)可禁用
http.csrf(AbstractHttpConfigurer::disable);

// 传统 MVC 应用保留 CSRF Token
// Thymeleaf 自动在表单中注入 _csrf

4.2 CORS 配置

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://frontend.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);

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

4.3 密码安全

// 推荐 BCrypt
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    // {bcrypt}$2a$10$... 自动识别算法
}

// 密码强度规则
// 最低8位,含大小写字母+数字+特殊字符

第五章:OAuth2 与 JWT 最佳实践

5.1 Token 有效期策略

Access Token:15-30 分钟(短期)
Refresh Token:7-30 天(长期,存服务端)

5.2 Token 刷新机制

@PostMapping("/api/auth/refresh")
public Map<String, String> refresh(@RequestBody RefreshRequest req) {
    // 1. 验证 Refresh Token
    // 2. 检查是否在数据库中存在且未过期
    // 3. 签发新的 Access Token
    String newToken = jwtUtils.generateToken(username, role);
    return Map.of("accessToken", newToken);
}

5.3 黑名单机制

// 登出时将 Token 加入黑名单(Redis)
redisTemplate.opsForValue()
    .set("blacklist:" + token, "1", expiration, TimeUnit.MILLISECONDS);

// 过滤器检查
if (redisTemplate.hasKey("blacklist:" + token)) {
    throw new AccessDeniedException("Token 已失效");
}

思考题

  1. SecurityContextHolder 中的 SecurityContext 在线程间如何传递?
  2. 如何设计一个支持多租户的权限系统?
  3. JWT vs Session 认证各自的优缺点和适用场景?
  4. 如何防止暴力破解和撞库攻击?