Java Spring Security的跨源资源共享

Java Spring Security的跨源资源共享,java,spring-security,cors,Java,Spring Security,Cors,我试图让CORS与Spring Security配合得很好,但它没有遵守。我进行了中所述的更改,并在applicationContext security.xml中更改了这一行。xml已获得我的应用程序的POST和GET请求(临时公开控制器方法,以便我可以测试CORS): 之前: 之后: 不幸的是,以下允许通过AJAX进行Spring安全登录的URL没有响应:http://localhost:8080/mutopia-服务器/资源/j_spring_安全检查。我正在从http://local

我试图让CORS与Spring Security配合得很好,但它没有遵守。我进行了中所述的更改,并在
applicationContext security.xml中更改了这一行。xml
已获得我的应用程序的POST和GET请求(临时公开控制器方法,以便我可以测试CORS):

  • 之前:
  • 之后:
不幸的是,以下允许通过AJAX进行Spring安全登录的URL没有响应:
http://localhost:8080/mutopia-服务器/资源/j_spring_安全检查
。我正在从
http://localhost:80
http://localhost:8080

含铬 当尝试访问
j\u spring\u security\u check
时,我会在Chrome中获取
(挂起)
,以获取选项飞行前请求和AJAX调用返回的HTTP状态代码0和消息“error”

在Firefox中 预飞行成功,HTTP状态代码为302,之后我仍然直接收到AJAX请求的错误回调,HTTP状态为0,消息为“error”

AJAX请求代码
请注意-除了Spring安全登录,我可以通过CORS发布到我应用程序中的任何其他URL。我已经阅读了很多文章,因此如果能深入了解这个奇怪的问题,我将不胜感激。

我可以通过扩展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响应响应飞行前选项请求,该响应还包含CORS标头
您需要在Spring配置中声明这个bean。有很多文章展示了如何做到这一点,所以我不会在这里复制


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

这是我的代码,对我来说非常好:我花了两天时间研究它并理解spring安全性,所以我希望你接受它作为答案,哈哈

 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" />

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

我将扩展CORS过滤器中的if子句,并在origin头上进行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 security将返回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>>" />

在我的例子中,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);
    }
}

对我来说,问题在于
选项
飞行前检查未通过身份验证,因为在该调用中未传递凭据

这对我很有用:

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());
                }
            }
        });
这解决了飞行前的
选项问题。这里发生的事情是,当您收到一个呼叫且身份验证失败时,您检查它是否是
选项
呼叫,如果是,就让它通过,让它做它想做的一切。这实际上禁用了所有浏览器端的飞行前检查,但正常的跨域策略仍然适用

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


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

大多数情况下,选项请求不包含用于验证spring安全性的cookie。
为了解决这个问题,可以修改spring security的配置,以允许选项请求而无需身份验证。
我做了很多研究,得到了两种解决方案:
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”):



最后,还有:)

自Spring Security 4.1以来,这是使Spring Security支持CORS的正确方法(Spring Boot 1.4/1.5中也需要):

以及:

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

  • http.authorizeRequests()
  • web.ignering().antMatchers(HttpMethod.OPTIONS)

参考资料:

由于问题的主要部分是关于登录点的未经筛选的CORS POST请求,我立即向您指出步骤2

但关于答案计数,这是与Spring Security CORS请求最相关的问题。 因此,我将描述使用Spring Security配置CORS的更优雅的解决方案。 因为除了极少数情况外,没有必要创建过滤器/拦截器/…来放置任何响应。 我们将在春天以声明的方式这样做。 从SpringFramework4.2开始,我们就有了现成的CORS,比如过滤器、处理器等。 和一些链接阅读

我们走吧:

1.准备CORS配置源。 这可以通过不同的方式完成:

  • 作为全局Spring MVC CORS配置(在配置中
    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");
        }
    }
    
    @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();
    }
    
    <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>
    
    @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;
        }
    }
    
    ...
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                .allowedOrigins("*")
                ...
        }
    
    @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);
        }
    }
    
        ...
        // @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
        }
    
    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;
        }
    }
    
    http.cors().configurationSource(corsConfig);