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 配置,以及暴露自定义响应头的重要性。

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

相关推荐
星夜离尘567 小时前
MATLAB 工程实验:不等间隔流量积分,先把缺口与覆盖率说清楚
开发语言·matlab·时间戳·数据质量·数值积分
_Evan_Yao7 小时前
Redis 缓存穿透、击穿、雪崩,一次讲清!
java·spring·mybatis
Ai-_Man7 小时前
Meta Muse还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·c#·1024程序员节
梓豪csd7 小时前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
秋饼7 小时前
Spring AI 2.1 Message Parts 深度实战:推理签名回放与跨轮记忆持久化的源码级落地
java·ai·技术分享·后端开发
学逆向的8 小时前
C 枚举变量
c语言·开发语言·网络安全
Sakura_j_h_8 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
thefool1122668 小时前
TCP 1
java·1024程序员节
金銀銅鐵10 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手11 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务