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