作为 Java 开发者,在前后端分离架构盛行的今天,跨域问题和跨站攻击几乎是绕不开的坎。本文将从概念本质、攻击场景到 Java 生态下的具体解决方案,全方位解析 CORS 和 CSRF,帮你彻底理清这两个容易混淆的安全概念。
一、CORS:跨域资源共享的 “通行证” 机制
1. 什么是 CORS?
CORS(Cross-Origin Resource Sharing,跨域资源共享)是浏览器的一种安全策略,用于限制一个域的网页如何访问另一个域的资源。当前端页面与后端接口不在同一个域名(或端口、协议)时,就会触发跨域检查。
举个例子:
前端页面部署在 http://localhost:3000
,而后端接口部署在 http://localhost:8080
,当前端通过 AJAX 请求后端接口时,浏览器会先进行跨域检查,若后端未正确配置 CORS,请求会被拦截并抛出类似错误:
Access to XMLHttpRequest at 'http://localhost:8080/api' from origin 'http://localhost:3000' has been blocked by CORS policy
2. 为什么会有 CORS?
浏览器的 “同源策略”(Same-Origin Policy)是 Web 安全的基础,它限制不同源的文档之间互相访问资源。同源需满足三个条件:协议相同(http/https)、域名相同、端口相同。
但前后端分离架构中,前后端通常部署在不同域名 / 端口,完全禁止跨域请求会导致业务无法开展。CORS 应运而生,它允许服务器通过特定响应头,明确告知浏览器 “允许哪些域的请求访问我的资源”,是同源策略的 “灵活例外机制”。
3. CORS 的工作原理:简单请求与预检请求
浏览器将跨域请求分为两类,处理方式不同:
(1)简单请求
同时满足以下条件的请求为简单请求,直接发送,无需预先检查:
- 请求方法为 GET、POST、HEAD
- 请求头仅包含 Accept、Accept-Language、Content-Language、Content-Type(仅限 application/x-www-form-urlencoded、multipart/form-data、text/plain)
流程:
前端发送请求时携带 Origin
头(包含当前域),后端响应中返回 Access-Control-Allow-Origin
头,浏览器检查该头是否包含前端的 Origin,若包含则允许访问,否则拦截。
(2)预检请求(Preflight)
不符合简单请求条件的请求(如 PUT、DELETE 方法,或自定义请求头),浏览器会先发送一个 OPTIONS 方法的 “预检请求”,验证服务器是否允许该跨域请求。
流程:
- 浏览器发送 OPTIONS 请求,携带
Origin
(请求域)、Access-Control-Request-Method
(实际请求方法)、Access-Control-Request-Headers
(实际请求头); - 服务器响应中返回 CORS 相关头(如允许的域、方法、头、预检有效期等);
- 浏览器验证通过后,才发送实际的请求;否则拦截。
4. Java 中的 CORS 解决方案
Java 后端处理 CORS 主要通过配置响应头实现,不同框架有不同的实现方式,以下是最常用的方案:
(1)Spring Boot 全局 CORS 配置
通过 WebMvcConfigurer
配置全局跨域规则,推荐在生产环境使用,避免重复配置:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 对哪些接口生效
.allowedOrigins("https://example.com", "http://localhost:3000") // 允许的源(生产环境避免使用*)
.allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的方法
.allowedHeaders("*") // 允许的请求头
.exposedHeaders("Authorization") // 允许前端获取的响应头
.allowCredentials(true) // 是否允许携带Cookie
.maxAge(3600); // 预检请求有效期(秒),有效期内不重复发送预检
}
}
(2)局部 CORS 配置:@CrossOrigin 注解
对单个控制器或方法配置跨域,优先级高于全局配置:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
// 控制器级别配置:允许example.com域访问该控制器下所有接口
@CrossOrigin(origins = "https://example.com", allowCredentials = "true")
public class UserController {
// 方法级别配置:覆盖控制器配置,仅允许localhost:3000访问该方法
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/api/user")
public String getUser() {
return "user info";
}
}
(3)Spring Security 中的 CORS 配置
若项目集成了 Spring Security,需单独配置 CORS,否则安全框架会拦截 OPTIONS 请求:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // 配置CORS
.csrf(csrf -> csrf.disable()); // 临时关闭CSRF(下文会讲)
return http.build();
}
// 定义CORS配置源
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
(4)注意事项
- 生产环境禁止使用
allowedOrigins("*")
,尤其是当allowCredentials=true
时(浏览器不允许*
与 credentials 同时存在),需指定具体域名; - 若前端需要读取自定义响应头(如
Authorization
),需通过exposedHeaders
配置; - 预检请求(OPTIONS)不会携带 Cookie 和 Token,后端不要对其进行身份验证。
二、CSRF:跨站请求伪造的 “钓鱼” 攻击
1. 什么是 CSRF?
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式:攻击者诱导用户在已登录的情况下,访问一个恶意网站,该网站会利用用户的身份凭证(如 Cookie),以用户名义向目标网站发送非预期请求。
举个攻击场景:
- 你登录了银行网站
https://bank.com
,浏览器保存了银行的登录 Cookie; - 你在未退出银行网站的情况下,点击了一个恶意链接
https://evil.com
; - 恶意网站自动向
https://bank.com/transfer
发送请求(携带你已登录的 Cookie),内容为 “向攻击者转账 1000 元”; - 银行网站验证 Cookie 有效,执行转账操作,攻击者得逞。
2. CSRF 攻击的前提条件
- 用户已登录目标网站,且 Cookie 未过期(攻击者利用 “身份凭证”);
- 目标网站的接口仅通过 Cookie 验证身份,未对请求来源进行校验;
- 攻击者能构造出符合目标网站接口要求的请求(如参数、路径正确)。
3. CSRF 与 CORS 的区别
很多开发者会混淆这两个概念,核心区别如下:
维度 | CORS | CSRF |
---|---|---|
本质 | 浏览器的跨域访问控制机制 | 一种利用用户身份的攻击方式 |
目的 | 解决 “合法跨域请求被拦截” 问题 | 防止 “恶意跨站请求被执行” |
作用对象 | 浏览器对跨域请求的拦截规则 | 服务器对请求合法性的校验 |
典型错误 | 控制台显示 CORS 相关错误 | 无明显错误,但执行了非预期操作 |
4. Java 中的 CSRF 防护方案
CSRF 防护的核心思路是:让服务器能区分请求是用户主动发起的,还是恶意网站伪造的。以下是 Java 生态中的常用方案:
(1)Spring Security 内置的 CSRF 防护
Spring Security 提供了成熟的 CSRF 防护机制,默认开启(对非 GET、HEAD、OPTIONS、TRACE 方法生效),原理是 “Token 验证”:
- 服务器生成一个随机的 CSRF Token,存储在 session 中,并通过响应返回给前端;
- 前端发送请求时,必须携带该 Token(表单隐藏字段或请求头);
- 服务器验证请求中的 Token 与 session 中的是否一致,不一致则拒绝请求。
配置方式:
Spring Security 5.7+ 推荐使用 lambda 表达式配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // Token存储在Cookie,前端可读取
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}
前端如何携带 Token:
- 表单提交:在表单中添加隐藏字段
_csrf
,值为服务器返回的 Token; - AJAX 请求:从 Cookie 中读取 Token(
XSRF-TOKEN
),通过请求头X-XSRF-TOKEN
发送。
示例(React 前端):
// 从Cookie获取CSRF Token(需引入js-cookie库)
import Cookies from 'js-cookie';
const csrfToken = Cookies.get('XSRF-TOKEN');
// 发送POST请求
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken // 携带Token
},
body: JSON.stringify({ to: 'attacker', amount: 1000 })
});
(2)验证 Referer/Origin 头
通过检查请求头中的 Referer
(请求来源页面)或 Origin
(请求来源域),判断请求是否来自可信域名。
实现方式:自定义拦截器
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
public class CsrfInterceptor implements HandlerInterceptor {
// 可信域名列表
private static final List<String> TRUSTED_ORIGINS = Arrays.asList(
"https://example.com",
"http://localhost:3000"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 非修改类请求(如GET)可跳过验证
String method = request.getMethod();
if ("GET".equals(method) || "HEAD".equals(method)) {
return true;
}
// 检查Origin头
String origin = request.getHeader("Origin");
if (origin != null && !TRUSTED_ORIGINS.contains(origin)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid Origin");
return false;
}
// 检查Referer头(作为Origin的补充,部分浏览器可能不发送Origin)
String referer = request.getHeader("Referer");
if (referer != null && !TRUSTED_ORIGINS.stream().anyMatch(referer::startsWith)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid Referer");
return false;
}
return true;
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CsrfInterceptor())
.addPathPatterns("/api/**"); // 对所有API生效
}
}
(3)使用 SameSite Cookie 属性
设置 Cookie 的 SameSite
属性,限制 Cookie 仅在同站请求中携带,从源头阻止跨站请求使用 Cookie。
配置方式:在 Spring Boot 中通过配置文件设置
# application.yml
server:
servlet:
session:
cookie:
same-site: Lax # 或 Strict
SameSite=Lax
:允许部分跨站请求携带 Cookie(如 GET 方法的链接跳转),安全性与可用性平衡;SameSite=Strict
:完全禁止跨站请求携带 Cookie,安全性最高,但可能影响正常功能(如第三方登录回调)。
5. 不需要 CSRF 防护的场景
- 纯后端接口(无前端页面),且通过 Token(如 JWT)在请求头验证身份(不依赖 Cookie);
- 仅允许 GET 方法的查询接口(GET 方法应无副作用,符合 REST 规范);
- 内部系统,且严格限制访问来源。
三、实战中的常见问题与最佳实践
1. 同时处理 CORS 和 CSRF 的注意事项
- 若启用 CSRF 防护,CORS 配置中
allowedOrigins
不能设为*
(需指定具体域名),否则 Token 验证可能失效; - 前端同时携带 Token(如 JWT)和 CSRF Token 时,需确保两者都能正确传递;
- Spring Security 中,CORS 配置需在 CSRF 配置之前,否则可能出现拦截顺序问题。
2. 排查 CORS 问题的步骤
- 查看浏览器控制台的错误信息,确认是简单请求还是预检请求失败;
- 检查响应头中是否包含
Access-Control-Allow-Origin
且值正确; - 若涉及 credentials,确保
Access-Control-Allow-Credentials
为true
,且allowedOrigins
未使用*
; - 若为预检请求失败,检查后端是否正确处理 OPTIONS 方法(Spring Security 需允许 OPTIONS 匿名访问)。
3. 排查 CSRF 问题的步骤
- 若请求被拒绝并返回 403 Forbidden,检查是否携带了 CSRF Token;
- 确认前端获取的 Token 与服务器 session 中的 Token 一致;
- 检查请求方法是否为非 GET 方法(CSRF 防护默认对这些方法生效);
- 若使用 SameSite Cookie,确认属性值是否与业务场景匹配。
四、总结
CORS 和 CSRF 虽都涉及 “跨域”,但本质完全不同:
- CORS 是浏览器的安全策略,解决 “合法跨域请求被拦截” 的问题,需通过后端配置响应头放行;
- CSRF 是一种攻击方式,目标是 “盗用用户身份执行恶意操作”,需通过 Token 验证、来源检查等方式防护。
作为 Java 开发者,掌握 Spring Boot 和 Spring Security 中的相关配置是关键。在实际开发中,应根据业务场景(如是否使用 Cookie、是否有跨域需求)选择合适的方案,既保证安全性,又不影响用户体验。
最后记住:安全没有银弹,需结合多种手段(如 HTTPS、输入验证、权限控制)构建全方位的防护体系。