spring webflux CORS 标头被删除

Posted

技术标签:

【中文标题】spring webflux CORS 标头被删除【英文标题】:spring webflux CORS headers getting removed 【发布时间】:2020-05-01 05:27:43 【问题描述】:

编辑:请阅读更新,问题已显着改变。

我在这方面被严重阻止了。我有一个 spring webflux 应用程序,我正在尝试在其上启用 CORS 标头,以便我能够在同一个浏览器会话中执行来自不同来源的请求。但无论我做什么,CORS 标头都会被删除(即使我手动将它们放在 ServerResponse 中)。我正在使用的 security/ 和 config/ 中的一些类是:

package com.document.feed.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

import reactor.core.publisher.Mono;

    @EnableWebFluxSecurity
    @EnableReactiveMethodSecurity
    public class SecurityConfig 

        @Autowired
        private AuthenticationManager authenticationManager;

        @Autowired
        private SecurityContextRepository securityContextRepository;

        @Bean
        SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) 
            String[] patterns = new String[] "/auth/**", "/vanillalist";
            return http
                    .exceptionHandling()
                    .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> 
                        swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                    )).accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> 
                        swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                    )).and()
                    .csrf().disable()
                    .authenticationManager(authenticationManager)
                    .securityContextRepository(securityContextRepository)
                    .authorizeExchange()
                        .pathMatchers(patterns).permitAll()
                        .pathMatchers(HttpMethod.OPTIONS).permitAll()
                    .anyExchange().authenticated()
                    .and()
                    .build();
        
    

SecurityContextRepository.java

package com.document.feed.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import com.document.feed.config.JwtTokenUtil;
import reactor.core.publisher.Mono;

@Component
public class SecurityContextRepository implements ServerSecurityContextRepository 

    private static final Logger logger = LoggerFactory.getLogger(SecurityContextRepository.class);

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public Mono save(ServerWebExchange serverWebExchange, SecurityContext sc) 
        throw new UnsupportedOperationException("Not supported yet.");
    

    @Override
    public Mono load(ServerWebExchange serverWebExchange) 
        System.out.println("serverWebExchange:" + serverWebExchange.getAttributes());
        ServerHttpRequest request = serverWebExchange.getRequest();
        String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        String authToken = null;
        if (authHeader != null && authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) 
            authToken = authHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
        else 
            logger.warn("couldn't find bearer string, will ignore the header.");
        
        System.out.println("SecurityContextRepository.authToken=" + authToken +
                    "\nauthHeader=" + authHeader);
        String username;
        try 
            username = jwtTokenUtil.getUsernameFromToken(authToken);
         catch (Exception e) 
            username = null;
        
        System.out.println("SecurityContextRepository.username:" + username);
        if (authToken != null) 
            Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
            return authenticationManager.authenticate(auth).map((authentication) -> 
                SecurityContextHolder
                        .getContext().setAuthentication((Authentication) authentication);
                return new SecurityContextImpl((Authentication) authentication);
            );
         else 
            return Mono.empty();
        
    



package com.document.feed.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;

import com.sun.org.apache.xerces.internal.parsers.SecurityConfiguration;

@Configuration
@EnableWebFlux
@Import(CorsConfiguration.class, SecurityConfiguration.class)
public class CorsGlobalConfiguration implements WebFluxConfigurer 

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) 
        corsRegistry.addMapping("/**")
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(false)
                .exposedHeaders("Access-Control-Allow-Origin",
                        "Access-Control-Allow-Methods",
                        "Access-Control-Allow-Headers",
                        "Access-Control-Max-Age",
                        "Access-Control-Request-Headers",
                        "Access-Control-Request-Method");
    

看看,Access-Control-Allow-* 是如何被丢弃的,而Access-Control-Request-* 是如何保留在响应头中的。

在 chrome 的控制台中看到的错误:

