关于 Spring Security OAuth2 中 CORS 跨域问题
CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制(跨域资源共享 CORS 详解)。
解决 CORS 跨域方法大致有如下几类:
- 使用 Nginx 代理配置转发请求。
- 在 Zuul (配置允许敏感头信息等) 或 Spring Cloud Gateway 层配置跨域网关路由转发到资源端不涉及跨域。
- Spring Boot 资源端配置以支持跨域(适用于无网关场景)。
Spring Boot 实现 CORS 跨域 (官方)
- 单个方法的跨域支持,可以使用
@CrossOrigin
的注解实现 - 采用 JavaConfig 实现
- 采用 Filter
但是如果项目中包含 Spring Security 就会有 401 的问题,Spring Security 本身是通过 Filter 实现的,如果没有对其单独做 CORS 的处理,在 Web Security 报错 401 的时候是不会返回相应的 CORS 的字段的。这会导致出现的 401 错误成为了一个无法进行跨域的错误,导致前端程序无法正常的处理 401 相应 。对于spring security oauth2 默认接口,例如 /oauth/token 跨域问题,可以通过全局 CORS Filter 解决。
@Configuration public class GlobalCorsConfiguration { @Bean public CorsFilter corsFilter() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowCredentials(true); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); // corsConfiguration.addExposedHeader("head1"); //corsConfiguration.addExposedHeader("Location"); UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); return new CorsFilter(urlBasedCorsConfigurationSource); } }
配置 CorsFilter 优先级与 Spring Security 不拦截 OPTIONS 请求
- 配置 Spring Security 策略,不拦截 OPTIONS 请求
- 自定义 CorsFilter,设置 order 优先级比 Spring Security 的 order 高。
配置服务器允许 /oauth/token的 OPTIONS 方法,因为 /oauth/token 接口是先发一个 OPTIONS 请求,然后再发送 POST请求,如果是 OPTIONS 接口不被允许,就会返回 401 错误。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @Order(-1) public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // http.requestMatchers().antMatchers(HttpMethod.OPTIONS, "/oauth/**","/login/**","/logout/**") // .and() // .authorizeRequests() // .antMatchers().permitAll() // .and() // .formLogin().permitAll(); //新增login form 支持用户登录及授权 http.requestMatchers().antMatchers(HttpMethod.OPTIONS, "/oauth/**") .and() .cors() .and() .csrf().disable(); } }
再次 Vue(vue-resource) 前端测试调用,问题解决。
this.$http.post('http://47.100.188.242:8888/oauth/token', {'grant_type':'password','scope':'read','username':'test','password':'test'} ,{emulateJSON:true,headers:{Authorization: 'Basic Y2ssxpZW50Xzkd5c3VsuX3dlYjpzwZWNyZXRfOTlezdsW5fMTIzNDU2'}}).then(function(){ }, function(){ });
报文
OPTIONS http://localhost:8888/oauth/token HTTP/1.1 Host: localhost:8888 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Access-Control-Request-Method: POST Origin: http://localhost:5000 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36 Access-Control-Request-Headers: authorization Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 HTTP/1.1 200 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: http://localhost:5000 Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: authorization Access-Control-Allow-Credentials: true X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Length: 0 Date: Thu, 27 Sep 2018 04:38:56 GMT ------------------------------------------------------------------- POST http://localhost:8888/oauth/token HTTP/1.1 Host: localhost:8888 Connection: keep-alive Content-Length: 60 Pragma: no-cache Cache-Control: no-cache Accept: */* Origin: http://localhost:5000 Authorization: Basic Y2xpZW50Xzks5c3VuX3dlYjpzZWNyZXRfOsTlzdW5fMTIzNDUs2 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36 Content-Type: application/x-www-form-urlencoded Referer: http://localhost:5000/test.html Accept-Encoding: gzip, deflate, br grant_type=password&username=test&password=test&soap=api HTTP/1.1 200 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Access-Control-Allow-Origin: http://localhost:5000 Access-Control-Allow-Credentials: true Cache-Control: no-store Pragma: no-cache X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block X-Frame-Options: DENY Content-Type: application/json;charset=UTF-8 Date: Thu, 27 Sep 2018 04:38:56 GMT Content-Length: 1560 {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpcnZpbmciLCJzY29wZSI6WyJhbGwiLCJhcGkiLCJ1c2VyIiwicmVhZCIsIndyaXRlIl0sImV4dF9uYW1lIjoiaXJ2aW5nIiwiZXhwIjoxNTM4MDMwMjk4LCJjbGllbnRfbmFtZSI6ImlydmluZyIsImp0aSI6IjIwOTg1ZjJhLWNiMGUtNDRiZi1hZWIyLTYzMGQ5NWFhNDI3ZSIsImNsaWVudF9pZCI6ImNsaWVudF85OXN1bl93ZWIifQ.koBwYKeLGr0KgZkOAN9ENGtKtHyQnzBRR_b0N1Ck1g2hk5VOGKikBTcCf-HBycHDvFZzADyqzi1640JDWo7MwwZm72r4Ih_QN-A_CztAMLyxsMvotMx9Du4z8wOJ4qaOGEuFGj3HFdFaMG3ltaN1WnoxORolFLqd6O-Q21SUhXaOMldOUZ_AyQildNnJ3-EJSavVSEc78jnW0P5fEJtp7QpRTS6nyvwwQifD7uoshnPWWbAeX7rYfAhEie3m7csx6sB_7nnJavKyk_AUpddioOgvw8QDZSmIGPKDeLlFLNpq1NGssgCqECyxdwmWVCs3x7rr6CwRDFfva87moJQAIQ","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJpcnZpbmciLCJzY29wZSI6WyJhbGwiLCJhcGkiLCJ1c2VyIiwicmVhZCIsIndyaXRlIl0sImF0aSI6IjIwOTg1ZjJhLWNiMGUtNDRiZi1hZWIyLTYzMGQ5NWFhNDI3ZSIsImV4dF9uYW1lIjoiaXJ2aW5nIiwiZXhwIjoxNTM4MDIzMjc1LCJjbGllbnRfbmFtZSI6ImlydmluZyIsImp0aSI6IjRiNTc1MTVlLWEwOTUtNGZiMS05MGQ3LTE0MjQyOTcyMWI1YyIsImNsaWVudF9pZCI6ImNsaWVudF85OXN1bl93ZWIifQ.E8gviZsAb8ci_PMAzj4Oj7bqopr8xNwG3LAwa-pi987yVhg7CTlDhD0QOZLqHVPViwMY7dql-j2ccefwpZsfeaL4i1x5xouANoJ-zRJi7aruxJ_3guy2Ln0fReEYnOnkzKRGjWkdeCbxmrFgg0TkDASB_vTsegsbjqpVfCRg7LAvIcZwQCj1uiSqV8jaHbadvZpA1yZXt7lOMILHwtKkcEOkM7xDUbCHrG1J6qNjRNRbZhI34xXzP0EdXq8_FBA-ykpI1NWTt8jqYtQJfjyZCb9StcQmTqIq234d1ES6uiyZz-pqaiAo7qiVbrK85uOPmuCzuYtZcbWs8neiDeL8_g","expires_in":7161,"scope":"all api user read write","client_name":"irving","ext_name":"irving","jti":"20985f2a-cb0e-44bf-aeb2-630d95aa427e"}
备注:
Spring Security 文档中 (https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)定义的方式。
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // by default uses a Bean by the name of corsConfigurationSource .cors().and() ... } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://example.com")); configuration.setAllowedMethods(Arrays.asList("GET","POST")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
Spring Cloud Gateway (http://cloud.spring.io/spring-cloud-gateway/single/spring-cloud-gateway.html#_cors_configuration)
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "docs.spring.io" allowedMethods: - GET
ZUUL
https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.0.M3/single/spring-cloud-netflix.html#_enabling_cross_origin_requests
@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/path-1/**") .allowedOrigins("http://allowed-origin.com") .allowedMethods("GET", "POST"); } }; }
REFER:
https://aisensiy.github.io/2017/11/08/spring-cors-and-security/
http://www.spring4all.com/article/177
https://blog.csdn.net/GeorgeShaw1/article/details/75089734
https://stackoverflow.com/questions/37516755/spring-boot-rest-service-options-401-on-oauth-token
https://github.com/pagekit/vue-resource
http://cloud.spring.io/spring-cloud-gateway/single/spring-cloud-gateway.html#_cors_configuration