Spring Security 6.x 实战指南
·
Spring Security 6.x 实战指南
Spring Security 6.x 是 Spring Security 历史上最重要的版本升级,本指南深入解析其核心架构、Lambda DSL配置、过滤器链机制,以及与OAuth2、JWT的无缝集成方案。
一、版本重大变化概览
1.1 6.x vs 5.x 核心差异
1.2 主要破坏性变更
| 变更项 | 5.x | 6.x | 影响 |
|---|---|---|---|
| 配置方式 | extends WebSecurityConfigurerAdapter | @Bean SecurityFilterChain | 所有现有项目需迁移 |
| Lambda DSL | 可选 | 推荐 | 代码更简洁 |
| hasRole() | 需手动加 ROLE_ 前缀 | 自动添加 ROLE_ | 需检查现有代码 |
| authorizeHttpRequests | 不存在 | 必须使用 | 权限配置必改 |
| requestMatchers | antMatchers | 基于Ant风格但更灵活 | 路径匹配更精确 |
1.3 迁移路线图
二、核心组件体系
2.1 架构总览
2.2 SecurityFilterChain 详解
/**
* SecurityFilterChain 是 6.x 的核心配置单元
* 替代了 5.x 的 WebSecurityConfigurerAdapter
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
// API 白名单
.securityMatcher("/api/**", "/health/**")
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
// Web 页面配置
.securityMatcher("/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard"))
.logout(logout -> logout
.logoutSuccessUrl("/login?logout"));
return http.build();
}
}
2.3 默认过滤器链
Spring Security 6.x 默认包含以下过滤器(按执行顺序):
// org.springframework.security.web.FilterOrderResolver
// 过滤器执行顺序(部分)
1. ChannelProcessingFilter // 通道处理(HTTP/HTTPS)
2. WebAsyncManagerIntegrationFilter // 异步管理集成
3. SecurityContextHolderFilter // 安全上下文持有
4. WebSocketHandlerFilter // WebSocket 处理
5. CorsFilter // CORS 处理
6. CsrfFilter // CSRF 防护
7. LogoutFilter // 登出处理
8. OAuth2AuthorizationRequestRedirectFilter // OAuth2 重定向
9. UsernamePasswordAuthenticationFilter // 用户名密码认证
10. ConcurrentSessionFilter // 并发会话控制
11. BearerTokenAuthenticationFilter // Bearer Token 认证
12. BasicAuthenticationFilter // HTTP Basic 认证
13. RequestCacheAwareFilter // 请求缓存
14. SecurityContextHolderAwareRequestFilter // 安全上下文请求
15. AnonymousAuthenticationFilter // 匿名认证
16. ExceptionTranslationFilter // 异常转换
17. AuthorizationFilter // 权限授权(6.x 新增)
2.4 过滤器链执行流程
三、Lambda DSL 深度解析
3.1 Lambda DSL vs 传统配置
5.x 传统配置方式:
// 5.x 配置(已废弃)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
6.x Lambda DSL 方式:
// 6.x 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN") // 自动添加 ROLE_ 前缀
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home"))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
}
3.2 常用配置示例
3.2.1 HTTP Basic 认证
@Bean
public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated())
.httpBasic(basic -> basic
.realmName("My API"));
return http.build();
}
3.2.2 表单登录
@Bean
public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/error", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/doLogin")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error")
.successHandler(loginSuccessHandler())
.failureHandler(loginFailureHandler()))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.addLogoutHandler(logoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID"));
return http.build();
}
3.2.3 OAuth2 社交登录
@Bean
public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/error").permitAll()
.anyRequest().authenticated())
.oauth2Login(oauth2 -> oauth2
.loginPage("/login/oauth2")
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorization"))
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/code/*"))
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService())
.userService(oAuth2UserService())
.customUserType(GoogleUser.class, "google"))
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailureHandler()))
.oidcLogin(OidcConfigurer -> {})
.tokenEndpoint(token -> token
.accessTokenResponseClient(accessTokenResponseClient()));
return http.build();
}
3.2.4 JWT 无状态认证
@Bean
public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder())));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation("http://auth-server");
}
3.3 常用配置方法对照表
| 5.x 方法 | 6.x Lambda DSL | 说明 |
|---|---|---|
| authorizeRequests() | authorizeHttpRequests() | 权限配置 |
| antMatchers() | requestMatchers() | 路径匹配 |
| permitAll() | permitAll() | 允许访问 |
| hasRole() | hasRole() | 角色判断(自动加 ROLE_) |
| hasAuthority() | hasAuthority() | 权限判断(不加前缀) |
| anyRequest().authenticated() | anyRequest().authenticated() | 其他请求需认证 |
| formLogin().loginPage() | formLogin(form -> form.loginPage()) | 登录页 |
| httpBasic() | httpBasic(basic -> {}) | HTTP Basic |
| csrf().disable() | csrf(csrf -> csrf.disable()) | 禁用 CSRF |
四、认证机制深度解析
4.1 AuthenticationManager 体系
4.2 认证流程源码解析
// org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 判断是否是 POST 请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationException("Authentication method not supported") {};
}
// 2. 获取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 3. 创建认证 Token
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 4. 设置详细信息
setDetails(request, authRequest);
// 5. 委托给 AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// org.springframework.security.authentication.ProviderManager
public class ProviderManager implements AuthenticationManager,
MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : providers) {
// 1. 检查是否支持该认证类型
if (!provider.supports(toTest)) {
continue;
}
try {
// 2. 执行认证
Authentication result = provider.authenticate(authentication);
// 3. 认证成功,复制其他属性
if (result != null) {
copyDetails(authentication, result);
eraseCredentials(result);
return result;
}
} catch (AccountStatusException e) {
// 账户状态异常
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
// 继续尝试其他 Provider
lastException = e;
}
}
// 4. 所有 Provider 都认证失败
if (lastException == null) {
lastException = new ProviderNotFoundException(noProvidersMessage);
}
parentAuthenticationFailed(result, authentication);
throw lastException;
}
}
4.3 自定义认证逻辑
4.3.1 自定义 UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 1. 查询用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
// 2. 构建权限列表
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
// 3. 返回 UserDetails
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(user.getLocked())
.credentialsExpired(false)
.disabled(!user.getEnabled())
.build();
}
}
4.3.2 自定义 AuthenticationProvider
@Component
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
// 1. 验证短信验证码
String phone = token.getName();
String smsCode = (String) token.getCredentials();
if (!validateSmsCode(phone, smsCode)) {
throw new BadCredentialsException("Invalid SMS code");
}
// 2. 加载用户信息
UserDetails user = userDetailsService.loadUserByUsername(phone);
// 3. 创建认证成功的 Token
SmsCodeAuthenticationToken result =
new SmsCodeAuthenticationToken(user, smsCode, user.getAuthorities());
result.setDetails(token.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
4.3.3 自定义认证过滤器
public class SmsCodeAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String PHONE_PARAMETER = "phone";
public static final String SMS_CODE_PARAMETER = "smsCode";
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
String phone = request.getParameter(PHONE_PARAMETER);
String smsCode = request.getParameter(SMS_CODE_PARAMETER);
if (phone == null || smsCode == null) {
throw new AuthenticationException("Phone or SMS code is null") {};
}
SmsCodeAuthenticationToken authRequest =
new SmsCodeAuthenticationToken(phone, smsCode);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
五、授权机制深度解析
5.1 权限模型
5.2 hasRole vs hasAuthority
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// hasRole: 自动添加 ROLE_ 前缀
// 用户需要 ROLE_ADMIN 权限才能访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// hasAuthority: 不添加前缀,使用精确权限名
.requestMatchers("/user/**").hasAuthority("USER_READ")
// hasAnyRole: 任意一个角色即可
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
// hasAnyAuthority: 任意一个权限即可
.requestMatchers("/report/**").hasAnyAuthority(
"REPORT_VIEW", "REPORT_EXPORT")
// 权限表达式
.requestMatchers("/api/data/**").access((context, object) -> {
Authentication auth = context.getAuthorizationManager()
.getAuthorizationManager();
return new AuthorizationDecision(
auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("DATA_ACCESS")));
})
.anyRequest().authenticated());
return http.build();
}
}
5.3 基于表达式的权限控制
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 使用 SpEL 表达式
.requestMatchers("/admin/**").access((authentication, object) -> {
Authentication auth = authentication.get();
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
// 检查 IP 地址
HttpServletRequest request = authentication.getRequest();
String ip = request.getRemoteAddr();
boolean isInternalIp = ip.startsWith("192.168.");
return new AuthorizationDecision(isAdmin && isInternalIp);
})
// 使用 hasRole 或其他标准规则
.requestMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated());
return http.build();
}
}
5.4 方法级安全注解
5.4.1 启用方法级安全
@Configuration
@EnableMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class MethodSecurityConfig {
// 配置类
}
5.4.2 使用安全注解
@Service
public class UserService {
// 需要 ADMIN 角色
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
// 需要特定权限
@Secured("ROLE_USER_READ")
public User getUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
// 使用 SpEL 表达式
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.userId or hasRole('ADMIN')")
public User updateUser(Long userId, UserUpdateRequest request) {
// 检查当前用户是否有权修改
return userRepository.findById(userId)
.map(user -> {
user.setEmail(request.getEmail());
return userRepository.save(user);
})
.orElseThrow(() -> new UserNotFoundException(userId));
}
// 使用 SpEL 复杂表达式
@PreAuthorize("#oauth2.clientAuthorities.contains('SCOPE_message:read')")
public Message getMessage(Long messageId) {
return messageRepository.findById(messageId)
.orElseThrow(() -> new MessageNotFoundException(messageId));
}
// 在方法执行后进行权限检查
@PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')")
public User getUserProfile(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
// 过滤输入参数
@PreFilter("filterObject.owner == authentication.principal.username")
public void deleteMessages(List<Message> messages) {
messageRepository.deleteAll(messages);
}
// 过滤返回结果
@PostFilter("filterObject.owner == authentication.principal.username")
public List<Message> getMyMessages() {
return messageRepository.findAll();
}
}
六、会话管理
6.1 会话创建策略
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 会话创建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// 并发会话控制
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // 踢掉之前登录
// 会话过期处理
.expiredUrl("/session-expired")
// 无效会话处理
.invalidSessionUrl("/session-invalid"));
return http.build();
}
}
SessionCreationPolicy 选项:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| ALWAYS | 总是创建会话 | 需要会话管理 |
| IF_REQUIRED | 按需创建(默认) | 标准 Web 应用 |
| NEVER | 不会主动创建,已有则使用 | API 服务 |
| STATELESS | 从不创建会话 | JWT、无状态 API |
6.2 会话固定攻击防护
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 会话 fixation 保护
.sessionFixation(sessionFixation -> sessionFixation
// 登录时创建新会话,旧会话数据迁移
.migrateSession()
// 或者使用 none/changeSessionId
// .none()
// .changeSessionId()
));
return http.build();
}
}
会话固定攻击防护选项:
| 选项 | 说明 | 安全性 |
|---|---|---|
| migrateSession | 登录后创建新会话,复制属性 | 推荐 |
| changeSessionId | 保持会话 ID,修改值 | 兼容性 |
| newSession | 创建全新会话 | 最高安全 |
6.3 集群会话管理
@Configuration
public class SessionConfig {
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(
FindByIndexNameSessionRepository<?> sessionRepository) {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
}
Redis 会话配置:
@Configuration
@EnableRedisHttpSession(
maxInactiveIntervalInSeconds = 3600, // 1小时
redisNamespace = "spring:session",
redisFlushMode = RedisFlushMode.ON_SAVE
)
public class RedisSessionConfig {
@Bean
public RedisIndexedSessionRepository sessionRepository(
RedisConnectionFactory connectionFactory) {
return new RedisIndexedSessionRepository(
new RedisOperationsSessionRepository(connectionFactory));
}
}
七、密码加密
7.1 PasswordEncoder 接口
public interface PasswordEncoder {
/**
* 加密密码
*/
String encode(CharSequence rawPassword);
/**
* 验证密码是否匹配
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* 判断是否需要重新编码
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
7.2 常用密码加密器对比
| 加密器 | 说明 | 安全性 | 性能 |
|---|---|---|---|
| NoOpPasswordEncoder | 不加密(仅测试) | ❌ | - |
| PlaintextPasswordEncoder | 明文存储 | ❌ | - |
| BCryptPasswordEncoder | BCrypt 算法 | ✅ | 较慢 |
| Argon2PasswordEncoder | Argon2 算法 | ✅✅ | 慢 |
| SCryptPasswordEncoder | SCrypt 算法 | ✅✅ | 慢 |
| Pbkdf2PasswordEncoder | PBKDF2 算法 | ✅✅ | 慢 |
7.3 密码编码配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 1. BCrypt(推荐)
return new BCryptPasswordEncoder();
// 2. Argon2(更安全,但慢)
return Argon2PasswordEncoder.defaultsForSpringSecurity_5_8();
// 3. 委托链(自动选择最优)
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
7.4 DelegatingPasswordEncoder
Spring Security 推荐使用委托密码编码器,它会自动处理不同格式的密码:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 生成的密码格式:{编码器ID}encodedPassword
// 例如:{bcrypt}$2a$10$...
// 例如:{sha256}XXXX
自定义委托编码器:
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(12));
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_5_8());
encoders.put("ldap", new org.springframework.security.crypto.password.
LdapPasswordEncoder());
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
八、CORS 跨域配置
8.1 全局 CORS 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 禁用 CSRF(API 场景)
.csrf(csrf -> csrf.disable())
// 2. 配置 CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(
List.of("http://localhost:3000", "https://*.example.com"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
8.2 跨域与安全配置结合
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CORS 配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 允许跨域请求携带认证信息
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated())
// 如果需要 Cookie,设置 SameSite
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestAttribute("_csrf"));
return http.build();
}
}
九、CSRF 防护
9.1 CSRF 攻击原理
9.2 CSRF 防护配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Cookie 保存 CSRF Token
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfRequestAttribute("_csrf"))
// 忽略特定路径
.ignoringRequestMatchers("/api/**", "/webhook/**");
return http.build();
}
}
9.3 前端 CSRF Token 处理
// 1. 从 Cookie 获取 CSRF Token(Spring Security 默认行为)
function getCsrfToken() {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'XSRF-TOKEN') {
return value;
}
}
return null;
}
// 2. 添加到请求头
function fetchWithCsrf(url, options = {}) {
const csrfToken = getCsrfToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'X-XSRF-TOKEN': csrfToken
}
});
}
// 3. Axios 配置
const axiosInstance = axios.create({
baseURL: '/api'
});
axiosInstance.interceptors.request.use(config => {
const csrfToken = getCsrfToken();
if (csrfToken) {
config.headers['X-XSRF-TOKEN'] = csrfToken;
}
return config;
});
十、安全响应头
10.1 常用安全响应头
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 安全响应头
.securityHeaders(headers -> headers
// 防止点击劫持
.frameOptions(frame -> frame.deny())
// XSS 防护
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline';"))
// 防止 MIME 类型嗅探
.contentTypeOptions(contentType -> {})
// Strict Transport Security
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000))
// 权限策略
.permissionsPolicy(policy -> policy
.addPolicyDirectives("geolocation=(), camera=(), microphone=()")));
return http.build();
}
}
10.2 常用安全头说明
| 响应头 | 说明 | 值示例 |
|---|---|---|
| X-Frame-Options | 防止页面被 iframe 嵌入 | DENY / SAMEORIGIN |
| X-Content-Type-Options | 防止 MIME 嗅探 | nosniff |
| X-XSS-Protection | XSS 过滤器(已废弃,依赖 CSP) | 1; mode=block |
| Strict-Transport-Security | 强制 HTTPS | max-age=31536000 |
| Content-Security-Policy | 内容安全策略 | default-src ‘self’ |
| Permissions-Policy | 权限策略 | camera=(), microphone=() |
| Referrer-Policy | 来源策略 | strict-origin-when-cross-origin |
10.3 Content Security Policy 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.securityHeaders(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives(
// 默认来源:仅同源
"default-src 'self' " +
// 脚本:同源 + 内联(开发环境)
"script-src 'self' 'unsafe-inline' " +
// 图片:同源 + CDN
"img-src 'self' https://cdn.example.com data: " +
// 样式:同源 + 内联
"style-src 'self' 'unsafe-inline' " +
// 连接:仅同源
"connect-src 'self' " +
// 字体:同源
"font-src 'self' " +
// frame 来源
"frame-src 'none' " +
// object 来源
"object-src 'none'"
)
));
return http.build();
}
}
十一、Remember-Me 功能
11.1 基于 Cookie 的 Remember-Me
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(rememberMe -> rememberMe
// Cookie 名
.rememberMeCookie("remember-me")
// Token 有效期(秒)
.tokenValiditySeconds(86400 * 14) // 14 天
// 记住我参数名
.rememberMeParameter("remember-me")
// 用户名参数(用于验证)
.userDetailsService(userDetailsService())
// Key 用于签名
.key("my-secret-key"));
return http.build();
}
}
11.2 基于持久化的 Remember-Me
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(rememberMe -> rememberMe
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400 * 14)
.userDetailsService(userDetailsService()));
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
// 首次使用自动创建表
// repository.setCreateTableOnStartup(true);
return repository;
}
}
持久化表结构:
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
十二、异常处理
12.1 认证异常处理
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(exceptions -> exceptions
// 认证失败处理
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(
"{\"error\": \"Unauthorized\", \"message\": \"" +
authException.getMessage() + "\"}");
})
// 授权失败处理
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write(
"{\"error\": \"Forbidden\", \"message\": \"" +
accessDeniedException.getMessage() + "\"}");
}));
return http.build();
}
}
12.2 自定义异常页面
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/error/**").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
// API 请求返回 JSON
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(
JSON.toJSONString(ApiResult.unauthorized(authException.getMessage())));
} else {
// 页面请求重定向到登录页
response.sendRedirect("/login?unauthorized=true");
}
}
}
十三、测试安全配置
13.1 使用 @WithMockUser 测试
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "USER")
void userCanAccessUserEndpoint() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
@Test
void anonymousCannotAccessProtectedEndpoint() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isUnauthorized());
}
}
13.2 使用 @WithUserDetails 测试
@SpringBootTest
@AutoConfigureMockMvc
class UserDetailsIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserDetailsService userDetailsService;
@Test
@WithUserDetails("testuser")
void userCanAccessOwnData() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails("admin")
void adminCanAccessAllData() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}
13.3 SecurityTest 注解
@SpringBootTest
@AutoConfigureMockMvc
class MethodSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser
void testMethodSecurity() throws Exception {
mockMvc.perform(get("/api/admin/method"))
.andExpect(status().isOk());
}
@Test
@WithSecurityContext(factory = CustomSecurityContextFactory.class)
void testCustomSecurityContext() throws Exception {
// 自定义安全上下文测试
mockMvc.perform(get("/api/special"))
.andExpect(status().isOk());
}
}
public class CustomSecurityContextFactory
implements WithSecurityContextFactory<WithSecurityContext> {
@Override
public SecurityContext createSecurityContext(WithSecurityContext annotation) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
CustomUserDetails user = new CustomUserDetails();
user.setUsername("custom-user");
user.addAuthority(new SimpleGrantedAuthority("CUSTOM_ROLE"));
Authentication auth = new UsernamePasswordAuthenticationToken(
user, "password", user.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
十四、总结
14.1 核心要点
- 从 WebSecurityConfigurerAdapter 迁移到 @Bean SecurityFilterChain
- 使用 Lambda DSL 进行配置(更简洁、更易读)
- authorizeHttpRequests 替代 authorizeRequests
- requestMatchers 替代 antMatchers
- hasRole() 自动添加 ROLE_ 前缀
14.2 配置检查清单
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 路径配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
// 2. 认证配置(表单/OAuth2/JWT)
.formLogin(form -> form
.loginPage("/login")
.permitAll())
// 3. 登出配置
.logout(logout -> logout
.logoutUrl("/logout"))
// 4. CSRF 配置
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**"))
// 5. CORS 配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 6. 会话管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
// 7. 异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler()));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
14.3 迁移检查表
| 检查项 | 说明 |
|---|---|
| WebSecurityConfigurerAdapter | 改用 @Bean SecurityFilterChain |
| configure(HttpSecurity) | 改用 Lambda DSL |
| antMatchers() | 改用 requestMatchers() |
| authorizeRequests() | 改用 authorizeHttpRequests() |
| hasRole(“ADMIN”) | 检查是否需要添加 ROLE_ 前缀 |
| csrf().disable() | 改用 csrf(csrf -> csrf.disable()) |
| httpBasic() | 改用 httpBasic(basic -> {}) |
| formLogin() | 改用 formLogin(form -> form.xxx) |
| PasswordEncoder | 使用 PasswordEncoderFactories 创建 |
本文深入解析了 Spring Security 6.x 的核心配置、Lambda DSL、认证授权机制、安全响应头等内容。结合实际项目需求,灵活运用这些配置,可以构建安全可靠的 Web 应用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)