fetch('http://localhost:8080/vanillalist', 
  method: 'GET',
  headers: 
    'Content-type': 'application/json; charset=UTF-8'
  
)
.then(res => res.json())
.then(console.log)
Promise <pending>
2VM778:1 OPTIONS http://localhost:8080/vanillalist 404 (Not Found)
(anonymous) @ VM778:1
:3000/#/:1 Access to fetch at 'http://localhost:8080/vanillalist' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
:3000/#/:1 Uncaught (in promise) TypeError: Failed to fetch

更新:第三条评论的图片。

更新2: @mikeb 请求curl -v,在请求标头中添加OPTIONS

(venv) NB292:scaligent devansh.dalal$ curl -v http://localhost:8080/vanillalist > /tmp/r.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /vanillalist HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Type: application/json
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< Referrer-Policy: no-referrer
< 
 [8 bytes data]
100 1350k    0 1350k    0     0  19.9M      0 --:--:-- --:--:-- --:--:-- 19.9M
* Connection #0 to host localhost left intact

Update3:现在问题已经减少,可以处理OPTIONS 类型的请求。

【问题讨论】:

所以当我为OPTIONS 添加路由并开始为它返回ServerResponse.ok() 时,fetch 开始为我工作。但基本上这是解决方法。我要为OPTIONS REST 调用添加处理程序? 【参考方案1】:

您的问题是 fetch 尝试执行 OPTIONS 请求作为预检,您需要允许 OPTIONS 以及 GET、PUT 等...

.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")

这应该可以解决您的问题。

请看这里:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests

与“简单请求”(上面讨论过)不同,“预检”请求首先通过 OPTIONS 方法向另一个域上的资源发送 HTTP 请求,以确定实际请求是否可以安全发送。跨站点请求是这样预检的,因为它们可能会影响用户数据。

为所有请求支持 OPTIONS 的最简单方法是通过过滤器。编写一个过滤器来检测和 OPTIONS 请求并响应它。

【讨论】:

curl -v测试你的请求,看看你用GET请求得到了什么。然后用 OPTIONS 而不是 GET 进行测试,看看你得到了什么标题。 请看第三条评论,我收到了Vary: Origin Vary: Access-Control-Request-MethodVary: Access-Control-Request-Headerscurl -v 中还有其他内容吗 再次,使用curl -v 测试您的请求,这样您就可以确切地看到发生了什么。浏览器屏幕截图对我没有太大帮助。 根据您的要求更新了问题。 @mikeb 似乎我的 spring 服务器没有返回 OPTIONS 飞行前的正常状态,这就是我收到 CORS 错误的原因。想知道,我是否还需要为 OPTIONS 调用编写处理程序? @SecretAgentMan。【参考方案2】:

使用 Spring Security 后重写 addCorsMappings() 无效。只需定义一个 CorsConfigurationSource bean 即可。看下面代码,是用kotlin写的,

@Configuration
class GlobalWebConfig 

    private fun corsConfiguration(corsProperties: CorsProperties): CorsConfiguration 
        val corsConfiguration = CorsConfiguration()
        corsConfiguration.allowCredentials = corsProperties.credentials
        corsConfiguration.allowedHeaders = corsProperties.headers
        corsConfiguration.allowedOrigins = corsProperties.origins
        corsConfiguration.allowedMethods = corsProperties.methods
        corsConfiguration.maxAge = corsProperties.age
        return corsConfiguration
    

    @Bean
    fun corsConfigurationSource(corsProperties: CorsProperties): CorsConfigurationSource 
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", corsConfiguration(corsProperties))
        return source
    

【讨论】:

或者你可以定义一个CorsWebFilter bean。

以上是关于spring webflux CORS 标头被删除的主要内容,如果未能解决你的问题,请参考以下文章

Java Spring Boot Webflux cors 被阻止

WebTestClient - 带有 Spring Boot 和 Webflux 的 CORS

Spring Boot Cors“Access-Control-Max-Age”标头被浏览器忽略

如何为 Webflux、Spring-Security、okta-spring-boot-starter 和 Okta-React 正确使用 CORS?

spring security CORS 过滤器允许没有“Origin”标头的请求

如何记录 spring-webflux WebClient 请求 + 响应详细信息(正文、标头、elasped_time)?