经历了几次集成测试,你此时肯定被莫名的CORS错误搞的焦头烂额。可能既不知道它是怎么产生的,又不知道它为何产生。在本教程的1.5.2小节前后台对接初期便遇到了此CORS错误,在教程的2.2.4前后台对接时仍然遇到了此CORS错误,在5.2.9小节的集成测试中又出现了此CORS错误。本节中尝试带领大家找找这错误的根本原因。 # OPTIONS 在教程中学习了使用get方法查询数据、post方法新增数据、put方法更新数据、delete方法删除数据。在前面的章节中已提到`浏览在进行跨域访问时,如果发现请求的方法不是get(各浏览器处理的方式不同),那么将首先发起options请求`,而这个options请求在chrome的控制台中并未发现。但换一个浏览器就不一样了:使用firefox进行用户登录将在网络中查看到如下信息: 在登录前首先在登录地址上发起options请求: ![](https://img.kancloud.cn/6a/d6/6ad61eed0b5d6f4b16ddb5b22df40488_979x499.png) 当发现`Access-Control-Allow-Methods`的值`PUT,DELETE,POST,GET,PATCH`包含`POST`时,而且`Access-Control-Allow-Origin`包含当前请求域名时才继续发起`POST`请求;在以后发起的每次请求的响应中,浏览器还会比较`Access-Control-Allow-Methods`的值`PUT,DELETE,POST,GET,PATCH`是否包含当前请求的方法以及`Access-Control-Allow-Origin`的值是否包含当前请求域名。如未满足包含条件将阻断相应的请求。 ![](https://img.kancloud.cn/87/80/878002597b3a11caf33f50c1a7b1a7ce_464x96.png) ## 测试一 注销到当前登录用户,然后终止前台服务,并使用`ng serve --port 4201`重新启动前台。使用浏览器打开`http://localhost:4201/`并使用用户名密码登录: ![](https://img.kancloud.cn/4a/0e/4a0e09db718bd7b89cfb537dc5e52a7c_1228x486.png) options请求直接返回了403(无权限),浏览器未接收到`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`,自动终止了用户登录的post请求。 ## 测试二 终止前台服务,重新使用`ng serve`启动前台。找到后台的`config/WebConfig.java`。删除` .allowedMethods("PUT", "DELETE", "POST", "GET", "PATCH")`中的`"POST"`: ```java .allowedOrigins("http://localhost:4200") .allowedMethods("PUT", "DELETE", "GET", "PATCH") .exposedHeaders("auth-token"); ``` 重新启动后台后在前台`http://localhost:4200/`继续尝试登录: ![](https://img.kancloud.cn/b6/5c/b65c31bf78a2c577cc4a7698dd1cf344_532x477.png) 发起options请求时直接返回了403,浏览器未接收到`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`,浏览器同样直接终止了用户的登录请求。 **注意:** 请恢复后台代码,重启后台后继续学习。 # 错误重现 若想根本上解决CORS问题关键在于错误重现。此CORS错误可以按以下步骤重现。步骤一:启动前台、后台,使用用户进行登录。步骤二:重新启动后台。此时在前台进行操作将得到如下错误信息: ![](https://img.kancloud.cn/47/31/4731bbf60e3074a4d5d93786179be5e0_1247x158.png) ## 解决错误 可以在sessionStorage中清除isLogin达到清除错误的目的。间接的说明该错误是由于登录缓存isLogin引起的。为了彻底弄清楚这个问题,点击注销并查看网络请求信息: ![](https://img.kancloud.cn/a5/e5/a5e57292a4f7e406cd761d59c20f4054_660x83.png) 用户点击注销时,浏览器首先在注销地址发起了options请求,接收到了`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`并符合发向注销地址发请get请求的要求,近而继续发起了get请求。 ![](https://img.kancloud.cn/e6/73/e673123d13b599e89cdd0b0f3988634e_524x399.png) 在该请求中并未接收到`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`信息,所以浏览器终止了此请求。只所以未接收到有效的`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`信息,是由于在后台的认证拦截器对注销功能进行了拦截:未登录的用户在访问注销接口时,将直接返回401未认证的状态码。也就是说当前后只支持已登录的用户发起注销请求,未登录的用户无法发起注销请求。这个逻辑并没有任何问题。问题出在:如果由于后台重启的原因导致前台的authToken失效,前台需要在接收到401的状态码后及时的切换用户的登录状态为:未登录。而当前由于后台返回401时未返回`Access-Control-Allow-Methods`以及`Access-Control-Allow-Origin`信息,从而导致了前台代码无法获取该401状态码的信息。 综上,似使用以下步骤依次解决由于后台重启的原因导致的authToken失效的问题。 ### 加入Access-Control-Allow-Methods 找到后台的认证拦截器,加入`Access-Control-Allow-Methods`信息: interceptor/AuthInterceptor.java ```java response.setStatus(401); response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,PATCH"); return false; } ``` 重新启动后台,再次点击注销按钮: ![](https://img.kancloud.cn/3f/79/3f795fdf841e8c45a7765f8c5a7ac787_513x371.png) 控制台信息: ![](https://img.kancloud.cn/4f/6d/4f6d218b42cedcca842d8245c3b445d4_1134x46.png) ### 加入Access-Control-Allow-Origin interceptor/AuthInterceptor.java ```java response.setStatus(401); response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,PATCH"); response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200"); return false; } ``` ![](https://img.kancloud.cn/88/8c/888c238efa3ac679fd3d65f998181cdf_504x361.png) 控制台关于CORS的错误消失 ### 当接收到401的在前台完成注销操作 后台发出了401的信息则说明当前的认证用户已经由于各种完成自动被后台注销掉了,那么此时应该在前台主动的跳出登录界面。我们在前台中使用了拦截器来处理authToken,同样的还可以在前台的拦截器中统一处理401信息。 在数据流的转发过程中若想获取错误的数据流,则需要使用`tap`操作符。该操作符可接收3个回调函数做为参数,当执行成功时将调用第一个回调函数(必须传入),当执行失败时将调用第二个回调函数(选择传入),无论执行成功或失败均调用第3个回调函数(选择传入)。 它的用法如下: core/auth-token-interceptor.ts ```typescript return next.handle(reqClone).pipe(map((httpEvent) => { if (httpEvent instanceof HttpResponse) { const httpResponse = httpEvent as HttpResponse<any>; const authToken = httpResponse.headers.get('auth-token'); CacheService.setAuthToken(authToken); } return httpEvent; }), tap(() => {}, () => {})); } ``` 当前需要对`网络错误`进行拦截并判断是否发生了401认证错误,所以在tap操作符中仅需要处理第二个回调函数即可: core/auth-token-interceptor.ts ```typescript }), tap(() => { }, (event: HttpErrorResponse➊) => { if (event.status === 401) { console.log('发生了401错误'); } })); } ``` * ➊ 当发生网络错误时tap操作符会将响应的信息做为回调函数的参数传入。 ![](https://img.kancloud.cn/84/76/8476f07580b764319c23da2db20e1e07_273x82.png) 最后在拦截器中注入TeacherService并调用setIsLogin方法来实现当后台发出401时前台自动完成注销的功能。 core/auth-token-interceptor.ts ```typescript constructor(private teacherService: TeacherService) { } ... }), tap(() => { }, (event: HttpErrorResponse) => { if (event.status === 401) { this.teacherService.setIsLogin(false); } })); } ``` 此时当后台重新启动后,无论前台进行任何的非登录操作均会因收到401状态码而导致前台自动完成注销操作。 # Chrome Chrome浏览器可能是出于安全的角度考虑在网络中默认关闭了`options`的请求。如果希望在Chrome的网络中查看`options`请求,则可以在Chrome打开 `chrome://flags/#out-of-blink-cors`,将`Out of blink CORS`项改为`disabled`,然后重新启动Chrome: ![](https://img.kancloud.cn/0c/f5/0cf598083135ee53640136d6e8f12d9c_811x100.png) 此时再次查看网络,将查看到`options`方法的请求信息: ![](https://img.kancloud.cn/fc/2d/fc2d4e0f9fa7bb307f7de7cf6a82b953_928x253.png) ## 单元测试 由于在auth-token-interceptor引入了`TeacherService`,对应修正单元测试如下: core/auth-token-interceptor.spec.ts ```typescript import { AuthTokenInterceptor } from './auth-token-interceptor'; import {async, TestBed} from '@angular/core/testing'; import {TeacherService} from '../service/teacher.service'; import {TeacherStubService} from '../test/service/teacher-stub.service'; describe('AuthTokenInterceptor', () => { beforeEach(async(() => { TestBed.configureTestingModule({ providers: [ {provide: TeacherService, useClass: TeacherStubService} ] }) .compileComponents(); })); it('should create an instance', () => { const teacherService: TeacherService = TestBed.get(TeacherService); ➊ expect(new AuthTokenInterceptor(teacherService)).toBeTruthy(); ➊ }); }); ``` * ➊ 无法使用`TestBed.get(AuthTokenInterceptor)`来直接获取`AuthTokenInterceptor`实例,你知道这是为什么吗? # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.10](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.10) | - | | tap操作符 | [https://rxjs-dev.firebaseapp.com/api/operators/tap](https://rxjs-dev.firebaseapp.com/api/operators/tap) | 10 |