Spring Security的跨源资源共享

我正在努力让CORS与Spring Security很好地合作,但它并不符合。 我做了本文中描述的更改并在applicationContext-security.xml更改此行已经为我的应用程序提供了POST和GET请求(暂时公开控制器方法,因此我可以测试CORS):

  • 之前:
  • 之后:

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

在Chrome中

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

在Firefox中

预检成功使用HTTP状态代码302,然后我仍然直接获得我的AJAX请求的错误回调,其中HTTP状态为0,消息为“error”。

在此处输入图像描述

在此处输入图像描述

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 POST到我的应用程序中的任何其他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响应响应飞行前OPTIONS请求,该响应还包含CORS标头。

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

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

这是我的代码工作得很好,对我来说很完美:我花了两天时间研究它并理解弹簧安全性所以我希望你接受它作为答案,哈哈

  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); } } } 

那么你还需要设置你的filter来调用:

  ... //your other configs  // this goes to your filter  

那么你需要一个bean用于你创建的自定义filter:

  

从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

大多数情况下,OPTIONS请求不带cookie来validationspring security。
要重新启用它,可以修改spring security的配置以允许OPTIONS请求而无需身份validation。
我经常研究并得到两个解决方案:
1.使用带有Spring安全配置的Java配置,

 @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”):

      

最后,有解决方案的来源…… 🙂

对我来说,问题是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预检问题。 这里发生的是当你收到一个电话并且validation失败时,你检查它是否是一个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"); } } 

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

在我的情况下,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); } } 

我完全赞同Bludream给出的答案,但我有一些评论:

我会在CORSfilter中扩展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安全性将返回HTTP状态403:FORBIDDEN并返回CORS头。 但是,如果我使用未知令牌或无效的令牌,则返回HTTP状态401:UNAUTHORIZED WITHOUT CORS标头。

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

  ... //your other configs   

我们的自定义filter的以下bean:

  

由于问题的主要部分是关于未登录的CORS POST请求到登录点,我立即指向第2步。

但是关于答案,这是Spring Security CORS请求中最相关的问题。 因此,我将描述使用Spring Security配置CORS的更优雅的解决方案。 因为除了极少数情况之外,没有必要创建filter/拦截器/ ……来放置任何响应。 我们将在春季以声明方式做到这一点。 从Spring Framework 4.2开始,我们就有了filter,处理器等开箱即用的CORSfunction。 还有一些链接要读1 2 。

我们走吧:

1.准备CORS配置源。

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

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

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

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

     // @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自动assembly或明确设置(参见示例)。 此外,我们允许未经授权访问某些端点,如登录:

  ... // @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 MVC看不到Spring MVC全局CORS配置。 所以我使用直接使用外部类的解决方案:

 http.cors().configurationSource(corsConfig);