跳到主要内容

Spring Security 速查表

本章提供 Spring Security 常用配置的快速参考,方便在实际开发中查阅。

依赖配置

Maven

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

<!-- OAuth2 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

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

<!-- JWT 支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>

Gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'

基本配置

最小配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());

return http.build();
}
}

完整配置模板

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

// 表单登录
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
)

// 登出
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)

// 记住我
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400 * 14)
)

// 会话管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.expiredUrl("/login?expired")
)

// CSRF
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")
)

// CORS
.cors(Customizer.withDefaults())

// 安全头
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
);

return http.build();
}

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

@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User.builder()
.username("user")
.password(encoder.encode("password"))
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}
}

常用请求匹配器

// 路径匹配
.requestMatchers("/api/**").permitAll()
.requestMatchers("/*.html").permitAll()

// HTTP 方法匹配
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/posts/**").hasRole("AUTHOR")

// 正则表达式
.requestMatchers(RegexRequestMatcher.regexMatch("^/api/v[0-9]+/.*")).authenticated()

// 多条件组合
.requestMatchers(request ->
request.getRequestURI().startsWith("/api") &&
request.getMethod().equals("POST")
).hasRole("API_USER")

授权规则

.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // 允许所有
.requestMatchers("/blocked/**").denyAll() // 拒绝所有
.requestMatchers("/user/**").authenticated() // 需要认证
.requestMatchers("/guest/**").anonymous() // 仅匿名
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要角色
.requestMatchers("/manage/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/api/**").hasAuthority("API_ACCESS") // 需要权限
.requestMatchers("/data/**").hasAnyAuthority("READ", "WRITE")
.requestMatchers("/special/**").access("hasRole('VIP') and @checker.isAllowed()") // SpEL
)

认证配置

表单登录

.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
)

HTTP Basic

.httpBasic(basic -> basic
.realmName("My Application")
)

自定义认证成功/失败处理

.formLogin(form -> form
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
)

用户存储

内存用户

@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

manager.createUser(User.builder()
.username("user")
.password(encoder.encode("password"))
.roles("USER")
.build());

return manager;
}

JDBC 用户

@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);

manager.setUsersByUsernameQuery(
"SELECT username, password, enabled FROM users WHERE username = ?"
);
manager.setAuthoritiesByUsernameQuery(
"SELECT username, authority FROM authorities WHERE username = ?"
);

return manager;
}

自定义 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {

User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));

return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getAuthorities()
);
}
}

密码编码器

// BCrypt(推荐)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 委托编码器(支持多算法)
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

// Argon2(高安全)
@Bean
public PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder(16, 32, 1, 65536, 3);
}

方法安全

启用

@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize, @PostAuthorize
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed
)
public class MethodSecurityConfig {
}

常用注解

// 方法调用前检查
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAuthority('user:read')")
@PreAuthorize("#id == authentication.principal.id")
@PreAuthorize("@securityService.canAccess(#id, authentication)")

// 方法返回后检查
@PostAuthorize("returnObject.owner == authentication.name")

// 过滤参数
@PreFilter("filterObject.owner == authentication.name")

// 过滤返回值
@PostFilter("filterObject.owner == authentication.name")

// 简单角色检查
@Secured("ROLE_ADMIN")

// JSR-250
@RolesAllowed("ADMIN")
@PermitAll
@DenyAll

会话管理

.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建
.invalidSessionUrl("/login?expired") // 会话过期跳转
.maximumSessions(1) // 单点登录
.maxSessionsPreventsLogin(true) // 阻止新登录
.expiredUrl("/login?expired") // 被踢下线跳转
)

CSRF 配置

// 启用(默认)
.csrf(Customizer.withDefaults())

// 禁用
.csrf(csrf -> csrf.disable())

// 忽略特定路径
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**", "/webhook/**")
)

// Cookie 存储(前后端分离)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)

CORS 配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));

return http.build();
}

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

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

return source;
}

JWT 配置

资源服务器

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

return http.build();
}

JWT 配置

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com

异常处理

.exceptionHandling(ex -> ex
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.accessDeniedPage("/access-denied")
)

安全头

.headers(headers -> headers
.contentTypeOptions(Customizer.withDefaults())
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'")
)
)

多过滤器链

@Configuration
@EnableWebSecurity
public class MultiSecurityConfig {

@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());

return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());

return http.build();
}
}

测试支持

测试依赖

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

常用测试注解

@WithMockUser(username = "user", roles = "USER")
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
@WithAnonymousUser
@WithUserDetails("customuser")

MockMvc 测试

@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests {

@Autowired
private MockMvc mockMvc;

@Test
void unauthorizedRequest() throws Exception {
mockMvc.perform(get("/api/private"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockUser(roles = "USER")
void authorizedRequest() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}

@Test
void withCsrf() throws Exception {
mockMvc.perform(post("/api/data")
.with(csrf()))
.andExpect(status().isOk());
}

@Test
void formLogin() throws Exception {
mockMvc.perform(formLogin("/login")
.user("username", "user")
.password("password", "password"))
.andExpect(authenticated().withUsername("user"));
}
}

获取当前用户

// 方式 1:通过 Authentication 参数
@GetMapping("/profile")
public String profile(Authentication authentication) {
String username = authentication.getName();
// ...
}

// 方式 2:通过 @AuthenticationPrincipal
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal UserDetails user) {
String username = user.getUsername();
// ...
}

// 方式 3:通过 SecurityContextHolder
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

// 方式 4:注入 SecurityContextHolderStrategy
@Autowired
private SecurityContextHolderStrategy strategy;

@GetMapping("/profile")
public String profile() {
Authentication auth = strategy.getContext().getAuthentication();
// ...
}

常见问题排查

启用调试日志

logging:
level:
org.springframework.security: DEBUG

常见错误

错误可能原因解决方案
401 Unauthorized未认证检查登录状态,添加认证
403 Forbidden权限不足检查用户角色/权限配置
重定向循环登录页面需要认证确保登录页面 permitAll()
CSRF Token 无效Token 缺失或不匹配检查表单是否包含 CSRF Token
Session 失效会话过期或被踢检查会话配置

配置文件参考

# application.yml
spring:
security:
user:
name: admin
password: admin123
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
resourceserver:
jwt:
issuer-uri: https://auth.example.com

server:
servlet:
session:
timeout: 30m
cookie:
http-only: true
secure: true