# 對接后臺
本節我們嘗試完成個人中心后臺的對接。
## 接口信息
我們為大家提供了如下接口來獲取當前登錄用戶的基本信息:
```bash
GET /teacher/me
```
#### 參數 Parameters
| type | name | Description | Schema |
| ---------- | ------------------------------- | ------------ | ---------------- |
| **Header** | **認證x-auth-token** *requried* | 獲取登錄信息 | 當前登錄教師實體 |
#### 返回值 Responses
| HTTP Code | Description | Schema |
| --------- | ------------------------------------------------------ | ------ |
| **200** | 認證x-auth-token合法 | Ok |
| **401** | 未攜帶x-auth-token信息,未攜帶的x-auth-token信息不正確 | 未授權 |
接口中提到了x-auth-token信息,我們暫時先不考慮它,看看按以前請求思想會發生什么。
## HttpClient
```typescript
+++ b/first-app/src/app/personal-center/personal-center.component.ts
@@ -1,5 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {Teacher} from '../entity/teacher';
+import {HttpClient} from '@angular/common/http';
@Component({
selector: 'app-personal-center',
@@ -9,18 +10,14 @@ import {Teacher} from '../entity/teacher';
export class PersonalCenterComponent implements OnInit {
me = {} as Teacher;
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
ngOnInit(): void {
- this.me = new Teacher(
- 1,
- 'zhangsan@yunzhi.club',
- '張三',
- 'password',
- null as unknown as boolean,
- 'zhangsan'
- );
+ const url = 'http://angular.api.codedemo.club:81/teacher/me';
+ this.httpClient.get<Teacher>(url)
+ .subscribe(teacher => this.me = teacher,
+ error => console.log('請求發生錯誤', error));
}
}
```
單元測試中的動態測試模塊:
```typescript
+++ b/first-app/src/app/personal-center/personal-center.component.spec.ts
@@ -2,6 +2,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PersonalCenterComponent} from './personal-center.component';
import {SexPipe} from './sex.pipe';
+import {HttpClientModule} from '@angular/common/http';
describe('PersonalCenterComponent', () => {
let component: PersonalCenterComponent;
@@ -9,7 +10,8 @@ describe('PersonalCenterComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [PersonalCenterComponent, SexPipe]
+ declarations: [PersonalCenterComponent, SexPipe],
+ imports: [HttpClientModule]
})
.compileComponents();
});
@@ -20,7 +22,7 @@ describe('PersonalCenterComponent', () => {
fixture.detectChanges();
});
- it('should create', () => {
+ fit('should create', () => {
expect(component).toBeTruthy();
fixture.autoDetectChanges();
});
```
測試結果:

## 登錄
與前面章節中對教師的操作不同,后臺獲取當前登錄教師的接口必須在**用戶登錄成功**后,才能夠獲取到。也就是說只有登錄的用戶才有資格訪問該接口。
那么如果在請求該接口前先完成用戶的登錄,是否可以請求成功呢?心動不如行動,我們參考登錄組件簡單寫下代碼:
```typescript
+++ b/first-app/src/app/personal-center/personal-center.component.ts
@@ -1,5 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {Teacher} from '../entity/teacher';
+import {HttpClient, HttpHeaders} from '@angular/common/http';
@Component({
selector: 'app-personal-center',
@@ -9,18 +10,29 @@ import {Teacher} from '../entity/teacher';
export class PersonalCenterComponent implements OnInit {
me = {} as Teacher;
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
ngOnInit(): void {
- this.me = new Teacher(
- 1,
- 'zhangsan@yunzhi.club',
- '張三',
- 'password',
- null as unknown as boolean,
- 'zhangsan'
- );
- }
+ const authString = 'zhangsan:codedemo.club';
+ const authToken = btoa(authString);
+ let httpHeaders = new HttpHeaders();
+ httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);
+ this.httpClient
+ .get<Teacher>(
+ 'http://angular.api.codedemo.club:81/teacher/login',
+ {headers: httpHeaders})
+ .subscribe(() => {
+ console.log('登錄成功,接著嘗試獲取當前登錄用戶');
+ const url = 'http://angular.api.codedemo.club:81/teacher/me';
+ this.httpClient.get<Teacher>(url)
+ .subscribe(teacher => {
+ console.log('請求當前登錄用戶成功');
+ this.me = teacher;
+ },
+ error => console.log('請求當前登錄用戶發生錯誤', error));
+ },
+ error => console.log('發生錯誤, 登錄失敗', error));
+ }
}
```

