啟動前后臺后進行單元測試以驗證攔截器是否生效,并修正在集成測試中發現的一些問題。
## CORS錯誤
點擊登錄按鈕后發現如下錯誤:
```
Access to XMLHttpRequest at 'http://localhost:8080/Teacher/login' 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.
```
該錯誤是個老生常談的問題,教程伊始便與該錯誤打過交道。它的原因是后臺沒有返回對應的`Access-Control-Allow-Origin`,解決的方法是對應添加跨域設置。而我們在上一個小節的攔截器環節中確認并沒有動跨域的任何設置,所以錯誤的方向還是應該由攔截器入手。
找到后臺的日志簡單瀏覽一下看是否能夠得到一些有幫助的信息,日志中有以下兩條:
```
請求的地址為/Teacher/login請求的方法為:OPTIONS?
當前token未綁定登錄用戶,返回401
```
? 在前臺的代碼明明使用的是`this.httpClient.post`方法,為何在用戶登錄時后臺會接收到options請求呢?。這是由于瀏覽器在進行跨域訪問時,如果發現請求的方法不是`get`,那么在請求以前則會向該請求地址(此時為/Teacher/login)發送`options`方法來確認后臺允許前臺發起的請求方法。仍然以登錄為例:當后臺返回的允許請求方法中包括了 `POST`方法時,瀏覽器才會向`/Teacher/login`進行`post`請求,否則將放棄請求。
所以才有了在教程開始時的這段配置代碼:
config/WebConfig.java
```java
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("PUT", "DELETE", "POST", "GET", "PATCH")
.exposedHeaders("auth-token");
}
```
上述的代碼的是在說:當由`http://localhost:4200`發起對本系統任意地址(`/**`)的訪問請求,允許發起"PUT", "DELETE", "POST", "GET", "PATCH"5種請求方法,同時允許前臺獲取`header`中的"auth-token"。
但由于在整體訪問流程中,攔截器早于此處代碼段執行,所以還未執行到此代碼段的生效位置,就被攔截器截胡了。解決的方法是:在攔截中獲取請求方法為`options`中直接放行:
interceptor/AuthInterceptor.java
```java
System.out.println("請求的地址為" + url + "請求的方法為:" + method);
if( "OPTIONS".equals(method)) {
// 請求方法為OPTIONS,不攔截
return true;
}
// 判斷請求地址、方法是否與用戶登錄相同
```
重新啟動后臺,繼續測試。
## 刷新錯誤
使用用戶名密碼登錄系統后,在任意界面進行刷新都將在控制臺發生如下網絡錯誤:

點擊任意錯誤請求后,點擊響應header上的 view source

發現錯誤的類型均為401

這是由于在進行頁面刷新時前臺用于存儲auth-token的CacheService重新進行了初始化。而在初始化的過程中,將auth-token重置為undefined的原因:
service/cache.service.ts
```typescript
export class CacheService {
/** 認證令牌 */
private static authToken: string = undefined; ?
```
* ? 刷新前臺時authToken被重置為undefined
這個問題與前面碰到的由于未對登錄狀態進行緩存,從而導致每次刷新瀏覽器都要重新登錄一次的原因是一樣的。解決的方法也一樣:使用瀏覽器提供的緩存來存儲auth-token,以保證用戶在進行瀏覽器刷新時能夠保持auth-token不變:
service/cache.service.ts
```typescript
private static authToken: string = undefined; ?
private static authToken: string =
sessionStorage.getItem('authToken') === null ? undefined : sessionStorage.getItem('authToken'); ?
constructor() {
}
static setAuthToken(token: string) {
CacheService.authToken = token;
sessionStorage.setItem('authToken', token); ?
}
```
* ? 使用sessionStorage存儲的值設置authToken
* ? 更新sessionStorage存儲
sessionStorage獲取某個不存在項時返回了null,這與CacheService的authToken的默認值為undefined不同。所以在初始化時,需要使用比目運算符進行轉換。如果將CacheService的authToken的默認值同樣設置為null。代碼還會精簡一些:
service/cache.service.ts
```typescript
private static authToken: string = ?
sessionStorage.getItem('authToken') === null ? undefined : sessionStorage.getItem('authToken'); ?
private static authToken: string = sessionStorage.getItem('authToken');
...
static getAuthToken() {
if (CacheService.authToken === undefined) { ? ?
if (CacheService.authToken === null) {
return ''; ?
}
return CacheService.authToken;
}
```
* ? 由于angular在在處理header的過程中遇到值為undefined時會報異常,所以當authToken的值為undefined時對應返回`''`?以規避以異常。
**注意:** 你此時需要參考下圖清下緩存

## logout
最后再修正下這個看不到的注銷。當前的注銷功能并未調用后臺對應的logout接口。這將導致用戶注銷后實質上為后臺為當前窗口分配的auth-token仍然是生效的。這增加了系統數據被滲透的風險。用戶點擊注銷時只有真正的觸發后臺的注銷接口,才會起到auth-token與用戶的解綁作用。
為此,為service/teacher.service.ts新增logout方法如下:
service/teacher.service.ts
```typescript
/**
* 注銷
*/
logout(): Observable<void> {
const url = 'http://localhost:8080/Teacher/logout';
return this.httpClient.get<void>(url);
}
```
測試過程略。
<hr>
在C層的注銷方法中調用logout方法:
nav/nav.component.ts
```typescript
onLogout() {
this.teacherService.logout()
.subscribe(() => {
this.teacherService.setIsLogin(false);
});
}
```
修正單元測試如下:
nav/nav.component.spec.ts
```typescript
fit('onLogout', () => {
const service = TestBed.get(TeacherService) as TeacherService;
spyOn(service, 'setIsLogin');
spyOn(service, 'logout').and.returnValue(of(null)); ?
component.onLogout();
expect(service.logout).toHaveBeenCalled(); ?
expect(service.setIsLogin).toHaveBeenCalledWith(false);
});
```
* ? 設置logout方法的替身,并指定替身的返回值
* ? 斷言方法被調用
測試結果:

這是由于沒有為TeacherService的測試替身TeacherStubService同步添加logout的原因所致
test/service/teacher-stub.service.ts
```typescript
logout(): Observable<void> {
return of(null);
}
```
再次運行單元測試通過。
# 測試結果

# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.9](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.9) | - |
| spring-mvc-handlerinterceptor | [https://www.baeldung.com/spring-mvc-handlerinterceptor](https://www.baeldung.com/spring-mvc-handlerinterceptor) | - |
| HttpServletRequest | [https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html) | - |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用