31

我试图让 CORS 与 Spring Security 很好地配合,但它不符合要求。我进行了本文中描述的更改,并更改了这一行,applicationContext-security.xml使 POST 和 GET 请求适用于我的应用程序(暂时公开控制器方法,因此我可以测试 CORS):

  • 前:<intercept-url pattern="/**" access="isAuthenticated()" />
  • 后:<intercept-url pattern="/**" access="permitAll" />

不幸的是,以下允许通过 AJAX 登录 Spring Security 的 URL 没有响应:http://localhost:8080/mutopia-server/resources/j_spring_security_check. 我正在从http://localhost:80to发出 AJAX 请求http://localhost:8080

在 Chrome 中

尝试访问时,j_spring_security_check我进入(pending)Chrome 以获取 OPTIONS 预检请求,并且 AJAX 调用返回 HTTP 状态代码 0 和消息“错误”。

在火狐中

预检以 HTTP 状态代码 302 成功,之后我仍然会直接收到我的 AJAX 请求的错误回调,其中包含 HTTP 状态 0 和消息“错误”。

在此处输入图像描述

在此处输入图像描述

AJAX 请求代码

function get(url, json) {
    var args = {
        type: 'GET',
        url: url,
        // async: false,
        // crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        success: function(response) {
            console.debug(url, response);
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
        }
    };
    if (json) {
        args.contentType = 'application/json'
    }
    $.ajax(args);
}

function post(url, json, data, dataEncode) {
    var args = {
        type: 'POST',
        url: url,
        // async: false,
        crossDomain: true,
        xhrFields: {
            withCredentials: false
        },
        beforeSend: function(xhr){
            // This is always added by default
            // Ignoring this prevents preflight - but expects browser to follow 302 location change
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.setRequestHeader("X-Ajax-call", "true");
        },
        success: function(data, textStatus, xhr) {
            // var location = xhr.getResponseHeader('Location');
            console.error('success', url, xhr.getAllResponseHeaders());
        },
        error: function(xhr) {
            console.error(url, xhr.status, xhr.statusText);
            console.error('fail', url, xhr.getAllResponseHeaders());
        }
    }
    if (json) {
        args.contentType = 'application/json'
    }
    if (typeof data != 'undefined') {
        // Send JSON raw in the body
        args.data = dataEncode ? JSON.stringify(data) : data;
    }
    console.debug('args', args);
    $.ajax(args);
}

var loginJSON = {"j_username": "username", "j_password": "password"};

// Fails
post('http://localhost:8080/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
post('http://localhost/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);

// Works
get('http://localhost:8080/mutopia-server/landuses?projectId=6', true);

// Works
post('http://localhost:8080/mutopia-server/params', true, {
    "name": "testing",
    "local": false,
    "generated": false,
    "project": 6
}, true);

请注意 - 除了 Spring Security 登录之外,我可以通过 CORS 发布到我的应用程序中的任何其他 URL。我已经阅读了很多文章,因此对这个奇怪问题的任何见解将不胜感激

4

8 回答 8

23

我能够通过扩展 UsernamePasswordAuthenticationFilter 来做到这一点......我的代码在 Groovy 中,希望没关系:

public class CorsAwareAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    static final String ORIGIN = 'Origin'

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
        if (request.getHeader(ORIGIN)) {
            String origin = request.getHeader(ORIGIN)
            response.addHeader('Access-Control-Allow-Origin', origin)
            response.addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
            response.addHeader('Access-Control-Allow-Credentials', 'true')
            response.addHeader('Access-Control-Allow-Headers',
                    request.getHeader('Access-Control-Request-Headers'))
        }
        if (request.method == 'OPTIONS') {
            response.writer.print('OK')
            response.writer.flush()
            return
        }
        return super.attemptAuthentication(request, response)
    }
}

上面的重要位:

  • 如果检测到 CORS 请求,则仅将 CORS 标头添加到响应中
  • 使用简单的非空 200 响应响应飞行前 OPTIONS 请求,该响应还包含 CORS 标头。

您需要在 Spring 配置中声明此 bean。有很多文章展示了如何做到这一点,所以我不会在这里复制。

在我自己的实现中,我使用原始域白名单,因为我只允许 CORS 供内部开发人员访问。以上是我正在做的事情的简化版本,可能需要调整,但这应该给你一个大致的想法。

于 2013-11-26T08:07:23.683 回答
17

