Clickjacking
点击劫持(Clickjacking)是一种通过将目标网站透明或伪装嵌入到恶意页面的 iframe 中,诱导用户在不知情的情况下点击目标网站按钮的攻击方式。Spring Security 通过 X-Frame-Options 响应头(由 XFrameOptionsHeaderWriter 写入)控制页面是否允许被嵌入 iframe,是防御 Clickjacking 的标准手段。
定义与作用
一句话定义:攻击者在恶意页面上使用 iframe 加载目标网站,通过 CSS 设置 iframe 透明或视觉欺骗,让用户以为自己点击的是恶意页面上的按钮,实际上点击的是目标网站(如转账确认、权限授予)上的按钮。
| 攻击要素 | 说明 |
|---|---|
| 加载方式 | <iframe src="https://target.com/action"> |
| 视觉欺骗 | CSS opacity: 0 或 z-index 叠加,将 iframe 覆盖在诱导按钮上方 |
| 用户状态 | 用户已在 target.com 登录,Cookie 自动携带 |
| 攻击结果 | 用户误触目标网站的真实操作按钮,执行非本意操作 |
Spring Security 的 XFrameOptionsHeaderWriter 由 HeaderWriterFilter 驱动,默认对所有响应写入 X-Frame-Options: DENY,告诉浏览器:该页面绝对不允许在任何 iframe 中被渲染。
攻击原理与防护流程
核心原理:X-Frame-Options 不是阻止服务器响应 iframe 请求,而是浏览器拿到响应后,检查该头部,如果值为 DENY,则完全不在任何 iframe / frame / object 中渲染该页面;如果值为 SAMEORIGIN,则只允许在同源父页面中渲染。由于渲染决策权在浏览器,攻击者的 iframe 无法显示目标页面内容,用户自然无法点击到目标按钮。
核心配置:X-Frame-Options 三种策略
Spring Security 5.x 的 http.headers().frameOptions() 提供三种策略:
| 策略 | DSL | 行为 | 适用场景 |
|---|---|---|---|
| DENY | .deny() | 拒绝任何 iframe 嵌入 | 绝大多数页面,尤其是含敏感操作的页面 |
| SAMEORIGIN | .sameOrigin() | 仅允许同源 iframe 嵌入 | 需要被同域子系统嵌入的场景(如内部平台) |
| 禁用 | .disable() | 不发送 X-Frame-Options | 完全不担心 Clickjacking 的特殊场景(极少) |
1. 全局 DENY(最严格,默认)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().deny() // 默认即 DENY,显式声明
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll();
}
}
2. SAMEORIGIN(允许同域嵌入)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin() // 允许 https://example.com 下的 iframe 嵌入 https://example.com/page
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
}
}
注意:SAMEORIGIN 只校验顶级域名和协议是否一致,不校验端口。如果同域下存在不可信子应用(如第三方插件运行在子路径),仍可能存在风险。
完整示例一:银行转账页面禁止一切 iframe 嵌入
场景说明
银行系统的转账确认页面 /transfer/confirm 需要最严格的 Clickjacking 防护。任何 iframe 嵌入都会导致用户可能在不知情的情况下点击转账确认按钮。同时,网银首页 /home 允许同域嵌入(因为银行内部有一个同域的 iframe 聚合门户)。
操作前配置(全局 SAMEORIGIN,转账页面风险高)
@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin() // ❌ 全局 SAMEORIGIN,/transfer/confirm 也能被同域嵌入
.and()
.authorizeRequests()
.antMatchers("/login", "/public/**").permitAll()
.antMatchers("/transfer/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
}
风险分析:攻击者无法通过跨域 iframe 嵌入,但如果银行同域下存在某个有 XSS 漏洞或可被控制的子页面(如 /public/legacy),该页面可通过同源 iframe 加载 /transfer/confirm,配合透明叠加技术仍然可以发起 Clickjacking。因此转账页面必须 DENY。
操作后配置(按路径差异化:转账页面 DENY,其他 SAMEORIGIN)
@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/public/**").permitAll()
.antMatchers("/transfer/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.headers()
// 默认策略:SAMEORIGIN
.frameOptions().sameOrigin()
.and()
// 对 /transfer/** 使用 DENY(覆盖默认)
.addHeaderWriter(new DelegatingRequestMatcherHeaderWriter(
new AntPathRequestMatcher("/transfer/**"),
new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY)
));
}
}
结果分析:访问 /home 时响应头为 X-Frame-Options: SAMEORIGIN,银行同域门户可以正常以 iframe 嵌入首页;访问 /transfer/confirm 时响应头为 X-Frame-Options: DENY,任何 iframe 请求该页面都被浏览器拒绝渲染。攻击者无法通过 iframe 加载转账页面,Clickjacking 攻击链路被彻底切断。
完整示例二:管理后台使用双重防护(X-Frame-Options + CSP)
场景说明
企业 ERP 管理后台(/admin/**)包含大量敏感操作按钮(删除用户、导出数据、修改权限)。除了 X-Frame-Options,还需要 CSP 的 frame-ancestors 提供第二重防护,以覆盖现代浏览器并支持更精细的域名控制。
操作前配置(仅 X-Frame-Options,无 CSP 补充)
@EnableWebSecurity
public class ErpSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().deny() // 单一层防护
.and()
.authorizeRequests()
.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.permitAll();
}
}
问题分析:X-Frame-Options 是旧标准,不支持指定具体域名列表。如果未来业务需求变为"仅允许 https://partner.example.com 嵌入 iframe",X-Frame-Options 无法实现(ALLOW-FROM 已被主流浏览器废弃)。此外,某些旧版浏览器或特殊环境(如企业内嵌浏览器)可能忽略 X-Frame-Options。
操作后配置(X-Frame-Options + CSP frame-ancestors 双重防护)
@EnableWebSecurity
public class ErpSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.permitAll()
.and()
.headers()
.frameOptions().deny()
.and()
.contentSecurityPolicy(
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"frame-ancestors 'none'; " + // 现代浏览器双重防护
"form-action 'self';")
.and()
.contentTypeOptions().nosniff()
.and()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(31536000);
}
}
结果分析:
- 旧浏览器(IE、旧版 Edge)识别
X-Frame-Options: DENY,拒绝 iframe 渲染; - 现代浏览器(Chrome、Firefox、Safari)识别
frame-ancestors 'none',拒绝 iframe 渲染; - 即使某一机制被绕过(如某些企业内嵌浏览器忽略旧头),另一机制仍然生效,形成纵深防御;
frame-ancestors未来如需改为允许特定合作方嵌入,可直接调整为frame-ancestors https://partner.example.com,而X-Frame-Options无法做到这一点。
易错场景:iframe 嵌入被阻止后,前端文件预览功能异常
面试高频考点:业务需要在前端页面中嵌入一个 iframe 预览 PDF 报告(如 /reports/annual.pdf),但 Spring Security 的 X-Frame-Options: DENY 导致 iframe 内显示空白,如何在不全局关闭防护的前提下解决?
错误做法:
.headers()
.frameOptions().disable(); // ❌ 全局关闭,所有页面都失去 Clickjacking 防护
问题分析:frameOptions().disable() 会完全移除 X-Frame-Options 头部,整个应用的所有页面都可以被任意 iframe 嵌入,攻击者可利用任何页面发起 Clickjacking。这是用全局安全降级换取局部功能,风险不可接受。
正确做法:仅对需要被嵌入的资源路径放宽策略,或改用 SAMEORIGIN 并确保嵌入父页面可信:
@EnableWebSecurity
public class ReportSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/reports/**").authenticated()
.anyRequest().authenticated()
.and()
.headers()
.frameOptions().deny() // 默认 DENY
.and()
// 仅 /reports/viewer 页面允许同域嵌入(该页面只有预览 UI,无操作按钮)
.addHeaderWriter(new DelegatingRequestMatcherHeaderWriter(
new AntPathRequestMatcher("/reports/viewer"),
new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)
));
}
}
更安全的替代方案:不直接嵌入后端返回的页面,而是使用前端原生组件预览 PDF(如 pdf.js 在 Canvas 中渲染),后端仅提供 /api/reports/annual.pdf 的 Blob 数据流。这样 iframe 完全不需要,从根本上消除 Clickjacking 面。
面试追问:X-Frame-Options 和 Content-Security-Policy: frame-ancestors 同时配置时,浏览器以哪个为准?
答:如果两者同时存在且策略冲突,现代浏览器优先遵循
frame-ancestors(CSP 指令),而忽略X-Frame-Options。例如X-Frame-Options: SAMEORIGIN但frame-ancestors 'none',浏览器最终执行frame-ancestors 'none'(拒绝所有嵌入)。如果只有X-Frame-Options存在,则按其规则执行。因此二者共存时,应确保策略一致,或让 CSP 策略更严格,避免矛盾配置导致困惑。
核心类速查
| 类 | 说明 |
|---|---|
HeaderWriterFilter | 写入安全响应头的过滤器 |
XFrameOptionsHeaderWriter | 写入 X-Frame-Options 的具体实现 |
XFrameOptionsHeaderWriter.XFrameOptionsMode | 枚举:DENY、SAMEORIGIN |
DelegatingRequestMatcherHeaderWriter | 按请求路径匹配,只对特定路径写入特定头部 |
ContentSecurityPolicyHeaderWriter | 写入 Content-Security-Policy(含 frame-ancestors) |
StaticHeadersWriter | 写入任意自定义静态响应头 |