> 注意:此時的用戶名可能已經不是`zhangsan`,請參考登錄一節查看當前可用的用戶名。
可見:即使是在請求當前用戶前發起登錄請求,在登錄成功的情況下仍然沒有被后臺認為是**已認證用戶**。
## 再學COOKIE
而值得一提的是:在前后臺統一的項目中,先完成用戶登錄再進行請求,是可以請求成功的。為什么會發生這種情況呢?
在前后臺地址相同的情況下,瀏覽器會自動處理用于認為的cookie信息。包括:將后臺返回的cookie信息緩存到瀏覽器中,以及再次請求中自動在header中攜帶當前已緩存的cookie信息。
比如我們在瀏覽器中直接訪問 [http://angular.api.codedemo.club:81](http://angular.api.codedemo.club:81),則會在請求頭中發現如下信息(如果沒有,就再次訪問一次):

該信息則由瀏覽器自動處理。
但如果我們在當前項目中向樣的地址發起請求,則會發現請求頭中并沒有攜帶cookie信息,反而增加了Origin信息:

實際上,即使是我們使用類似于登錄時的代碼,手動的在header中添加cookie信息,瀏覽器在發起請求時也會將此信息刪除。
```typescript
+++ b/first-app/src/app/personal-center/personal-center.component.ts
@@ -18,6 +18,7 @@ export class PersonalCenterComponent implements OnInit {
const authToken = btoa(authString);
let httpHeaders = new HttpHeaders();
httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);
+ httpHeaders = httpHeaders.append('Cookie', 'test cookie');
this.httpClient
.get<Teacher>(
```

這是瀏覽器出于保護普通的用戶,在安全方面做的考慮。
瀏覽器將這種在`http://localhost:4200`下請求`http://angular.api.codedemo.club:81`的請求稱為**跨域**請求。也就是說,瀏覽器將`http://localhost:4200`與`http://angular.api.codedemo.club:81`認為是兩個**域**。瀏覽器進行跨域判斷時,將對以下三個因素進行判斷:
1. 協議是否相同。比如在`http://www.codedemo.club`與`https://www.codedemo.club`的協議一個是`http`,一個是`https`,則認為協議不同。將被識別為跨域。
2. 域名是否相同。比如https://www.baidu.com與http://www.google.com,由于域名分別是`www.baidu.com`及`www.google.com`,所以認為域名不同。將被識別為跨域。
3. 端口號是否相同。瀏覽器發起請求時,會默認省略http協議的80端口,以及https協議的443端口。所以訪問:http://www.baidu.com,則相當于訪問:http://www.baidu.com:80;訪問https://www.baidu.com,則相當于訪問:https://www.baidu.com:443。除此以外,我們還可以為其指定特定的端口,比如我們的后臺地址為:`http://angular.api.codedemo.club:81`。該地址與`http://angular.api.codedemo.club`,一個端口為81,另一個為默認的80,不同。將被識別為跨域。
瀏覽器在發起請求時,如果發現請求的地址與當前訪問的地址在協議、域名以及端口號上有任意一點不同的時候,則將此次請求識別為跨域請求。
跨域更像是電影中的諜戰片,瀏覽器發現有人向外部傳遞信息時,自動的過濾掉被其認為是敏感信息的Cookie,以達到身份認證信息不被壞人獲取的目的(其實目的不是這樣的,但這不重要,只要知道Cookie是敏感信息,瀏覽器過濾掉了就可以)。
瀏覽器過濾掉了原來自動攜帶的用于認證的Cookie,這使得在跨域請求中,必須采取其的認證方式。
## 域
什么是域呢?如果你對網絡知識感興趣,或是嘗試建立過個人站點,那么對它肯定不陌生。在建立個人站點時,重要的一環就是購買域名,比如`codedemo.club`就是一個域名。
域為domain的譯文,原意為`范圍、領土`,簡單來說就是用它來表示一定的范圍。在現實生活中,我們接觸了很多域。比如國家是一個域、省是一個域、市縣等都是一個域。有了域,我們就能快速的根據域來找現實生活中的事物通訊,比如由于河北工業大學座落天津市這個域,所以我們在與河北工業大學通訊時,可以先指定其域名**天津**,可見域就是指范圍。當前我們的現實世界里有個頂級的大域是宇宙,然后第二個域是地球,接下來的域有國家、省、市等。
在計算機的網絡世界里同樣也是如此,網絡里也有根域名、二級域名、三級域名:

每個域名下都可以有很多臺計算機,每臺計算機在當前域下都有一個唯一的名字,我們把域名下計算機的名字稱作主機名。在進行http請求時,實際上是域中的某個計算機發起請求,所以在請求時要指名這個主機名。
### 域名
知道了什么是域,域名就簡單了。這就像是張三是張三的名字,李四是李四的名字,河北工業大學是河北工業大學的名字一樣。為了區分不同的域,就需要對不同的域起名字。所以域名就是域的名字。
## CORS
所以瀏覽器認為的跨域的請求,實際上是考慮了域、協議以及端口號。即使在同一個域下,如果協議或是端口號不同,也么被瀏覽器認為是跨域訪問。我們把域、協議以及端口號也稱為**同源**的三要素,即如果兩個請求的域、協議以及端口號均相同時,則認為兩個請求是同源的,而非同源則被認為是跨域的。所以,實際上**跨域**這個專有名詞是不確切、不足以正確的表達其中的含義的,正確的專有名詞應該是**跨源**,全稱為**跨源資源共享**,對應英文原文為:Cross-Origin Resource Sharing,稱寫為`CORS`。
而在前后臺分離的項目中,前臺與后臺的分離架構導致了前后臺必然不同源,而不同源時瀏覽器必然不攜帶cookie信息,也就無法實現**自動認證**。所以在CORS中,我們一般使用其它的認證方法。在下個小節中,我們將對其中的一種進行介紹。
| 名稱 | 地址 | |
| ---------------------- | ------------------------------------------------------------ | ---- |
| 跨域資源共享 CORS 詳解 | [https://www.ruanyifeng.com/blog/2016/04/cors.html](https://www.ruanyifeng.com/blog/2016/04/cors.html) | |
| 跨源資源共享(CORS) | [https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS) | |
| 域名 | [https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D](https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D) | |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step4.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step4.3.zip) | |
- 序言
- 第一章 Hello World
- 1.1 環境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請求后臺數據
- 2.2.1 HttpClient
- 2.2.2 請求數據
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調函數
- 2.2.5 集成測試
- 2.2.6 本章小節
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數
- 2.4.3 插值與模板表達式
- 2.4.4 初識泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標庫
- 2.6.3 firefox
- 2.7 總結
- 第三章 用戶登錄
- 3.1 初識單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結
- 第四章 個人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺
- 4.4 x-auth-token認證
- 4.5 攔截器
- 4.6 小結
- 第五章 系統菜單
- 5.1 延遲及測試
- 5.2 手動創建組件
- 5.3 隱藏測試信息
- 5.4 規劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數據驗證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構
- 6.1.8 小結
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應用組件
- 6.2.5 小結
- 6.3 班級列表
- 6.3.1 原型設計
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態分頁
- 6.3.5 動態分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應式表單驗證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構
- 6.4.7 小結
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉
- 6.6.4 ngStyle
- 6.7 初識Service
- 6.7.1 catchError
- 6.7.2 單例服務
- 6.7.3 單元測試
- 6.8 小結
- 第七章 學生管理
- 7.1 班級列表組件
- 7.2 新增學生
- 7.2.1 exports
- 7.2.2 自定義驗證器
- 7.2.3 異步驗證器
- 7.2.4 再識DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結
- 7.3 單元測試進階
- 7.4 學生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構分頁
- 7.5 刪除學生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向對象
- 7.6 集成測試
- 7.7 編輯學生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數
- 7.7.6 小結
- 7.8 總結
- 第八章 其它
- 8.1 打包構建
- 8.2 發布部署
- 第九章 總結