好吧,这是我的代码运行良好,对我来说非常完美:我花了两天时间研究它并了解 Spring Security,所以我希望你接受它作为答案,哈哈

 public class CorsFilter extends OncePerRequestFilter  {
    static final String ORIGIN = "Origin";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        System.out.println(request.getHeader(ORIGIN));
        System.out.println(request.getMethod());
        if (request.getHeader(ORIGIN).equals("null")) {
            String origin = request.getHeader(ORIGIN);
            response.setHeader("Access-Control-Allow-Origin", "*");//* or origin as u prefer
            response.setHeader("Access-Control-Allow-Credentials", "true");
           response.setHeader("Access-Control-Allow-Headers",
                    request.getHeader("Access-Control-Request-Headers"));
        }
        if (request.getMethod().equals("OPTIONS")) {
            try {
                response.getWriter().print("OK");
                response.getWriter().flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else{
        filterChain.doFilter(request, response);
        }
    }
}

那么你还需要设置你的过滤器被调用:

<security:http use-expressions="true" .... >
     ...
     //your other configs
    <security:custom-filter ref="corsHandler" after="PRE_AUTH_FILTER"/> // this goes to your filter
</security:http>

好吧,您需要一个 bean 来创建您创建的自定义过滤器:

<bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />
于 2014-04-21T11:30:03.787 回答
12

从 Spring Security 4.1 开始,这是使 Spring Security 支持 CORS 的正确方法(在 Spring Boot 1.4/1.5 中也需要):

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
    }
}

和:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();
        http.cors();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(ImmutableList.of("*"));
        configuration.setAllowedMethods(ImmutableList.of("HEAD",
                "GET", "POST", "PUT", "DELETE", "PATCH"));
        // setAllowCredentials(true) is important, otherwise:
        // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
        configuration.setAllowCredentials(true);
        // setAllowedHeaders is important! Without it, OPTIONS preflight request
        // will fail with 403 Invalid CORS request
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

请勿执行以下任何操作,这是尝试解决问题的错误方法:

  • http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
  • web.ignoring().antMatchers(HttpMethod.OPTIONS);

参考:http ://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html

于 2017-04-22T12:34:05.007 回答
5

大多数情况下,OPTIONS 请求不携带用于 Spring Security 身份验证的 cookie。
为了解决这个问题,可以修改 spring security 的配置以允许未经身份验证的OPTIONS请求。
我研究了很多,得到了两个解决方案:
1.将 Java 配置与 spring 安全配置一起使用,

@Override
protected void configure(HttpSecurity http) throws Exception
{
    http
    .csrf().disable()
    .authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
    .antMatchers("/resources/**").permitAll()
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .and()
    .httpBasic();
}

2.使用XML(注意。不能不写“POST,GET”):

<http auto-config="true">
    <intercept-url pattern="/client/edit" access="isAuthenticated" method="GET" />
    <intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="POST" />
    <intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="GET" />
</http>

最后,有解决方案的来源...... :)

于 2016-06-03T10:08:47.620 回答
3

就我而言, response.getWriter().flush() 不起作用

更改代码如下,它开始工作

public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

    LOGGER.info("Start API::CORSFilter");
    HttpServletRequest oRequest = (HttpServletRequest) request;
    HttpServletResponse response = (HttpServletResponse) res;
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "POST,PUT, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Max-Age", "3600");
    response.setHeader("Access-Control-Allow-Headers",
            " Origin, X-Requested-With, Content-Type, Accept,AUTH-TOKEN");
    if (oRequest.getMethod().equals("OPTIONS")) {
        response.flushBuffer();
    } else {
        chain.doFilter(request, response);
    }
}
于 2015-08-14T14:49:28.240 回答
3

对我来说,问题在于OPTIONS预检检查未通过身份验证,因为该调用未传递凭据。

这对我有用:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@EnableAsync
@EnableScheduling
@EnableSpringDataWebSupport
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .httpBasic().and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().anonymous().disable()
                .exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
            @Override
            public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
                if(HttpMethod.OPTIONS.matches(request.getMethod())){
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                }else{
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
                }
            }
        });

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

相关部分是:

.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
            @Override
            public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
                if(HttpMethod.OPTIONS.matches(request.getMethod())){
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
                    response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                }else{
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
                }
            }
        });

这解决了OPTIONS预检问题。这里发生的情况是,当您接到一个电话并且身份验证失败时,您检查它是否是一个OPTIONS电话,如果是,就让它通过并让它做它想做的一切。这实际上禁用了所有浏览器端的预检检查,但正常的跨域策略仍然适用。

当您使用最新版本的 Spring 时,您可以使用下面的代码来允许全局跨源请求(适用于所有控制器):

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Component
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("http://localhost:3000");
    }
}

请注意,像这样硬编码它很少是一个好主意。在我工作过的几家公司中,允许的来源可以通过管理门户进行配置,因此在开发环境中,您可以添加所需的所有来源。

于 2016-04-09T02:20:45.523 回答
3

由于问题的主要部分是关于登录点的未经授权的 CORS POST 请求,因此我立即将您指向第2步。

