Spring Security 防 CSRF/XSS/SQL注入——Security Headers 完整配置指南
Spring Security 防 CSRF/XSS/SQL注入——Security Headers 完整配置指南
适读人群:负责Web应用安全的Java后端工程师 | 阅读时长:约17分钟 | 核心价值:系统掌握Web常见漏洞的防御方案,以及Spring Security的安全响应头配置
一次渗透测试报告让我睡不好觉
2022年公司花钱做了一次渗透测试。报告出来,我看到了十几个高危和中危漏洞,其中有几个直接刺眼:
- XSS反射型注入:用户搜索框,输入
<script>alert(1)</script>弹了框 - CSRF:某个修改用户信息的接口没有CSRF保护,测试人员通过钓鱼页面成功修改了我的用户信息
- 点击劫持:将我们的页面嵌入iframe,再在上面覆盖透明按钮诱导用户点击
每一条都是实实在在的安全漏洞,不是纸上谈兵。
那次之后,我把Spring Security的安全头配置、输入验证、CSRF防护全部重新梳理了一遍。今天把整套防御体系讲清楚。
Spring Security 默认提供的安全响应头
Spring Security 默认会添加以下响应头(通过HeaderWriterFilter):
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Strict-Transport-Security: max-age=31536000 ; includeSubDomains但这些默认配置在某些场景下不够,需要扩展。
完整 Security Headers 配置
/**
* 完整的Spring Security安全头配置
* 防御:CSRF、XSS、点击劫持、MIME嗅探、信息泄漏等
*/
@Configuration
@EnableWebSecurity
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ===== 安全响应头配置 =====
.headers(headers -> headers
// 1. 禁止浏览器猜测Content-Type
.contentTypeOptions(Customizer.withDefaults())
// 2. 控制iframe嵌入:DENY=完全禁止,SAMEORIGIN=只允许同域
.frameOptions(frame -> frame.sameOrigin())
// 如果需要完全禁止:frame.deny()
// 3. HSTS:强制HTTPS(仅生产环境)
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000) // 1年
.includeSubDomains(true)
.preload(true)
)
// 4. CSP(Content Security Policy):最强大的XSS防御
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " + // 默认只允许同源
"script-src 'self' 'nonce-{random}'; " + // JS只允许同源或带nonce
"style-src 'self' 'unsafe-inline'; " + // CSS允许内联(部分框架需要)
"img-src 'self' data: https:; " + // 图片允许data URI和HTTPS
"font-src 'self'; " + // 字体只允许同源
"connect-src 'self' https://api.myapp.com; " + // 接口调用白名单
"frame-ancestors 'self'; " + // 禁止被外部iframe嵌入
"base-uri 'self'; " + // 限制<base>标签
"form-action 'self'" // 表单提交只允许同域
)
)
// 5. Referrer-Policy:控制Referer头信息
.referrerPolicy(referrer ->
referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
// 6. Permissions-Policy:限制浏览器特性
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=(), payment=()")
)
// 7. 缓存控制:防止敏感信息被缓存
.cacheControl(Customizer.withDefaults())
)
// ===== CSRF 配置 =====
// 前后端分离项目:如果用JWT,禁用CSRF(无状态不需要)
// .csrf(AbstractHttpConfigurer::disable)
// 传统Session项目:启用CSRF保护
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// CookieCsrfTokenRepository:Token存在Cookie里,JS可读取
// 前端需要读取Cookie中的XSRF-TOKEN,放到请求Header的X-XSRF-TOKEN
);
return http.build();
}
}CSRF 防御深度解析
什么是 CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造):攻击者诱导已登录用户访问恶意页面,该页面自动发起向受信任网站的请求(浏览器会自动携带Cookie),从而以用户身份执行恶意操作。
<!-- 攻击者的恶意页面 -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://mybank.com/transfer" method="POST">
<input type="hidden" name="toAccount" value="attacker-account"/>
<input type="hidden" name="amount" value="10000"/>
</form>
</body>
</html>用户访问这个页面,如果他之前登录了mybank.com且Cookie还有效,转账请求就会被执行。
Spring Security 的 CSRF 保护原理
Spring Security使用Synchronizer Token Pattern:
- 用户登录时,服务端生成一个随机CSRF Token
- 该Token通过Cookie或隐藏表单字段传给前端
- 前端在每次修改操作(POST/PUT/DELETE)时,把Token放在请求Header中
- 服务端验证请求中的Token与服务端记录的一致,才放行
攻击者的恶意页面无法读取用户在受信任网站的Cookie中的CSRF Token(同源策略限制),所以伪造的请求不携带Token,被拦截。
// Spring Security CSRF Token 的配置与使用
// 1. 配置Token仓库
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf
// 方式1:Cookie存储(适合前后端分离)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 方式2:Session存储(适合服务端渲染)
// .csrfTokenRepository(HttpSessionCsrfTokenRepository())
// 排除不需要CSRF保护的路径(如API接口、静态资源)
.ignoringRequestMatchers("/api/webhook/**") // Webhook来自外部
);
return http.build();
}// 前端 Axios 配置:自动携带 CSRF Token
function getCsrfToken() {
return document.cookie.split(';')
.find(row => row.trim().startsWith('XSRF-TOKEN='))
?.split('=')[1];
}
axios.interceptors.request.use(config => {
if (['post', 'put', 'delete', 'patch'].includes(config.method)) {
config.headers['X-XSRF-TOKEN'] = getCsrfToken();
}
return config;
});XSS 防御:输入验证 + 输出编码
输入验证
/**
* 全局XSS过滤器:对请求参数进行HTML编码
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XssFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(new XssHttpServletRequestWrapper(request), response);
}
// 包装请求,对参数进行XSS清洁
static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return cleanXss(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
return Arrays.stream(values).map(this::cleanXss).toArray(String[]::new);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return cleanXss(value);
}
private String cleanXss(String value) {
if (value == null) return null;
// 使用 OWASP Java HTML Sanitizer 进行清洁
// 或者使用 StringEscapeUtils.escapeHtml4
return value
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
.replace("javascript:", "")
.replace("vbscript:", "");
}
}
}更推荐使用OWASP AntiSamy或HTML Sanitizer做白名单过滤:
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.7.3</version>
</dependency>SQL 注入防御
最重要的原则:使用参数化查询
// 危险:字符串拼接SQL
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// 如果username = "' OR '1'='1",就是SQL注入
// 安全:参数化查询(MyBatis #{} 方式)
@Mapper
public interface UserMapper {
// #{username} 使用PreparedStatement参数绑定,防注入
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(String username);
// 危险:${username} 是字符串替换,不防注入
// @Select("SELECT * FROM users WHERE username = '${username}'")
}MyBatis 动态表名和ORDER BY 的安全处理
// 有时候需要动态指定ORDER BY列,但不能用#{}(会被加引号)
// 安全的做法:白名单验证
@Service
public class UserService {
private static final Set<String> ALLOWED_SORT_COLUMNS =
Set.of("id", "username", "created_at", "email");
public List<User> getUsers(String sortBy, String sortDir) {
// 白名单验证,防止ORDER BY注入
if (!ALLOWED_SORT_COLUMNS.contains(sortBy)) {
sortBy = "id"; // 非法列名,使用默认
}
if (!"asc".equalsIgnoreCase(sortDir) && !"desc".equalsIgnoreCase(sortDir)) {
sortDir = "asc";
}
return userMapper.selectUsers(sortBy, sortDir);
}
}三个踩坑实录
坑一:配置了 CSP 后,第三方组件全部失效
现象: 配置了严格的CSP之后,富文本编辑器(TinyMCE)、图表库(ECharts)、以及某些CDN引入的JS库全部无法加载。
原因: CSP的script-src 'self'只允许同源脚本,CDN上的JS被阻止了;style-src不允许unsafe-inline,某些组件的内联样式被阻止。
解法: CSP要逐步收紧,不要一开始就设最严格的。推荐使用CSP的"仅报告"模式先观察:
// 先用 Content-Security-Policy-Report-Only,不阻止只上报
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; report-uri /api/csp-report")
.reportOnly() // 仅报告,不拦截
)
// 观察一段时间,根据上报的违规调整策略,再改为真正的CSP坑二:移动端H5页面被第三方App内置WebView嵌套,导致CSRF Token失效
现象: 某功能在微信小程序的WebView中使用时,CSRF Token验证一直失败,报403。
原因: 微信WebView对Cookie有特殊处理,HttpOnly=false的Cookie在某些场景下无法被JS正常读取,导致前端拿不到CSRF Token。
解法: 对于H5嵌入App的场景,可以:
- 在URL中携带临时Token(而非Cookie)
- 后端提供专门的接口返回CSRF Token
- 对特定UserAgent的请求放宽CSRF验证(谨慎使用)
坑三:XSS 过滤把合法的 HTML 内容也给清理了
现象: 用户在富文本编辑器中输入了正常的格式化内容(如<b>加粗</b>),被XSS过滤器全部转义成实体字符,页面显示出了<b>加粗</b>原始文本。
原因: 全局XSS过滤器把所有<和>都做了转义,包括合法的HTML标签。
解法: 不能用全局替换,需要用HTML白名单清理:
- 允许的标签:
<b>,<i>,<strong>,<em>,<p>,<a href="...">等 - 禁止的标签:
<script>,<iframe>,<object>,<embed>等 - 禁止的属性:
onclick,onload,onerror,javascript:等
推荐使用OWASP Java HTML Sanitizer,它提供了预定义的安全策略:
PolicyFactory policy = Sanitizers.FORMATTING
.and(Sanitizers.LINKS)
.and(Sanitizers.BLOCKS);
String safeHtml = policy.sanitize(userInput);安全头检查工具
配置完安全头之后,推荐用以下工具验证:
- securityheaders.com:在线检测HTTP响应头
- OWASP ZAP:完整的Web应用安全扫描工具
- curl -I:快速查看响应头
curl -I https://yourapp.com/api/test
# 检查输出中是否有:
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY 或 SAMEORIGIN
# Strict-Transport-Security: max-age=...
# Content-Security-Policy: ...小结
Web安全的防御是分层的:
- 传输层:HTTPS + HSTS,防止中间人攻击
- 认证层:CSRF Token,防止跨站请求伪造
- 内容层:CSP + XSS过滤,防止脚本注入
- 数据层:参数化查询,防止SQL注入
- 信息层:安全响应头,减少信息泄漏
每一层都不是孤立的,需要协同防御。Spring Security提供了很好的基础设施,但需要正确配置和适当扩展,才能真正发挥防御作用。
