经历了几次集成测试,你此时肯定被莫名的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 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用