Java框架快速入门: Spring Security+OAuth2之跨域处理

纲要

  • 跨域问题背景
    • 浏览器同源策略与 CORS 错误
    • 前后端分离开发中的典型跨域场景
  • 跨域解决方案概览
    • 前端开发服务器代理
    • 后端 CORS 配置
  • Spring MVC 中的全局 CORS 配置
    • WebMvcConfigurer 与 addCorsMappings
    • 允许来源、方法、头部设置
  • Spring Security 中的 CORS 配置
    • 自定义 CorsConfigurationSource Bean
    • 按环境区分配允许的来源(开发 / 生产)
    • 暴露自定义响应头(X-Authenticity 等)
    • 在安全过滤链中启用 CORS
  • 完整可运行配置示例
  • 验证与踩坑
    • 响应头暴露的重要性
    • 跨域请求失败时的排查思路

跨域问题背景

现代前后端分离项目中,前端应用通常运行在 localhost:4001,而后端 API 可能部署在 localhost:8080。当浏览器从不同源(协议、域名、端口任一不同)发起 HTTP 请求时,就会触发浏览器的同源策略,出现类似以下的 CORS 错误:

Access to XMLHttpRequest at 'http://localhost:8080/authorize/token' from origin 'http://localhost:4001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

这意味着后端服务需要显式声明允许哪些源访问,否则浏览器会拦截响应。

跨域解决方案概览

解决跨域访问通常有两种思路:

  1. 前端代理:利用前端开发服务器(如 webpack-dev-server)将 API 请求代理到后端,从而变为同源访问。
  2. 后端 CORS 配置 :在服务端添加 Access-Control-Allow-Origin 等响应头,让浏览器允许跨域请求。

在 Spring 生态中,后端配置 CORS 更为通用,并且不依赖前端工具链,下面重点讲解后端的配置方式。

Spring MVC 中的全局 CORS 配置

如果项目未引入 Spring Security,可以直接在 WebMvcConfigurer 中配置全局 CORS 规则:

java 复制代码
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 GlobalCorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")                // 所有接口
                .allowedOrigins("http://localhost:4001")  // 允许的前端源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

这种方式适用于纯 Spring MVC 项目,但它不会影响 Spring Security 的过滤器链。

Spring Security 中的 CORS 配置

在整合了 Spring Security 的 OAuth2 授权服务中,跨域请求会先经过 Security 过滤器链。如果仅在 MVC 层配置 CORS,Security 层的请求仍会被拦截。正确做法是在 Spring Security 中显式启用 CORS 并提供一个 CorsConfigurationSource Bean。

自定义 CorsConfigurationSource

创建一个配置类,通过 @Value 读取外部配置,动态设置允许的源,并将 CorsConfigurationSource 注册到 Spring 容器中。这里特意暴露了自定义响应头 X-Authenticity,用于二次认证场景。

java 复制代码
package com.example.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 CorsConfig {

    @Value("${cors.allowed-origins:http://localhost:4001}")
    private String allowedOrigins;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        // 重要:暴露自定义响应头,例如二次认证时返回的 X-Authenticity
        configuration.setExposedHeaders(Arrays.asList("X-Authenticity", "Authorization"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

在安全配置中启用 CORS

在 Security 配置类中注入 CorsConfigurationSource,并通过 cors() 方法启用:

java 复制代码
package com.example.config;

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.CorsConfigurationSource;

@Configuration
public class SecurityConfig {

    private final CorsConfigurationSource corsConfigurationSource;

    public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
        this.corsConfigurationSource = corsConfigurationSource;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource))
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt()); // 示例 OAuth2 配置
        return http.build();
    }
}

为什么需要暴露自定义响应头?

在 OAuth2 二次验证流程中,后端可能会在 401 响应里附加 X-Authenticity 头,提示客户端需要进行下一步认证。对于跨域请求,浏览器默认只暴露 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 等少量简单响应头。如果不通过 setExposedHeaders() 显式声明,前端 JavaScript 代码将无法读取 X-Authenticity,导致认证流程中断,可能看到 500 错误或无响应头的情况。

跨域请求流程示意

