乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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写入任意自定义静态响应头
上一页
CORS 跨域防护
下一页
安全响应头