Web 安全是应用安全的基石。OWASP Top 10 中绝大多数漏洞与 Web 层密切相关。Spring Security 提供了丰富的安全功能,但默认配置往往不够——需要针对具体场景进行纵深防御配置。
1. 安全响应头(Security Headers)
1.1 核心响应头速查
| 响应头 | 用途 | 风险 |
|---|---|---|
Content-Security-Policy | 限制资源加载来源 | XSS、数据注入 |
X-Frame-Options | 防止页面被嵌入 iframe | Clickjacking |
X-Content-Type-Options | 禁止 MIME 嗅探 | 文件类型混淆攻击 |
Referrer-Policy | 控制 Referrer 信息 | 敏感 URL 泄露 |
Permissions-Policy | 限制浏览器 API | 摄像头/位置等隐私 |
Strict-Transport-Security | 强制 HTTPS | SSL Strip 攻击 |
1.2 Spring Security 配置
@Configuration
@EnableWebSecurity
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"img-src 'self' data: https://images.example.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"form-action 'self'; " +
"base-uri 'self';"
)
)
// 不再支持 iframe 嵌入
.frameOptions(frame -> frame.deny())
// 禁止 MIME 类型嗅探
.contentTypeOptions(contentType -> contentType.enable())
// Referrer 策略
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
// Permissions Policy
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=(), payment=()")
)
// HSTS
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000) // 1 year
.preload(true)
)
);
return http.build();
}
}
1.3 报告模式(Report-Only)
// 先使用 report-only 模式,观察 CSP 违规报告,确认无误后再强制启用
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; report-uri /csp-report")
.reportOnly() // 只报告,不拦截
)
);
1.5 XSS 防护与输入净化
跨站脚本攻击(XSS)是 OWASP Top 10 的常青树。Spring Security 提供了多层防护,但最根本的防御是输入净化 + 输出编码。
// 输入净化:使用 OWASP Java HTML Sanitizer
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
@Service
public class InputSanitizer {
private static final PolicyFactory POLICY = Sanitizers.FORMATTING
.and(Sanitizers.BLOCKS)
.and(Sanitizers.LINKS);
public String sanitize(String rawHtml) {
return POLICY.sanitize(rawHtml);
}
}
// 输出编码:Thymeleaf 自动转义(默认行为)
// <p th:text="${userInput}">安全:自动 HTML 实体编码</p>
// <p th:utext="${userInput}">危险:不编码直接输出</p>
XSS 防护检查清单:
| 层 | 措施 | 实现 |
|---|---|---|
| 输入 | 拒绝非法字符 | Regex 校验、白名单 |
| 存储 | 净化富文本 | OWASP HTML Sanitizer |
| 输出 | 上下文编码 | Thymeleaf/Handlebars 自动转义 |
| 传输 | Cookie 安全 | HttpOnly + Secure + SameSite |
| 浏览器 | CSP 策略 | script-src 'self' |
常见误区:只在入库前净化一次是不够的。攻击者可能通过 API 直接注入已净化的数据绕过前端校验。正确的做法是在输出时根据上下文编码(HTML/JS/CSS/URL 上下文各不相同)。
2. CSRF 防护
2.1 CSRF 原理
用户已登录 bank.com(Cookie 中有 session)
│
├── 访问 bank.com/transfer?to=attacker&amount=10000 ← 正常操作
│
└── 访问恶意网站 evil.com
└── 页面自动提交表单:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
→ 浏览器自动带上 bank.com 的 Cookie
→ 请求验证通过(因为 Cookie 是有效的)
2.2 Spring Security CSRF 配置
@Configuration
public class CsrfConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
// SPA/API 场景: 使用 Cookie 存储 Token
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 或: Header 存储(前后端分离)
// .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
// 忽略某些路径(如 Webhook)
.ignoringRequestMatchers("/webhook/**", "/api/public/**")
// 部分请求使用 CSRF
.requireCsrfProtectionMatcher(
new AndRequestMatcher(
CsrfFilter.DEFAULT_CSRF_MATCHER,
new NegatedRequestMatcher(
new AntPathRequestMatcher("/api/**", "GET")
)
)
)
);
return http.build();
}
}
2.3 前后端分离的 CSRF
// 后端: 启用 Cookie 传输 CSRF Token
@Bean
public CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookieName("XSRF-TOKEN");
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
// 前端 (Axios): 自动从 Cookie 读取并发送到 Header
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
// 更现代的做法: 如果 API 使用 JWT(无 Cookie Session),可以完全禁用 CSRF
// 因为 JWT 不会自动随请求发送,攻击者无法获取
@Configuration
@Profile("api-only")
public class ApiSecurityConfig {
@Bean
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // API 无 Cookie Session,无需 CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())));
return http.build();
}
}
2.5 JWT 安全:Token 生命周期与刷新策略
现代前后端分离应用广泛使用 JWT(JSON Web Token)进行认证。Spring Security OAuth2 Resource Server 提供了完整的 JWT 支持,但 Token 生命周期管理是关键。
@Configuration
public class JwtSecurityConfig {
// 自定义 JWT 解码器(添加时钟偏移容差)
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = AudienceValidator.of(audience);
OAuth2TokenValidator<Jwt> validator = OAuth2TokenValidator.and(withIssuer, withAudience);
decoder.setJwtValidator(validator);
return decoder;
}
// Token 刷新端点(双 Token 策略)
@RestController
@RequestMapping("/auth")
public class TokenController {
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(
@CookieValue(name = "refresh_token") String refreshToken
) {
// 1. 验证 Refresh Token(长寿命)
Jwt decoded = jwtDecoder.decode(refreshToken);
String userId = decoded.getSubject();
// 2. 检查 Token 是否在黑名单(注销场景)
if (tokenBlacklist.isBlacklisted(refreshToken)) {
return ResponseEntity.status(401).build();
}
// 3. 签发新 Access Token(短寿命,15 分钟)
String newAccessToken = jwtService.generateAccessToken(userId);
// 4. 轮换 Refresh Token(每次使用都更新)
String newRefreshToken = jwtService.generateRefreshToken(userId);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE,
ResponseCookie.from("refresh_token", newRefreshToken)
.httpOnly(true).secure(true).sameSite("Strict")
.maxAge(Duration.ofDays(7)).path("/auth/refresh").build().toString())
.body(new TokenResponse(newAccessToken));
}
}
}
Token 生命周期最佳实践:
| Token 类型 | 有效期 | 存储位置 | 用途 |
|---|---|---|---|
| Access Token | 5-15 分钟 | 内存(JS 变量) | API 认证 |
| Refresh Token | 7-30 天 | HttpOnly Cookie | 获取新 Access Token |
| ID Token | 同 Access Token | 内存 | 用户信息 |
安全原则:Refresh Token 必须支持轮换(每次使用即失效并签发新 Token)和撤销(用户主动注销或检测到异常时加入黑名单)。单一长寿命 Token 是重大安全隐患。
3. CORS 配置
3.1 CORS 原理
浏览器同源策略:
- 相同: protocol + host + port
- 不同源访问 → 浏览器拦截
CORS 流程:
1. 预检请求: OPTIONS /api/data
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header
2. 服务器响应:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 3600
3. 正式请求发送
3.2 Spring Security CORS 配置
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 明确指定允许的源,不要泛化为 *
configuration.setAllowedOrigins(Arrays.asList(
"https://app.example.com",
"https://admin.example.com"
));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList(
"Authorization", "Content-Type", "X-Requested-With", "X-Trace-Id"
));
configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Trace-Id"));
configuration.setAllowCredentials(true); // 允许 Cookie
configuration.setMaxAge(3600L); // 预检缓存 1 小时
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
}
CORS 安全红线:
- ❌ 不要
setAllowedOrigins(Arrays.asList("*"))+setAllowCredentials(true)同时设置 - ❌ 不要基于请求的
Origin动态反射(除非严格校验域名) - ✅ 使用明确的域名白名单
- ✅ 生产环境设置
Vary: Origin
4. XSS 防护
4.1 输入过滤与输出编码
@Component
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
XssHttpServletRequestWrapper wrappedRequest =
new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
}
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return value != null ? HtmlUtils.htmlEscape(value) : null;
}
}
4.2 富文本过滤
@Service
public class HtmlSanitizerService {
private final PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "u", "h1", "h2", "h3", "ul", "ol", "li", "a")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.toFactory();
public String sanitize(String html) {
return policy.sanitize(html);
}
}
4.3 Spring 自动转义
@Controller
public class CommentController {
@GetMapping("/comment")
public String comment(Model model) {
// Thymeleaf 自动转义: ${comment} → HTML 实体编码
// 仅明确需要时才使用 th:utext (unescaped text)
model.addAttribute("comment", userInput); // 自动安全
return "comment";
}
}
5. 会话安全
5.1 会话固定攻击防护
@Configuration
public class SessionSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation().migrateSession() // 登录后更换 Session ID
.maximumSessions(1) // 单点登录
.maxSessionsPreventsLogin(false) // 后登录者踢掉前者
.expiredUrl("/login?expired")
);
return http.build();
}
}
5.2 Cookie 安全属性
@Configuration
public class CookieConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSION");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setUseSecureCookie(true); // 仅 HTTPS 传输
serializer.setSameSite("Strict"); // SameSite 严格模式
return serializer;
}
}
6. 安全审计与监控
@Component
public class SecurityAuditListener {
private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
auditLog.info("LOGIN_SUCCESS|user={}|ip={}",
event.getAuthentication().getName(),
getClientIp());
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
auditLog.warn("LOGIN_FAILURE|user={}|reason={}|ip={}",
event.getAuthentication().getName(),
event.getException().getMessage(),
getClientIp());
}
}
纵深防御总结
Spring Security Web 安全的核心理念是纵深防御(Defense in Depth):不依赖单一防线,而是通过多层控制叠加实现安全冗余。
请求进入流程中的安全检查点:
┌─────────────┐
│ WAF/CDN │ DDoS 防护、恶意 IP 拦截
└──────┬──────┘
│
┌──────┴──────┐
│ Ingress │ TLS 终止、Rate Limiting
└──────┬──────┘
│
┌──────┴──────┐
│ Spring Sec │ 认证、授权、CSRF、CORS、安全头
└──────┬──────┘
│
┌──────┴──────┐
│ 应用代码 │ 输入校验、输出编码、业务权限
└──────┬──────┘
│
┌──────┴──────┐
│ 数据库 │ 参数化查询、最小权限账号
└─────────────┘
每一层被突破后,下一层仍然能提供保护。这才是生产级 Web 安全的正确心态。
延伸阅读
- Spring Boot 常用注解 — Spring Security 配置注解速查
- Java 企业级消息队列集成 — 异步安全事件处理
- DevSecOps 流水线实践 — 安全左移与 CI/CD 集成
- 信息安全与云安全 — 云原生安全专题总览
- 网络负载均衡 — WAF 与流量安全防护
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。