1. 从一个真实案例说起:Referer 头引发的安全事件去年我在做一个后台管理系统的安全审计时,碰到一个挺典型的问题。系统本身做了完整的登录鉴权,接口层有 Token 校验,权限模型也做得比较细,按理说安全性应该没什么大问题。但渗透测试的同事给我发来一个截图,他在一个第三方论坛页面里嵌入了一个隐藏表单,用户只要在已登录状态下访问那个论坛页面,表单就会自动向我们的后台接口发起请求,结果竟然成功了——用户的账号被悄无声息地修改了邮箱。这就是典型的CSRF(跨站请求伪造)攻击,而防御它的第一道防线,往往就是Referer 头的校验。很多人对 Referer 的理解还停留在“统计来源”的层面,觉得它只是个分析工具,跟安全没什么关系。但实际上,在 Web 安全体系里,Referer 是服务端判断请求来源是否可信的重要依据之一。这篇文章我想把 SpringBoot 项目里基于 Referer 的跨站点攻击防护方案完整梳理一遍。从攻击原理、Referer 机制、SpringBoot 中的实现方式,到实际落地时会踩的坑,都会讲到。适合正在做 Web 后端开发、安全加固,或者对 CSRF 防护感兴趣的朋友参考。不管你是刚接触安全的新手,还是已经做过一些防护方案的老手,应该都能从中找到有用的东西。2. 跨站点攻击与 Referer 防护的核心原理拆解2.1 CSRF 攻击到底是怎么发生的要理解 Referer 防护的价值,得先把 CSRF 的攻击链路搞清楚。我用一个生活化的类比来解释:你去银行办业务,银行柜员认识你的脸(相当于 Cookie/Session),只要你在柜台前,他就默认是你本人。现在有个骗子伪造了一张跟你一模一样的脸(伪造请求),跑到柜台前让柜员转账,柜员一看脸对得上,就转了。在 Web 场景里,这个“脸”就是浏览器自动携带的 Cookie。攻击者不需要知道你的密码,只需要诱导你的浏览器向目标站点发起请求,浏览器会自动带上该站点的 Cookie,服务端一看 Cookie 有效,就认为是合法请求。整个攻击链路大致是这样的:用户登录了目标站点 A,浏览器保存了 A 的会话 Cookie用户在未退出登录的情况下,访问了恶意站点 B站点 B 的页面里藏了一个指向站点 A 的请求(可能是表单自动提交、图片加载、AJAX 请求等)浏览器向站点 A 发起请求时,自动带上了 A 的 Cookie站点 A 的服务端只校验了 Cookie,没有校验请求来源,请求被执行关键点在于:浏览器自动携带 Cookie 这个行为,是 CSRF 攻击能够成立的根本原因。而 Referer 防护的思路就是——既然请求是浏览器发出来的,那浏览器会告诉我这个请求是从哪个页面发起的,我只要检查这个来源是不是我信任的页面就行了。2.2 Referer 头的工作机制与取值规则Referer 是 HTTP 协议中的一个请求头字段,用来标识当前请求是从哪个页面链接过来的。注意它的拼写是 Referer 而不是 Referrer,这是 HTTP 标准制定时的一个历史拼写错误,后来将错就错保留了下来。它的取值规则大致如下:请求场景Referer 值从 HTTPS 页面跳转到 HTTP 页面不发送 Referer从 HTTP 页面跳转到 HTTPS 页面发送完整 Referer同源页面之间的跳转发送完整 Referer从浏览器地址栏直接输入 URL不发送 Referer通过书签访问不发送 Referer页面内加载图片、脚本等资源发送完整 Referer使用rel="noreferrer"的链接不发送 Referer设置了Referrer-Policy为no-referrer不发送 Referer这里有个很重要的细节:Referer 并不是浏览器一定会发送的。用户可以通过浏览器设置、插件、隐私模式等方式屏蔽 Referer,页面也可以通过Referrer-Policy响应头或<meta>标签来控制 Referer 的发送策略。这就引出了一个核心矛盾:如果我把 Referer 校验做成强制的,那所有不带 Referer 的合法请求都会被拦截,用户体验会受影响;如果我不校验 Referer,那 CSRF 防护就形同虚设。所以实际落地时,需要根据业务场景做权衡。2.3 为什么选择 Referer 而不是其他方案CSRF 防护有好几种常见方案,我简单对比一下:防护方案原理优点缺点Referer 校验检查请求来源实现简单,对业务侵入小Referer 可能被屏蔽,不够可靠CSRF Token服务端下发随机 Token,请求时携带安全性高,不依赖浏览器行为需要改造前端,对 API 不友好SameSite Cookie限制 Cookie 跨站发送浏览器原生支持,配置简单兼容性受浏览器版本影响自定义请求头要求请求携带特定头简单有效需要前端配合Referer 校验的优势在于实现成本低、对现有业务侵入小。尤其是对于一些老系统改造,或者前后端已经分离但不想大动干戈的项目,加一层 Referer 校验过滤器就能挡住大部分 CSRF 攻击。当然,它不能作为唯一的防护手段,通常会和 CSRF Token、SameSite Cookie 配合使用,形成多层防护。3. SpringBoot 中 Referer 校验的完整实现方案3.1 整体设计思路与技术选型在 SpringBoot 里做 Referer 校验,最直接的方式是写一个Filter或者Interceptor。我倾向于用 Filter,原因是 Filter 在请求进入 Spring MVC 的 DispatcherServlet 之前就会执行,拦截时机更早,覆盖面更广,连静态资源请求都能管到。整体设计思路是这样的:定义一个可配置的白名单域名列表,只有来自这些域名的请求才被认为是合法的写一个 Filter,在请求到达 Controller 之前检查 Referer 头如果 Referer 为空,根据配置决定是放行还是拦截如果 Referer 不为空,解析出域名部分,和白名单比对校验不通过则返回 403 状态码,并记录日志这里有个关键决策:Referer 为空时怎么处理。我的建议是分场景处理——对于 API 接口,可以要求必须带 Referer;对于页面请求,可以适当放宽。但更稳妥的做法是,Referer 为空时直接拦截,因为正常的浏览器请求在跨站场景下一定会带 Referer,不带 Referer 的请求要么是用户直接输入 URL(这种场景下 CSRF 攻击不成立),要么是攻击者刻意去掉了 Referer。不过实际项目中,很多团队会选择“Referer 为空时放行”,因为担心误伤正常用户。这个取舍需要根据业务的安全等级来定。金融类、后台管理类系统建议严格校验,普通资讯类站点可以适当放宽。3.2 核心代码实现与配置详解先定义一个配置类,用来读取白名单域名:@ConfigurationProperties(prefix = "security.referer") public class RefererProperties { private boolean enabled = true; private boolean allowEmpty = false; private List<String> allowedDomains = new ArrayList<>(); private List<String> excludePaths = new ArrayList<>(); // getter 和 setter 省略 }然后在application.yml里配置:security: referer: enabled: true allow-empty: false allowed-domains: - "example.com" - "admin.example.com" - "localhost:8080" exclude-paths: - "/api/public/**" - "/health" - "/actuator/**"接下来是 Filter 的核心实现:@Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) public class RefererCheckFilter implements Filter { private static final Logger log = LoggerFactory.getLogger(RefererCheckFilter.class); private final RefererProperties properties; private final AntPathMatcher pathMatcher = new AntPathMatcher(); public RefererCheckFilter(RefererProperties properties) { this.properties = properties; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; if (!properties.isEnabled()) { chain.doFilter(request, response); return; } String requestPath = httpRequest.getRequestURI(); // 排除路径直接放行 for (String excludePath : properties.getExcludePaths()) { if (pathMatcher.match(excludePath, requestPath)) { chain.doFilter(request, response); return; } } String referer = httpRequest.getHeader("Referer"); if (referer == null || referer.isEmpty()) { if (properties.isAllowEmpty()) { chain.doFilter(request, response); return; } log.warn("请求缺少 Referer 头,已拦截。URI: {}, IP: {}", requestPath, getClientIp(httpRequest)); rejectRequest(httpResponse); return; } if (!isAllowedReferer(referer)) { log.warn("Referer 校验失败,已拦截。Referer: {}, URI: {}, IP: {}", referer, requestPath, getClientIp(httpRequest)); rejectRequest(httpResponse); return; } chain.doFilter(request, response); } private boolean isAllowedReferer(String referer) { try { URI uri = new URI(referer); String host = uri.getHost(); if (host == null) { return false; } int port = uri.getPort(); String hostWithPort = port > 0 ? host + ":" + port : host; for (String allowed : properties.getAllowedDomains()) { if (hostWithPort.equalsIgnoreCase(allowed) || host.equalsIgnoreCase(allowed)) { return true; } } return false; } catch (URISyntaxException e) { log.warn("Referer 格式解析失败: {}", referer); return false; } } private void rejectRequest(HttpServletResponse response) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"请求来源不合法\"}"); } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty()) { ip = request.getRemoteAddr(); } return ip; } }这段代码有几个细节值得展开说:关于@Order注解:我把 Filter 的优先级设得比较高,确保它在其他业务 Filter 之前执行。如果你的项目里有多个 Filter,需要根据实际执行顺序调整这个值。关于域名匹配:这里我用了hostWithPort和host双重比对,是为了兼容带端口和不带端口的场景。实际项目中,如果你的服务部署在非标准端口上,Referer 里会带上端口号,需要特别注意。关于路径排除:excludePaths用来放行一些不需要校验的路径,比如健康检查、公开 API 等。用AntPathMatcher做通配符匹配,支持/**这种写法。3.3 域名匹配的边界情况处理域名匹配看起来简单,实际上有不少坑。我列几个实际遇到过的情况:子域名问题:如果白名单里配了example.com,那admin.example.com算不算合法?从安全角度,子域名应该单独配置,不能自动放行。因为如果某个子域名被攻击者控制(比如存在 XSS 漏洞),攻击者就能从那个子域名发起请求。所以我的实现里是精确匹配,不做子域名通配。端口问题:example.com和example.com:8080是两个不同的来源。Referer 里如果带了端口,必须精确匹配。有些开发者会忽略端口,导致校验被绕过。协议问题:http://example.com和https://example.com在 Referer 里是不同的。严格来说,应该校验协议,确保请求来自 HTTPS 页面。但实际项目中,很多系统同时支持 HTTP 和 HTTPS,这时候可以只校验域名部分。大小写问题:域名是不区分大小写的,但 URL 路径区分。所以比对域名时要用equalsIgnoreCase。国际化域名:如果域名包含中文等非 ASCII 字符,Referer 里会是 Punycode 编码。这种情况需要先解码再比对,或者直接配置 Punycode 形式的域名。提示:域名匹配建议做成可配置的,不要硬编码在代码里。不同环境(开发、测试、生产)的域名不一样,硬编码会导致部署时频繁改代码。4. 实际落地中的常见问题与排查技巧4.1 Referer 校验失败的典型场景在实际项目中,Referer 校验失败的原因五花八门。我整理了一个速查表,方便排查:现象可能原因排查方法解决方案所有请求都被拦截白名单配置错误检查配置文件中的域名修正白名单部分请求被拦截请求来自未配置的子域名查看日志中的 Referer 值添加对应域名移动端请求被拦截App 内 WebView 不发送 Referer抓包查看请求头对 App 请求单独处理反向代理后全部拦截代理未透传 Referer检查 Nginx 配置添加 Referer 透传HTTPS 跳 HTTP 被拦截浏览器不发送 Referer查看浏览器控制台统一使用 HTTPS前端本地开发被拦截localhost 未加入白名单检查 Referer 值添加 localhost我重点说几个容易踩的坑:反向代理场景:这是最常见的坑。请求经过 Nginx 或其他反向代理后,如果代理没有正确透传 Referer 头,后端拿到的 Referer 就是空的。解决方法是检查 Nginx 配置,确保proxy_set_header Referer $http_referer;这行配置存在。有些代理默认会去掉 Referer,需要显式配置保留。移动端 App 场景:App 内的 WebView 加载 H5 页面时,Referer 的行为和普通浏览器不一样。有些 App 会自定义 Referer,有些干脆不发送。如果你们的 H5 页面需要调用后端接口,而 App 又没配好 Referer,就会导致接口全部 403。这种情况需要和客户端同学协商,要么让 App 统一设置 Referer,要么把 App 的请求来源加入白名单。前端本地开发场景:开发环境前端跑在localhost:3000,后端跑在localhost:8080,跨域请求时 Referer 是localhost:3000。如果白名单里只配了localhost:8080,开发环境就会全部被拦截。建议在开发环境的配置里把localhost的各种端口都加上,或者干脆在开发环境关闭 Referer 校验。4.2 性能与安全的平衡策略Referer 校验本身性能开销很小,就是一次字符串解析和比对,对接口响应时间的影响基本可以忽略。但有几个地方需要注意:日志量控制:如果攻击者大量发起不带 Referer 的请求,你的日志会被刷爆。建议对拦截日志做限流,比如同一个 IP 每分钟只记录一次,或者用采样记录。缓存白名单:如果白名单是从数据库或配置中心动态加载的,建议加本地缓存,避免每次请求都去查。可以用@Scheduled定时刷新,或者用配置中心的推送机制。异步校验:对于高并发场景,可以把 Referer 校验做成异步的,不阻塞主请求流程。但这样会带来一个问题——校验还没完成,请求可能已经被处理了。所以异步校验只适合做审计,不适合做拦截。分层防护:Referer 校验只是第一层,不要把它当成唯一防线。建议配合以下措施:SameSite Cookie:在 Cookie 上设置SameSite=Lax或SameSite=Strict,从浏览器层面限制跨站 Cookie 发送CSRF Token:对敏感操作(如转账、改密码)强制要求携带 CSRF Token二次验证:对高风险操作要求输入密码或短信验证码CORS 配置:严格限制跨域请求的来源,不要用*通配符4.3 绕过 Referer 校验的攻击手法与应对Referer 校验不是万能的,有几种绕过手法需要了解:空 Referer 绕过:攻击者可以通过rel="noreferrer"、Referrer-Policy: no-referrer、或者直接用file://协议发起请求,让 Referer 为空。如果你的配置是allowEmpty=true,那就被绕过了。应对方法是尽量设置allowEmpty=false,或者对空 Referer 的请求做额外校验(比如要求携带自定义头)。域名伪造绕过:攻击者可以注册一个包含你域名的子域名,比如example.com.attacker.com。如果你的匹配逻辑写得不严谨,用contains而不是equals,就会被绕过。应对方法是严格用equals或endsWith(".example.com")这种精确匹配。Referer 篡改:虽然浏览器不允许前端 JS 修改 Referer,但攻击者可以用自己的工具构造任意 Referer 的请求。所以 Referer 校验只能防住浏览器发起的 CSRF,防不住直接构造的 HTTP 请求。对于后者,需要靠 Token 校验来防护。降级攻击:从 HTTPS 页面跳转到 HTTP 页面时,浏览器不发送 Referer。如果攻击者能诱导用户从 HTTPS 页面发起请求,就能让 Referer 为空。应对方法是全站 HTTPS,并配置 HSTS。注意:Referer 校验的核心价值是提高攻击成本,而不是做到绝对安全。任何单一防护措施都可能被绕过,多层防护才是正道。5. 进阶优化与生产环境实践建议5.1 动态白名单与配置热更新生产环境中,白名单域名可能会变化,比如新增了子域名、换了 CDN 域名等。如果每次都要改配置重启服务,运维成本太高。我推荐用配置中心(如 Nacos、Apollo)来管理白名单,支持热更新。实现思路是:把RefererProperties注册为配置中心的监听器,当配置变化时自动刷新。Spring Cloud 提供了@RefreshScope注解,可以很方便地实现这一点:@Component @RefreshScope @ConfigurationProperties(prefix = "security.referer") public class RefererProperties { // 字段定义同上 }然后在配置中心里修改security.referer.allowed-domains的值,服务会自动感知变化,不需要重启。这个方案我在多个项目中用过,实测下来很稳。如果不想引入配置中心,也可以用数据库存储白名单,配合定时任务刷新。但要注意加缓存,避免每次请求都查库。5.2 监控告警与日志分析Referer 校验的拦截日志是安全监控的重要数据源。建议做以下几件事:结构化日志:把拦截日志打成 JSON 格式,方便后续分析。关键字段包括:时间戳、客户端 IP、请求 URI、Referer 值、User-Agent、拦截原因。实时告警:当拦截量突增时触发告警。比如 5 分钟内拦截次数超过 100 次,就发通知给安全团队。这通常意味着有人在扫描或攻击。趋势分析:定期分析拦截日志,看看哪些 Referer 被拦截得最多。如果发现某个合法域名频繁被拦截,可能是白名单配置漏了;如果发现大量随机 Referer,可能是攻击行为。IP 画像:对频繁触发拦截的 IP 做画像分析,如果某个 IP 在短时间内尝试了大量不同的 Referer,基本可以判定为攻击源,可以考虑加入黑名单。我在实际项目中用 ELK 做日志收集和分析,配合 Grafana 做可视化,效果不错。拦截日志按天索引,保留 30 天,既能满足审计需求,又不会占用太多存储。5.3 与其他安全机制的协同配合Referer 校验不是孤立的,它需要和其他安全机制配合才能发挥最大价值。我梳理了一个协同方案:与 Spring Security 集成:如果你用了 Spring Security,可以把 Referer 校验做成一个自定义的SecurityFilterChain,和认证、授权逻辑统一管理。这样 Filter 的执行顺序、异常处理都能统一控制。与 CORS 配置配合:CORS 管的是跨域请求的响应头,Referer 校验管的是请求来源。两者配合使用,可以形成完整的跨域安全防护。注意 CORS 的allowedOrigins和 Referer 白名单要保持一致,避免出现配置冲突。与 WAF 联动:如果有 WAF(Web 应用防火墙),可以把 Referer 校验的规则同步到 WAF 上,在流量入口层就拦截掉恶意请求,减轻后端压力。WAF 的规则通常比代码里的校验更灵活,支持正则匹配、频率限制等高级功能。与风控系统对接:对于金融类业务,可以把 Referer 校验的结果作为风控系统的一个输入因子。如果某个请求的 Referer 异常,即使通过了校验,也可以触发风控系统的二次验证。最后分享一个我在实际项目中总结的经验:Referer 校验的配置一定要分环境管理。开发环境可以宽松一些,方便调试;测试环境要和生产环境保持一致,避免上线后才发现问题;生产环境要严格,但也要留好应急开关,万一白名单配错了,能快速关闭校验,避免影响业务。这个方案我在几个项目中落地过,整体效果不错。最关键的体会是:安全防护没有银弹,Referer 校验只是其中一环。真正要做好安全,需要从架构设计、编码规范、运维监控等多个层面一起发力。希望这些经验对正在做安全加固的朋友有所帮助。
1. 从一个真实案例说起:Referer 头引发的安全事件去年我在做一个后台管理系统的安全审计时,碰到一个挺典型的问题。系统本身做了完整的登录鉴权,接口层有 Token 校验,权限模型也做得比较细,按理说安全性应该没什么大问题。但渗透测试的同事给我发来一个截图,他在一个第三方论坛页面里嵌入了一个隐藏表单,用户只要在已登录状态下访问那个论坛页面,表单就会自动向我们的后台接口发起请求,结果竟然成功了——用户的账号被悄无声息地修改了邮箱。这就是典型的CSRF(跨站请求伪造)攻击,而防御它的第一道防线,往往就是Referer 头的校验。很多人对 Referer 的理解还停留在“统计来源”的层面,觉得它只是个分析工具,跟安全没什么关系。但实际上,在 Web 安全体系里,Referer 是服务端判断请求来源是否可信的重要依据之一。这篇文章我想把 SpringBoot 项目里基于 Referer 的跨站点攻击防护方案完整梳理一遍。从攻击原理、Referer 机制、SpringBoot 中的实现方式,到实际落地时会踩的坑,都会讲到。适合正在做 Web 后端开发、安全加固,或者对 CSRF 防护感兴趣的朋友参考。不管你是刚接触安全的新手,还是已经做过一些防护方案的老手,应该都能从中找到有用的东西。2. 跨站点攻击与 Referer 防护的核心原理拆解2.1 CSRF 攻击到底是怎么发生的要理解 Referer 防护的价值,得先把 CSRF 的攻击链路搞清楚。我用一个生活化的类比来解释:你去银行办业务,银行柜员认识你的脸(相当于 Cookie/Session),只要你在柜台前,他就默认是你本人。现在有个骗子伪造了一张跟你一模一样的脸(伪造请求),跑到柜台前让柜员转账,柜员一看脸对得上,就转了。在 Web 场景里,这个“脸”就是浏览器自动携带的 Cookie。攻击者不需要知道你的密码,只需要诱导你的浏览器向目标站点发起请求,浏览器会自动带上该站点的 Cookie,服务端一看 Cookie 有效,就认为是合法请求。整个攻击链路大致是这样的:用户登录了目标站点 A,浏览器保存了 A 的会话 Cookie用户在未退出登录的情况下,访问了恶意站点 B站点 B 的页面里藏了一个指向站点 A 的请求(可能是表单自动提交、图片加载、AJAX 请求等)浏览器向站点 A 发起请求时,自动带上了 A 的 Cookie站点 A 的服务端只校验了 Cookie,没有校验请求来源,请求被执行关键点在于:浏览器自动携带 Cookie 这个行为,是 CSRF 攻击能够成立的根本原因。而 Referer 防护的思路就是——既然请求是浏览器发出来的,那浏览器会告诉我这个请求是从哪个页面发起的,我只要检查这个来源是不是我信任的页面就行了。2.2 Referer 头的工作机制与取值规则Referer 是 HTTP 协议中的一个请求头字段,用来标识当前请求是从哪个页面链接过来的。注意它的拼写是 Referer 而不是 Referrer,这是 HTTP 标准制定时的一个历史拼写错误,后来将错就错保留了下来。它的取值规则大致如下:请求场景Referer 值从 HTTPS 页面跳转到 HTTP 页面不发送 Referer从 HTTP 页面跳转到 HTTPS 页面发送完整 Referer同源页面之间的跳转发送完整 Referer从浏览器地址栏直接输入 URL不发送 Referer通过书签访问不发送 Referer页面内加载图片、脚本等资源发送完整 Referer使用rel="noreferrer"的链接不发送 Referer设置了Referrer-Policy为no-referrer不发送 Referer这里有个很重要的细节:Referer 并不是浏览器一定会发送的。用户可以通过浏览器设置、插件、隐私模式等方式屏蔽 Referer,页面也可以通过Referrer-Policy响应头或<meta>标签来控制 Referer 的发送策略。这就引出了一个核心矛盾:如果我把 Referer 校验做成强制的,那所有不带 Referer 的合法请求都会被拦截,用户体验会受影响;如果我不校验 Referer,那 CSRF 防护就形同虚设。所以实际落地时,需要根据业务场景做权衡。2.3 为什么选择 Referer 而不是其他方案CSRF 防护有好几种常见方案,我简单对比一下:防护方案原理优点缺点Referer 校验检查请求来源实现简单,对业务侵入小Referer 可能被屏蔽,不够可靠CSRF Token服务端下发随机 Token,请求时携带安全性高,不依赖浏览器行为需要改造前端,对 API 不友好SameSite Cookie限制 Cookie 跨站发送浏览器原生支持,配置简单兼容性受浏览器版本影响自定义请求头要求请求携带特定头简单有效需要前端配合Referer 校验的优势在于实现成本低、对现有业务侵入小。尤其是对于一些老系统改造,或者前后端已经分离但不想大动干戈的项目,加一层 Referer 校验过滤器就能挡住大部分 CSRF 攻击。当然,它不能作为唯一的防护手段,通常会和 CSRF Token、SameSite Cookie 配合使用,形成多层防护。3. SpringBoot 中 Referer 校验的完整实现方案3.1 整体设计思路与技术选型在 SpringBoot 里做 Referer 校验,最直接的方式是写一个Filter或者Interceptor。我倾向于用 Filter,原因是 Filter 在请求进入 Spring MVC 的 DispatcherServlet 之前就会执行,拦截时机更早,覆盖面更广,连静态资源请求都能管到。整体设计思路是这样的:定义一个可配置的白名单域名列表,只有来自这些域名的请求才被认为是合法的写一个 Filter,在请求到达 Controller 之前检查 Referer 头如果 Referer 为空,根据配置决定是放行还是拦截如果 Referer 不为空,解析出域名部分,和白名单比对校验不通过则返回 403 状态码,并记录日志这里有个关键决策:Referer 为空时怎么处理。我的建议是分场景处理——对于 API 接口,可以要求必须带 Referer;对于页面请求,可以适当放宽。但更稳妥的做法是,Referer 为空时直接拦截,因为正常的浏览器请求在跨站场景下一定会带 Referer,不带 Referer 的请求要么是用户直接输入 URL(这种场景下 CSRF 攻击不成立),要么是攻击者刻意去掉了 Referer。不过实际项目中,很多团队会选择“Referer 为空时放行”,因为担心误伤正常用户。这个取舍需要根据业务的安全等级来定。金融类、后台管理类系统建议严格校验,普通资讯类站点可以适当放宽。3.2 核心代码实现与配置详解先定义一个配置类,用来读取白名单域名:@ConfigurationProperties(prefix = "security.referer") public class RefererProperties { private boolean enabled = true; private boolean allowEmpty = false; private List<String> allowedDomains = new ArrayList<>(); private List<String> excludePaths = new ArrayList<>(); // getter 和 setter 省略 }然后在application.yml里配置:security: referer: enabled: true allow-empty: false allowed-domains: - "example.com" - "admin.example.com" - "localhost:8080" exclude-paths: - "/api/public/**" - "/health" - "/actuator/**"接下来是 Filter 的核心实现:@Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) public class RefererCheckFilter implements Filter { private static final Logger log = LoggerFactory.getLogger(RefererCheckFilter.class); private final RefererProperties properties; private final AntPathMatcher pathMatcher = new AntPathMatcher(); public RefererCheckFilter(RefererProperties properties) { this.properties = properties; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; if (!properties.isEnabled()) { chain.doFilter(request, response); return; } String requestPath = httpRequest.getRequestURI(); // 排除路径直接放行 for (String excludePath : properties.getExcludePaths()) { if (pathMatcher.match(excludePath, requestPath)) { chain.doFilter(request, response); return; } } String referer = httpRequest.getHeader("Referer"); if (referer == null || referer.isEmpty()) { if (properties.isAllowEmpty()) { chain.doFilter(request, response); return; } log.warn("请求缺少 Referer 头,已拦截。URI: {}, IP: {}", requestPath, getClientIp(httpRequest)); rejectRequest(httpResponse); return; } if (!isAllowedReferer(referer)) { log.warn("Referer 校验失败,已拦截。Referer: {}, URI: {}, IP: {}", referer, requestPath, getClientIp(httpRequest)); rejectRequest(httpResponse); return; } chain.doFilter(request, response); } private boolean isAllowedReferer(String referer) { try { URI uri = new URI(referer); String host = uri.getHost(); if (host == null) { return false; } int port = uri.getPort(); String hostWithPort = port > 0 ? host + ":" + port : host; for (String allowed : properties.getAllowedDomains()) { if (hostWithPort.equalsIgnoreCase(allowed) || host.equalsIgnoreCase(allowed)) { return true; } } return false; } catch (URISyntaxException e) { log.warn("Referer 格式解析失败: {}", referer); return false; } } private void rejectRequest(HttpServletResponse response) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"请求来源不合法\"}"); } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty()) { ip = request.getRemoteAddr(); } return ip; } }这段代码有几个细节值得展开说:关于@Order注解:我把 Filter 的优先级设得比较高,确保它在其他业务 Filter 之前执行。如果你的项目里有多个 Filter,需要根据实际执行顺序调整这个值。关于域名匹配:这里我用了hostWithPort和host双重比对,是为了兼容带端口和不带端口的场景。实际项目中,如果你的服务部署在非标准端口上,Referer 里会带上端口号,需要特别注意。关于路径排除:excludePaths用来放行一些不需要校验的路径,比如健康检查、公开 API 等。用AntPathMatcher做通配符匹配,支持/**这种写法。3.3 域名匹配的边界情况处理域名匹配看起来简单,实际上有不少坑。我列几个实际遇到过的情况:子域名问题:如果白名单里配了example.com,那admin.example.com算不算合法?从安全角度,子域名应该单独配置,不能自动放行。因为如果某个子域名被攻击者控制(比如存在 XSS 漏洞),攻击者就能从那个子域名发起请求。所以我的实现里是精确匹配,不做子域名通配。端口问题:example.com和example.com:8080是两个不同的来源。Referer 里如果带了端口,必须精确匹配。有些开发者会忽略端口,导致校验被绕过。协议问题:http://example.com和https://example.com在 Referer 里是不同的。严格来说,应该校验协议,确保请求来自 HTTPS 页面。但实际项目中,很多系统同时支持 HTTP 和 HTTPS,这时候可以只校验域名部分。大小写问题:域名是不区分大小写的,但 URL 路径区分。所以比对域名时要用equalsIgnoreCase。国际化域名:如果域名包含中文等非 ASCII 字符,Referer 里会是 Punycode 编码。这种情况需要先解码再比对,或者直接配置 Punycode 形式的域名。提示:域名匹配建议做成可配置的,不要硬编码在代码里。不同环境(开发、测试、生产)的域名不一样,硬编码会导致部署时频繁改代码。4. 实际落地中的常见问题与排查技巧4.1 Referer 校验失败的典型场景在实际项目中,Referer 校验失败的原因五花八门。我整理了一个速查表,方便排查:现象可能原因排查方法解决方案所有请求都被拦截白名单配置错误检查配置文件中的域名修正白名单部分请求被拦截请求来自未配置的子域名查看日志中的 Referer 值添加对应域名移动端请求被拦截App 内 WebView 不发送 Referer抓包查看请求头对 App 请求单独处理反向代理后全部拦截代理未透传 Referer检查 Nginx 配置添加 Referer 透传HTTPS 跳 HTTP 被拦截浏览器不发送 Referer查看浏览器控制台统一使用 HTTPS前端本地开发被拦截localhost 未加入白名单检查 Referer 值添加 localhost我重点说几个容易踩的坑:反向代理场景:这是最常见的坑。请求经过 Nginx 或其他反向代理后,如果代理没有正确透传 Referer 头,后端拿到的 Referer 就是空的。解决方法是检查 Nginx 配置,确保proxy_set_header Referer $http_referer;这行配置存在。有些代理默认会去掉 Referer,需要显式配置保留。移动端 App 场景:App 内的 WebView 加载 H5 页面时,Referer 的行为和普通浏览器不一样。有些 App 会自定义 Referer,有些干脆不发送。如果你们的 H5 页面需要调用后端接口,而 App 又没配好 Referer,就会导致接口全部 403。这种情况需要和客户端同学协商,要么让 App 统一设置 Referer,要么把 App 的请求来源加入白名单。前端本地开发场景:开发环境前端跑在localhost:3000,后端跑在localhost:8080,跨域请求时 Referer 是localhost:3000。如果白名单里只配了localhost:8080,开发环境就会全部被拦截。建议在开发环境的配置里把localhost的各种端口都加上,或者干脆在开发环境关闭 Referer 校验。4.2 性能与安全的平衡策略Referer 校验本身性能开销很小,就是一次字符串解析和比对,对接口响应时间的影响基本可以忽略。但有几个地方需要注意:日志量控制:如果攻击者大量发起不带 Referer 的请求,你的日志会被刷爆。建议对拦截日志做限流,比如同一个 IP 每分钟只记录一次,或者用采样记录。缓存白名单:如果白名单是从数据库或配置中心动态加载的,建议加本地缓存,避免每次请求都去查。可以用@Scheduled定时刷新,或者用配置中心的推送机制。异步校验:对于高并发场景,可以把 Referer 校验做成异步的,不阻塞主请求流程。但这样会带来一个问题——校验还没完成,请求可能已经被处理了。所以异步校验只适合做审计,不适合做拦截。分层防护:Referer 校验只是第一层,不要把它当成唯一防线。建议配合以下措施:SameSite Cookie:在 Cookie 上设置SameSite=Lax或SameSite=Strict,从浏览器层面限制跨站 Cookie 发送CSRF Token:对敏感操作(如转账、改密码)强制要求携带 CSRF Token二次验证:对高风险操作要求输入密码或短信验证码CORS 配置:严格限制跨域请求的来源,不要用*通配符4.3 绕过 Referer 校验的攻击手法与应对Referer 校验不是万能的,有几种绕过手法需要了解:空 Referer 绕过:攻击者可以通过rel="noreferrer"、Referrer-Policy: no-referrer、或者直接用file://协议发起请求,让 Referer 为空。如果你的配置是allowEmpty=true,那就被绕过了。应对方法是尽量设置allowEmpty=false,或者对空 Referer 的请求做额外校验(比如要求携带自定义头)。域名伪造绕过:攻击者可以注册一个包含你域名的子域名,比如example.com.attacker.com。如果你的匹配逻辑写得不严谨,用contains而不是equals,就会被绕过。应对方法是严格用equals或endsWith(".example.com")这种精确匹配。Referer 篡改:虽然浏览器不允许前端 JS 修改 Referer,但攻击者可以用自己的工具构造任意 Referer 的请求。所以 Referer 校验只能防住浏览器发起的 CSRF,防不住直接构造的 HTTP 请求。对于后者,需要靠 Token 校验来防护。降级攻击:从 HTTPS 页面跳转到 HTTP 页面时,浏览器不发送 Referer。如果攻击者能诱导用户从 HTTPS 页面发起请求,就能让 Referer 为空。应对方法是全站 HTTPS,并配置 HSTS。注意:Referer 校验的核心价值是提高攻击成本,而不是做到绝对安全。任何单一防护措施都可能被绕过,多层防护才是正道。5. 进阶优化与生产环境实践建议5.1 动态白名单与配置热更新生产环境中,白名单域名可能会变化,比如新增了子域名、换了 CDN 域名等。如果每次都要改配置重启服务,运维成本太高。我推荐用配置中心(如 Nacos、Apollo)来管理白名单,支持热更新。实现思路是:把RefererProperties注册为配置中心的监听器,当配置变化时自动刷新。Spring Cloud 提供了@RefreshScope注解,可以很方便地实现这一点:@Component @RefreshScope @ConfigurationProperties(prefix = "security.referer") public class RefererProperties { // 字段定义同上 }然后在配置中心里修改security.referer.allowed-domains的值,服务会自动感知变化,不需要重启。这个方案我在多个项目中用过,实测下来很稳。如果不想引入配置中心,也可以用数据库存储白名单,配合定时任务刷新。但要注意加缓存,避免每次请求都查库。5.2 监控告警与日志分析Referer 校验的拦截日志是安全监控的重要数据源。建议做以下几件事:结构化日志:把拦截日志打成 JSON 格式,方便后续分析。关键字段包括:时间戳、客户端 IP、请求 URI、Referer 值、User-Agent、拦截原因。实时告警:当拦截量突增时触发告警。比如 5 分钟内拦截次数超过 100 次,就发通知给安全团队。这通常意味着有人在扫描或攻击。趋势分析:定期分析拦截日志,看看哪些 Referer 被拦截得最多。如果发现某个合法域名频繁被拦截,可能是白名单配置漏了;如果发现大量随机 Referer,可能是攻击行为。IP 画像:对频繁触发拦截的 IP 做画像分析,如果某个 IP 在短时间内尝试了大量不同的 Referer,基本可以判定为攻击源,可以考虑加入黑名单。我在实际项目中用 ELK 做日志收集和分析,配合 Grafana 做可视化,效果不错。拦截日志按天索引,保留 30 天,既能满足审计需求,又不会占用太多存储。5.3 与其他安全机制的协同配合Referer 校验不是孤立的,它需要和其他安全机制配合才能发挥最大价值。我梳理了一个协同方案:与 Spring Security 集成:如果你用了 Spring Security,可以把 Referer 校验做成一个自定义的SecurityFilterChain,和认证、授权逻辑统一管理。这样 Filter 的执行顺序、异常处理都能统一控制。与 CORS 配置配合:CORS 管的是跨域请求的响应头,Referer 校验管的是请求来源。两者配合使用,可以形成完整的跨域安全防护。注意 CORS 的allowedOrigins和 Referer 白名单要保持一致,避免出现配置冲突。与 WAF 联动:如果有 WAF(Web 应用防火墙),可以把 Referer 校验的规则同步到 WAF 上,在流量入口层就拦截掉恶意请求,减轻后端压力。WAF 的规则通常比代码里的校验更灵活,支持正则匹配、频率限制等高级功能。与风控系统对接:对于金融类业务,可以把 Referer 校验的结果作为风控系统的一个输入因子。如果某个请求的 Referer 异常,即使通过了校验,也可以触发风控系统的二次验证。最后分享一个我在实际项目中总结的经验:Referer 校验的配置一定要分环境管理。开发环境可以宽松一些,方便调试;测试环境要和生产环境保持一致,避免上线后才发现问题;生产环境要严格,但也要留好应急开关,万一白名单配错了,能快速关闭校验,避免影响业务。这个方案我在几个项目中落地过,整体效果不错。最关键的体会是:安全防护没有银弹,Referer 校验只是其中一环。真正要做好安全,需要从架构设计、编码规范、运维监控等多个层面一起发力。希望这些经验对正在做安全加固的朋友有所帮助。