深入理解 CORS 与 CSRF:Java 开发者必备的跨域安全指南

作为 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 方法的 “预检请求”,验证服务器是否允许该跨域请求。

流程

  1. 浏览器发送 OPTIONS 请求,携带 Origin(请求域)、Access-Control-Request-Method(实际请求方法)、Access-Control-Request-Headers(实际请求头);
  2. 服务器响应中返回 CORS 相关头(如允许的域、方法、头、预检有效期等);
  3. 浏览器验证通过后,才发送实际的请求;否则拦截。

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),以用户名义向目标网站发送非预期请求。

举个攻击场景

  1. 你登录了银行网站 https://bank.com,浏览器保存了银行的登录 Cookie;
  2. 你在未退出银行网站的情况下,点击了一个恶意链接 https://evil.com
  3. 恶意网站自动向 https://bank.com/transfer 发送请求(携带你已登录的 Cookie),内容为 “向攻击者转账 1000 元”;
  4. 银行网站验证 Cookie 有效,执行转账操作,攻击者得逞。

2. CSRF 攻击的前提条件

  • 用户已登录目标网站,且 Cookie 未过期(攻击者利用 “身份凭证”);
  • 目标网站的接口仅通过 Cookie 验证身份,未对请求来源进行校验;
  • 攻击者能构造出符合目标网站接口要求的请求(如参数、路径正确)。

3. CSRF 与 CORS 的区别

很多开发者会混淆这两个概念,核心区别如下:

维度CORSCSRF
本质浏览器的跨域访问控制机制一种利用用户身份的攻击方式
目的解决 “合法跨域请求被拦截” 问题防止 “恶意跨站请求被执行”
作用对象浏览器对跨域请求的拦截规则服务器对请求合法性的校验
典型错误控制台显示 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 问题的步骤

  1. 查看浏览器控制台的错误信息,确认是简单请求还是预检请求失败;
  2. 检查响应头中是否包含 Access-Control-Allow-Origin 且值正确;
  3. 若涉及 credentials,确保 Access-Control-Allow-Credentials 为 true,且 allowedOrigins 未使用 *
  4. 若为预检请求失败,检查后端是否正确处理 OPTIONS 方法(Spring Security 需允许 OPTIONS 匿名访问)。

3. 排查 CSRF 问题的步骤

  1. 若请求被拒绝并返回 403 Forbidden,检查是否携带了 CSRF Token;
  2. 确认前端获取的 Token 与服务器 session 中的 Token 一致;
  3. 检查请求方法是否为非 GET 方法(CSRF 防护默认对这些方法生效);
  4. 若使用 SameSite Cookie,确认属性值是否与业务场景匹配。

四、总结

CORS 和 CSRF 虽都涉及 “跨域”,但本质完全不同:

  • CORS 是浏览器的安全策略,解决 “合法跨域请求被拦截” 的问题,需通过后端配置响应头放行;
  • CSRF 是一种攻击方式,目标是 “盗用用户身份执行恶意操作”,需通过 Token 验证、来源检查等方式防护。

作为 Java 开发者,掌握 Spring Boot 和 Spring Security 中的相关配置是关键。在实际开发中,应根据业务场景(如是否使用 Cookie、是否有跨域需求)选择合适的方案,既保证安全性,又不影响用户体验。

最后记住:安全没有银弹,需结合多种手段(如 HTTPS、输入验证、权限控制)构建全方位的防护体系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值