但是关于答案计数,这是与Spring Security CORS请求最相关的问题。因此,我将描述使用 Spring Security 配置 CORS 的更优雅的解决方案。因为除了极少数情况外,没有必要创建过滤器/拦截器/……来响应任何内容。我们将在 Spring 之前以声明方式执行此操作。从 Spring Framework 4.2 开始,我们就有了开箱即用的 CORS 东西,如过滤器、处理器等。还有一些阅读1 2的链接。

我们走吧:

1.准备CORS配置源。

它可以通过不同的方式完成:

  • 作为全局 Spring MVC CORS 配置(在配置类中WebMvcConfigurerAdapter

    ...
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                .allowedOrigins("*")
                ...
        }
    
  • 作为单独的corsConfigurationSource

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.applyPermitDefaultValues();
    
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
    }
    
  • 作为外部类(可以通过构造函数使用或作为组件自动装配)

    // @Component // <- for autowiring
    class CorsConfig extends UrlBasedCorsConfigurationSource {
    
        CorsConfig() {
            orsConfiguration config = new CorsConfiguration();
            config.applyPermitDefaultValues(); // <- frequantly used values
    
            this.registerCorsConfiguration("/**", config);
        }
    }
    

2.使用定义的配置启用CORS支持

我们将在 Spring Security 类中启用 CORS 支持,例如WebSecurityConfigurerAdapter. 确保corsConfigurationSource可以访问此支持。否则通过@Resource自动装配或显式设置提供它(参见示例)。我们还允许未经授权访问某些端点,例如登录:

    ...
    // @Resource // <- for autowired solution
    // CorseConfigurationSource corsConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();

        // or autowiring
        // http.cors().configurationSource(corsConfig);

        // or direct set
        // http.cors().configurationSource(new CorsConfig());

        http.authorizeRequests()
                .antMatchers("/login").permitAll() // without this line login point will be unaccessible for authorized access
                .antMatchers("/*").hasAnyAuthority(Authority.all()); // <- all other security stuff
    }

3.自定义CORS配置

如果基本配置有效,那么我们可以自定义映射、来源等。甚至为不同的映射添加多个配置。例如,我显式声明了所有 CORS 参数并让 UrlPathHelper 不修剪我的 servlet 路径:

class RestCorsConfig extends UrlBasedCorsConfigurationSource {

    RestCorsConfig() {
        this.setCorsConfigurations(Collections.singletonMap("/**", corsConfig()));
        this.setAlwaysUseFullPath(true);
    }

    private static CorsConfiguration corsConfig() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedHeader("*");
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.setMaxAge(3600L);
        return config;
    }
}

4.故障排除

为了调试我的问题,我正在跟踪org.springframework.web.filter.CorsFilter#doFilterInternal方法。我看到 CorsConfiguration 搜索返回null,因为 Spring Security 看不到 Spring MVC 全局 CORS 配置。所以我使用了直接使用外部类的解决方案:

http.cors().configurationSource(corsConfig);
于 2017-09-01T10:04:28.247 回答
2

我完全同意 Bludream 给出的答案,但我有几点意见:

我将扩展 CORS 过滤器中的 if 子句,并对源头进行 NULL 检查:

public class CorsFilter extends OncePerRequestFilter {

    private static final String ORIGIN = "Origin";


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        if (request.getHeader(ORIGIN) == null || request.getHeader(ORIGIN).equals("null")) {
            response.addHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.addHeader("Access-Control-Max-Age", "10");

            String reqHead = request.getHeader("Access-Control-Request-Headers");

            if (!StringUtils.isEmpty(reqHead)) {
                response.addHeader("Access-Control-Allow-Headers", reqHead);
            }
        }
        if (request.getMethod().equals("OPTIONS")) {
            try {
                response.getWriter().print("OK");
                response.getWriter().flush();
            } catch (IOException e) {
            e.printStackTrace();
            }
        } else{
            filterChain.doFilter(request, response);
        }
    }
 }

此外,我注意到以下不良行为:如果我尝试使用未经授权的角色访问 REST API,Spring 安全性会返回 HTTP 状态 403: FORBIDDEN 并返回 CORS 标头。但是,如果我使用未知令牌或不再有效的令牌,则会返回 HTTP 状态 401: UNAUTHORIZED 而没有 CORS 标头。

我设法通过更改安全 XML 中的过滤器配置使其工作,如下所示:

<security:http use-expressions="true" .... >
    ...
    //your other configs
    <sec:custom-filter ref="corsFilter" before="HEADERS_FILTER"/>
</security:http>

以下 bean 用于我们的自定义过滤器:

<bean id="corsFilter" class="<<location of the CORS filter class>>" />
于 2014-10-21T13:21:46.053 回答