SpringBoot 集成oauth2以及跨域配置问题分析

986 阅读7分钟

项目配置:

Springboot: 2.1.9.RELEASE

spring-security-oauth2: 2.5.2.RELEASE

spring-boot-starter-security: 2.1.9.RELEASE

浏览器:Chrome(版本 108.0.5359.98(正式版本) (x86_64)) Safari(版本15.4)

UI: AngularTs

关于跨域

跨域,全称跨源资源共享(CORS)。一般是浏览器设置的安全策略。

浏览器发起一个复杂请求[1]的时候,会先发送 Options 请求到对应接口去 “域检”,(只会发送一次,关闭窗口后需要重新发送)。

截屏2022-12-22 08.57.13.png

当跨域检查请求(options)返回的response的header中有 Access-Control-Allow-Origin 时浏览器胡认定跨域安全(上图可以看到options请求返回的header中是有Access-Control-Allow-Origin的),才会发送后续的请求,否则直接拦截。

跨域检查成功时:

截屏2022-12-24 10.05.56.png 可以看到图里面有这样几个细节,针对于userlist接口总共发送了两次请求,第一次即是上面说的浏览器发送的域检,第二次则是ts脚本发送的实际的post请求,同时浏览器控制台也给域检请求标注了一个Preflight,点击这个按钮时还会高亮显示脚本发送的post请求。

跨域检查失败时(基于上述已经成功过一次的事实): 1)在经过的上面的成功域检之后,在springboot中删去相关配置后,再次对userlist接口发送请求。

截屏2022-12-24 10.30.45.png

可以看到浏览器已经将Status标记为 CORS error,但是可能令人疑惑的是因为已经域检过一次,所以浏览器这次没有在进行跨域检查,但是还是返回了 CORS error,点击一下这个请求。

截屏2022-12-24 10.34.53.png

发现http状态码其实是 200,查看console,可以发现如下日志:

Access to XMLHttpRequest at 'http://localhost:8030/api/user/userlist' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

可以猜测因为此次ts脚本发送的请求返回的response的header中没有Access-Control-Allow-Origin,所以即使此次请求服务器正常返回还是被浏览器认定为跨域检查失败,同时console日志中,可以看到angular输出了如下内容:

user-list.component.ts:44 POST http://localhost:8030/api/user/userlist net::ERR_FAILED 200

user-list.component.ts:44 ERROR TypeError: Cannot read properties of undefined (reading 'data') at Object.next (user-list.component.ts:47:27) at ConsumerObserver.next (Subscriber.js:91:33) at SafeSubscriber._next (Subscriber.js:60:26) at SafeSubscriber.next (Subscriber.js:31:18) at Observable._subscribe (innerFrom.js:51:24) at Observable._trySubscribe (Observable.js:37:25) at Observable.js:31:30 at errorContext (errorContext.js:19:9) at Observable.subscribe (Observable.js:22:21) at catchError.js:14:31

截屏2022-12-24 09.51.00.png

有错误码, net::ERR_FAILED 200,表示请求成功但是有其他异常,这里是缺失了Access-Control-Allow-Origin的header,此时angular的httpclient接口返回HttpErrorResponse,没有返回服务器返回的数据。这里还有一个坑点,此时angular会把http status code设置为0。

private handleError<T>(operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {

    // TODO: send the error to remote logging infrastructure
    console.error(error); // log to console instead
    
    console.log("http status code:" + error.status);
   
    // Let the app keep running by returning an empty result.
    return of(result as T);
  };

http status code:0 后续配置上oauth2的时候,当请求被spring security 拦截时,虽然返回了了401状态码,但是由于缺失了Access-Control-Allow-Origin header,angular会把http status code设置为0,如果想根据状态码为401去重定向到登录页面时就会有问题,因为此时http status code为0,后续也会说到这一点。

跨域检查失败时(此前也未成功过): 需要先改一下服务器端口,或者换一个浏览器,或者打开一个无痕浏览窗口。(不过有趣的是,你得保证先前的任何一个无痕浏览窗口没有进行过成功的域检,否则也不会重复域检)。

截屏2022-12-24 11.06.52.png

可以看到域检请求返回了403错误。

关于cors配置,有好几种方式,配置CrossOrigin注解,实现一个WebMvcConfigurer,或者是实现一个Filter,我这里首先使用的方式是实现了一个WebMvcConfigurer

@Configuration
public class Crof implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600);
    }

