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

纲要

  • 跨域问题背景
    • 浏览器同源策略与 CORS 错误
    • 前后端分离开发中的典型跨域场景
  • 跨域解决方案概览
    • 前端开发服务器代理
    • 后端 CORS 配置
  • Spring MVC 中的全局 CORS 配置
    • WebMvcConfigureraddCorsMappings
    • 允许来源、方法、头部设置
  • 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-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma 等少量简单响应头。如果不通过 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-OriginAccess-Control-Expose-Headers,确认配置生效。

总结

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

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

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

相关推荐
雪芽蓝域zzs2 小时前
第四十二节:全局字典封装(后端字典,下拉选择复用)
开发语言·前端·javascript
我是大猴子2 小时前
MyBatis‑Plus & MyBatis‑Flex 区别
java·服务器·数据库
老王爱玩车2 小时前
第2讲:C语言数据类型和变量
c语言·开发语言
Bs_MoneyMagnet2 小时前
基于springboot+vue的在线音乐管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
YangYang9YangYan2 小时前
2026 校招管理会计 JD 拆解,数据分析能力要求与工具清单
java·数据库·数据分析
菜鸟~noob2332 小时前
【电子战】第12篇:TDOA 定位——双曲线与等时差线【含matlab代码】
开发语言·matlab
零依赖极客3 小时前
Day 9·1 KV 也量化——q8 KV 把缓存与 decode 带宽压到一半
c语言·开发语言·arm开发·人工智能·缓存·矩阵
写后端的胖头鱼3 小时前
【高频面试题】分布式锁在项目中的应用
java·分布式·后端·分布式锁·高频面试题