安全配置进阶
本教程共 48 篇 · 第 35 篇 · 更新于 2026-08-13 · 约 5 分钟阅读
本节目标:掌握 SecurityFilterChain 的多链与授权表达式,理解 CSRF 和 CORS 的取舍,学会给 Actuator 端点配置安全规则。
一条请求的安检之旅
SecurityFilterChain 本质是一条过滤器链。请求进来,按顺序过安检:
- 认证过滤器:解析凭证,建立身份
- 授权检查:比对身份和访问规则
- 异常处理:认证失败跳登录,授权失败返回 403
- 最后才到你的控制器
HttpSecurity 就是配置这条链的 DSL。每调一个方法,就多装一个「安检设备」。
上一节的配置能跑,但真实项目往往同时有页面和 API,规则要拆开。下面逐个进阶。
多链配置:不同路径,不同规则
一个应用可能同时有 /api/**(无状态 API)和页面(表单登录)。用多个 SecurityFilterChain Bean 加 securityMatcher 分流:
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class MultiChainConfig {
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}
@Order 数字小的先匹配。securityMatcher 不匹配的链直接跳过,落到下一条。
Warning链的顺序写反是高频事故。API 链不写
securityMatcher时,它会吞掉所有请求,页面链永远不生效。
权限表达式
authorizeHttpRequests 里支持 SpEL 表达式:
| 表达式 | 含义 |
|---|---|
permitAll() | 完全放行 |
authenticated() | 登录即可 |
hasRole("ADMIN") | 有 ROLE_ADMIN 角色 |
hasAuthority("user:read") | 有指定权限 |
hasAnyRole("ADMIN", "USER") | 任一角色 |
access("hasRole('ADMIN') and ipAddress('10.0.0.0/8')") | 组合条件 |
hasRole 和 hasAuthority 的区别:hasRole("ADMIN") 自动补 ROLE_ 前缀,hasAuthority("ADMIN") 按原样匹配。
两种风格对应两种权限模型:
- 角色模型:粗粒度,
ROLE_ADMIN、ROLE_USER,适合中小项目 - 权限模型:细粒度,
user:read、order:delete,适合权限点多的系统
方法级同样能用表达式:
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public void update(Long id) { }
#id 引用方法参数,authentication.principal 取当前用户。这是面试高频考点。
CSRF:表单的守护,API 的包袱
CSRF(跨站请求伪造):你登录着银行,黑客网页偷偷发一个转账 POST。Cookie 自动带上,银行以为是你。
Spring Security 默认开启 CSRF 防护:表单里注入隐藏 token,服务端校验。浏览器表单场景必须保留。
纯 API 场景没有 Cookie,CSRF 失去意义,可以关掉:
http.csrf(csrf -> csrf.disable());
判断标准很简单:客户端是不是浏览器?浏览器页面里发起的请求,CSRF 防护别关。
Warning只对「非浏览器客户端」的服务关 CSRF。开着 CSRF 时,用 POST 调 actuator 的 shutdown、loggers 端点会收到 403,这是防护在起作用,不是故障。
CORS:跨域放行
前后端分离时,浏览器跨域请求需要服务端声明允许。Security 的 CORS 配置:
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://myapp.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
然后在 HttpSecurity 上启用:
http.cors(withDefaults());
浏览器发复杂请求前会先发一个 OPTIONS 预检,问服务端「允不允许这么跨」。上面的配置回答了这个问题,预检通过才发正式请求。
Note
allowedOrigins别写*还开allowCredentials(true),浏览器会拒绝这种组合。精确列出前端域名更安全。
也可以直接在控制器或 MVC 配置里写@CrossOrigin,但全局规则统一放 Security 更清晰。
与 Actuator 集成
有了自定义 SecurityFilterChain,Actuator 端点也归你管。用 EndpointRequest 精确匹配端点路径:
package com.example.demo.config;
import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ENDPOINT_ADMIN"))
.httpBasic(withDefaults());
return http.build();
}
}
这样 /actuator/** 全部要求 ENDPOINT_ADMIN 角色。静态资源用 PathRequest.toStaticResources().atCommonLocations() 放行:
http.authorizeHttpRequests(auth -> auth
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
);
EndpointRequest.to("health") 只匹配指定端点,toAnyEndpoint() 匹配全部。端点路径换了 base-path 也能正确匹配,因为它读的是 management.endpoints.web.base-path 配置。
Note只要存在任何
SecurityFilterChainBean,Spring Boot 对 Actuator 的默认安全规则就退出,端点安全完全由你的配置决定。
未配置时,Boot 4.x 默认只暴露/health,其余端点不可见。
记住我:免登录的折中
会话一断,用户就要重新登录。rememberMe 是折中方案:登录成功后签发一个签名 Cookie,有效期内重新访问自动恢复登录态。
http.rememberMe(withDefaults());
登录表单加一个勾选框:
<input type="checkbox" name="remember-me"/>
安全提醒:记住我 Cookie 相当于长期凭证,泄露了就能冒充登录。敏感系统建议不开,或缩短有效期。
自定义认证逻辑
想加验证码校验、对接第三方账号体系,实现 AuthenticationProvider:
package com.example.demo.security;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DemoAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if ("admin".equals(name) && "admin123".equals(password)) {
return new UsernamePasswordAuthenticationToken(name, password,
List.of(new SimpleGrantedAuthority("ROLE_ADMIN")));
}
return null; // 交给下一个 Provider 处理
}
@Override
public boolean supports(Class<?> authenticationType) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
}
}
认证失败返回 null 表示「我不处理」,Spring Security 会尝试下一个 Provider。定义了 AuthenticationProvider 或 AuthenticationManager Bean 后,默认的 UserDetailsService 自动配置退出。
安全响应头
SecurityFilterChain 还会自动加响应头:X-Content-Type-Options、Cache-Control、HSTS(HTTPS 时)等。
frameOptions 默认 DENY,防止点击劫持。业务上确实需要被 iframe 嵌入时再放开:
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
这些头是免费的纵深防御,别随手关。只有明确冲突(比如要嵌入 iframe)才调整。
初学者常栽的坑
- 多链顺序反了:API 链没写
securityMatcher,吞掉所有请求 hasRole写权限名:hasRole("user:read")会补成ROLE_user:read,永远匹配不上- CSRF 忘了关:纯 API 项目表单登录页能进,POST 接口全是 403
- CORS 配置了但接口还是跨域报错:检查
.cors(withDefaults())有没有写,两处缺一不可 - 自定义 Provider 返回
null却以为认证失败:null是「不处理」,要抛BadCredentialsException才是明确拒绝
本节小结
- SecurityFilterChain 是过滤器链,
@Order+securityMatcher做多链分流 - 权限表达式区分
hasRole与hasAuthority,可组合access(...) - CSRF 默认开启,浏览器场景保留,纯 API 可关
- CORS 用
CorsConfigurationSourceBean +.cors(withDefaults()) - Actuator 端点用
EndpointRequest纳入安全规则