关于集成oauth2同时配置跨域

由于同时配置了oauth2和cors(都基于filter实现 存疑?),请求会先被spring security拦截,这个时候会直接返回401,同时浏览器报告401错误。这个时候上面说的那个angular的坑点就出现了。

后续配置上oauth2的时候,当请求被spring security 拦截时,虽然返回了了401状态码,但是由于缺失了Access-Control-Allow-Origin header,angular会把http status code设置为0,如果想根据状态码为401去重定向到登录页面时就会有问题,因为此时http status code为0

解决方法:

第一种尝试(尝试配置WebMvcConfigurer为最高优先级):

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class Crof implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600);
    }
    }

配置了 @Order(Ordered.HIGHEST_PRECEDENCE),把cors设置为了最高优先级。

截屏2022-12-24 12.01.16.png

由于此前已经进行过域检,所以直接发送了post请求,同时返回401错误码,这个是正常的。 需要让浏览器重新域检,这里是换了springboot的端口号。

截屏2022-12-24 12.04.27.png

可以看到域检已经被401了,被spring security 拦截了。

console依旧输出域检失败错误:

Access to XMLHttpRequest at 'http://localhost:8099/api/user/userlist' from origin 'http://localhost:4200' 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.

显然为 WebMvcConfigurer配置order为最高优先级并没有作用。猜测是WebMvcConfigurer应该是被某一个filter使用,只是配置WebMvcConfigurer的优先级没有配置这个filter是不会起到任何作用的。

第二种尝试(修改security: WebSecurityConfigurerAdapter配置,使其不拦截option请求):

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers(HttpMethod.OPTIONS);
}

截屏2022-12-24 12.11.02.png

可以看到域检成功,options请求返回的header中也有 Access-Control-Allow-Origin。 随后ts脚本对userlist接口发送http post请求。

截屏2022-12-24 12.14.23.png

被spring security 拦截,返回401,看起来一切正常了,但是查看console 日志,显示:

Access to XMLHttpRequest at 'http://localhost:8099/api/user/userlist' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

可以看到跟域检失败输出的日志的内容是比较像的,区别在于前者是针对于域检请求,后者大概率是请求完userlist接口,返回的header中没有Access-Control-Allow-Origin,所以被浏览器和angular判定为cors失败。这个坑点再次出现。

后续配置上oauth2的时候,当请求被spring security 拦截时,虽然返回了了401状态码,但是由于缺失了Access-Control-Allow-Origin header,angular会把http status code设置为0,如果想根据状态码为401去重定向到登录页面时就会有问题,因为此时http status code为0

截屏2022-12-24 12.18.51.png

Response header中确实也没有Access-Control-Allow-Origin

第三种尝试(配置一个cors filter,同时设置其为最高优先级)

@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
public class CORSFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,PUT,DELETE,PATCH,HEAD");
        response.setHeader("Access-Control-Allow-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "*");
        filterChain.doFilter(servletRequest, servletResponse);
        
//        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
//            response.setStatus(HttpServletResponse.SC_OK);
//        } else {
//        }
    }
    
    @Override
    public void destroy() {
        
    }
}

截屏2022-12-24 12.29.13.png 域检成功。

截屏2022-12-24 12.29.20.png console不再输出cors错误,angular也正常输出401错误码。

截屏2022-12-24 12.33.17.png header中是有Access-Control-Allow-Origin的。

总结:域检成功,同时console不再输出cors错误,angular也正常输出401错误码。

这里在配置了最高优先级的cors filter后有两种做法,一种是按照第二种尝试让spring security 忽略options请求,另一种则是上述代码被注释的而部分:

//        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
//            response.setStatus(HttpServletResponse.SC_OK);
//        } else {
//        }

让 options请求到此为止,否则后续被spring security filter处理后还是会被拦截。

截屏2022-12-24 12.42.59.png

Reference:

[1] 从前后端的角度分析options预检请求 - 掘金 (juejin.cn)

跨域403分析: zhuanlan.zhihu.com/p/73427930

oauth2 401问题排查: blog.csdn.net/jll126/arti…