以下时序图展示了前端发出跨域请求,后端配置 CORS 后顺利返回自定义响应头的过程:
资源服务器 Spring Security 过滤器 浏览器 (localhost:4001) 资源服务器 Spring Security 过滤器 浏览器 (localhost:4001) #mermaid-svg-dNDZsmMpdny2zjZU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dNDZsmMpdny2zjZU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dNDZsmMpdny2zjZU .error-icon{fill:#552222;}#mermaid-svg-dNDZsmMpdny2zjZU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dNDZsmMpdny2zjZU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dNDZsmMpdny2zjZU .marker.cross{stroke:#333333;}#mermaid-svg-dNDZsmMpdny2zjZU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dNDZsmMpdny2zjZU p{margin:0;}#mermaid-svg-dNDZsmMpdny2zjZU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dNDZsmMpdny2zjZU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dNDZsmMpdny2zjZU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .sequenceNumber{fill:white;}#mermaid-svg-dNDZsmMpdny2zjZU #sequencenumber{fill:#333;}#mermaid-svg-dNDZsmMpdny2zjZU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .messageText{fill:#333;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU .labelText,#mermaid-svg-dNDZsmMpdny2zjZU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .loopText,#mermaid-svg-dNDZsmMpdny2zjZU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dNDZsmMpdny2zjZU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dNDZsmMpdny2zjZU .noteText,#mermaid-svg-dNDZsmMpdny2zjZU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .actorPopupMenu{position:absolute;}#mermaid-svg-dNDZsmMpdny2zjZU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dNDZsmMpdny2zjZU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU .actor-man circle,#mermaid-svg-dNDZsmMpdny2zjZU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dNDZsmMpdny2zjZU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 前端可读取 X-Authenticity 并继续流程 OPTIONS /api/authorize (预检)200 OK (Access-Control-Allow-Origin: localhost:4001)POST /api/authorize (真实请求)转发认证401 + X-Authenticity: required401 + X-Authenticity (暴露)

完整项目配置示例

以下为 Spring Boot 项目的核心配置文件及结构,可直接运行。

项目结构

text 复制代码
src/main/java/com/example/config/
├── CorsConfig.java
└── SecurityConfig.java
src/main/resources/
├── application.yml

application.yml

yaml 复制代码
cors:
  allowed-origins: http://localhost:4001

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://your-issuer

CorsConfig.java

java 复制代码
package com.example.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 CorsConfig {

    @Value("${cors.allowed-origins:http://localhost:4001}")
    private String allowedOrigins;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("X-Authenticity", "Authorization"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

SecurityConfig.java

java 复制代码
package com.example.config;

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.CorsConfigurationSource;

@Configuration
public class SecurityConfig {

    private final CorsConfigurationSource corsConfigurationSource;

    public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
        this.corsConfigurationSource = corsConfigurationSource;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource))
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());
        return http.build();
    }
}

验证与踩坑

  1. 预检请求必须通过 :Spring Security 配置 CORS 后,OPTIONS 请求会被 CORS 过滤器处理并返回 200,无需额外放行。
  2. 响应头暴露缺失 :若未调用 setExposedHeaders,即使后端设置了 X-Authenticity,浏览器也会阻止 JavaScript 读取该头,导致认证失败。可根据业务需要添加需要暴露的头名称。
  3. 多环境切换 :通过 @Value 或 @Profile 灵活指定 allowed-origins,开发环境使用 localhost:4001,生产环境使用真实域名。
  4. 调试技巧 :打开浏览器开发者工具 Network 面板,检查响应头中是否包含 Access-Control-Allow-Origin 和 Access-Control-Expose-Headers,确认配置生效。

总结

本文详细梳理了在 Spring Security + OAuth2 项目中处理跨域访问的常见方案。

重点在于 Spring Security 侧的 CorsConfigurationSource 配置,以及暴露自定义响应头的重要性。

通过合理配置,前端可以无缝发起跨域认证请求,并完整读取后端返回的认证提示头,保障二次认证流程的顺利执行。

相关推荐
专业程序开发源2 分钟前
SSM校园拍摄交流服务平台36936-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·php·课程设计
用户094248568034 分钟前
第23章:OpenJDK逃逸分析、标量替换与锁优化
java·jvm
wuminyu6 分钟前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
樱花落木兰16 分钟前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
wuyk55519 分钟前
Python 网络爬虫入门到实战 第 04 章:请求头、UA 伪装、超时、异常处理、基础反爬绕过
开发语言·爬虫·python
HAHAXX823 分钟前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa
流形填表24 分钟前
题库去重实战:基于题干指纹的重复题清理
开发语言·c#
\光辉岁月/26 分钟前
3.java运算符
java·开发语言
知识分享小能手33 分钟前
C++ 学习教程,从入门到精通,string类和标准模板库 — 完整知识点详解(16)
开发语言·c++·学习