# Basic access authentication
用戶登錄在術(shù)語(yǔ)中更多的稱為用戶認(rèn)證,英文單詞以authentication為關(guān)鍵字,也常常被簡(jiǎn)寫為auth。認(rèn)證的方式有很多種,比如我們??吹降挠脩裘艽a認(rèn)證、手機(jī)號(hào)驗(yàn)證碼認(rèn)證、使用微信支付寶等第三方快捷認(rèn)證等。在此,我們僅講述用戶名密碼的認(rèn)證方式。
認(rèn)證的過(guò)程也可以有很多種,比如我們歷史上曾經(jīng)學(xué)習(xí)過(guò)將用戶名、密碼做為表單數(shù)據(jù),以post方式發(fā)送給過(guò)去,繼而完成用戶認(rèn)證。今天我們學(xué)習(xí)的是另一種更加通用的認(rèn)證方式:`Basic access authentication`,有時(shí)也被簡(jiǎn)稱為`Basic Auth`。
## Basic Auth
Basic Auth,顧名思義其為一種基本的認(rèn)證模式,它也是最常用的HTTP認(rèn)證方案。它的基本認(rèn)證邏輯是:將認(rèn)證信息放到Http請(qǐng)求的Header部分。
以用戶名為`zhangsan`密碼為`yunzhi.club`為例,使用Basic Auth認(rèn)證流程如下:
1. 將用戶名密碼與`:`相連,接拼為字符串`zhangsan:yunzhi.club`。
2. 使用base64進(jìn)行加密 `base64(zhangsan:yunzhi.club)`,加密結(jié)果為`emhhbmdzYW46eXVuemhpLmNsdWI=`。
3. 在http請(qǐng)求中的headers中增加以下項(xiàng):`Authorization: Basic emhhbmdzYW46eXVuemhpLmNsdWI=`
4. 向后臺(tái)發(fā)起請(qǐng)求
此時(shí),用戶名密碼便成功的通過(guò)headers以Basic Auth的模式發(fā)送給了后臺(tái)。
> 除最常用的Basic認(rèn)證外,還有**Bearer**、**Digest**、**HOBA**等認(rèn)證模式。
## 后臺(tái)接口
后臺(tái)為我們提供了專用的認(rèn)證地址(實(shí)際上并不拘泥于此),接口信息如下:
```bash
GET /teacher/login
```
認(rèn)證模式:Basic。認(rèn)證失敗將返回狀態(tài)碼401,認(rèn)證成功將返回用戶名密碼對(duì)應(yīng)的教師數(shù)據(jù)。
## 發(fā)起認(rèn)證
我們來(lái)到login組件的`onSubmit`方法,按Basic Auth的步驟逐步完成代碼。
### 自動(dòng)化
按前面學(xué)習(xí)的方法,我們可以利用`ng t`來(lái)啟動(dòng)組件測(cè)試,接著點(diǎn)擊登錄中的登錄按扭,以達(dá)到調(diào)用`onSubmit`的方法。其我們還可以借助單元測(cè)試的思想,寫一些自動(dòng)化的代碼,這樣當(dāng)我們每次改動(dòng)代碼并按`ctrl + s`保存文件后,這些代碼便會(huì)自動(dòng)執(zhí)行。在這些自動(dòng)執(zhí)行的代碼中實(shí)現(xiàn)**調(diào)用onSubmit**的方法。
是的,我們完全可以參考第一節(jié)的內(nèi)容,使用模塊點(diǎn)擊V層按鈕的方法。除此以外,我們還可以在單元測(cè)試代碼直接調(diào)用組件的方法。為此,我們?cè)黾尤缦麓a以協(xié)助開發(fā)用戶登錄。
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -39,4 +39,11 @@ fdescribe('LoginComponent', () => {
// 點(diǎn)擊按鈕以后,onSubmit方法應(yīng)該被調(diào)用了1次。
expect(component.onSubmit).toHaveBeenCalledTimes(1);
});
+
+ it('onSubmit 用戶登錄', () => {
+ // 啟動(dòng)自動(dòng)變更檢測(cè)
+ fixture.autoDetectChanges();
+
+ component.onSubmit();
+ });
});
```
使用`ng t`啟動(dòng),將自動(dòng)執(zhí)行本方法:

如果想僅僅執(zhí)行當(dāng)前方法,則可以在`it`前面加入`f`:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -40,7 +40,7 @@ fdescribe('LoginComponent', () => {
expect(component.onSubmit).toHaveBeenCalledTimes(1);
});
- it('onSubmit 用戶登錄', () => {
+ fit('onSubmit 用戶登錄', () => {
// 啟動(dòng)自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
```
此時(shí),單元測(cè)試則將僅僅執(zhí)行當(dāng)前方法:

控制臺(tái)日志如下:

### 接拼認(rèn)證信息
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -19,5 +19,7 @@ export class LoginComponent implements OnInit {
onSubmit(): void {
console.log('點(diǎn)擊了登錄按鈕');
+ const authString = this.teacher.username + ':' + this.teacher.password;
+ console.log(authString);
}??
}
```
控制臺(tái)信息如下:

由于初始化的teacher并不存在用戶名密碼信息,所以最終在控制臺(tái)打印了`undefined:undefined`。為此,在單元測(cè)試代碼中,我們?yōu)閌teacher`設(shè)置一個(gè)用戶名、密碼:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -43,7 +43,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動(dòng)自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
-
+ component.teacher = {username: 'zhangsan', password: 'codedemo.club'};
component.onSubmit();
});
});
```

### Base64加密
Base64是眾多加密算法中的一種,最近也被廣泛地用于在瀏覽器中顯示圖片。比如你可以將以下代碼粘貼到html文件中,在對(duì)應(yīng)的位置上將顯示一張圖片:
````
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE4NHB4IiBoZWlnaHQ9IjIwMHB4IiB2aWV3Qm94PSIwIDAgMTg0IDIwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDMuMi4yICg5OTgzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5zaGllbGQtbGFyZ2U8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8ZyBpZD0ic2hpZWxkLWxhcmdlIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIj4KICAgICAgICAgICAgPGcgaWQ9IkltcG9ydGVkLUxheWVycyIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC4wMDAwMDAsIDExLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTgzLjIsMS4wMzI0OTI3NSBMODMuMiwxLjAyODYwMTQ1IEwwLjMwNjgsMzAuNDQ4MTU5NCBMMTIuODk4NiwxMzkuNjczMTgxIEw4Mi45OTk4LDE3OC40NTAwMjkgTDE1NC4wOTgxLDEzOS4xNTA0NDkgTDE2Ny45NTg3LDI5LjkyNjcyNDYgTDgzLjIsMS4wMzI0OTI3NSIgaWQ9IkZpbGwtMSIgZmlsbD0iI0UyMzIzNyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2Ny44Mzc4LDI5LjkyNjcyNDYgTDgyLjk3MTIsMS4wMzI0OTI3NSBMODIuOTcxMiwxNzguNDUwMDI5IEwxNTQuMDgzOCwxMzkuMTUwNDQ5IEwxNjcuODM3OCwyOS45MjY3MjQ2IEwxNjcuODM3OCwyOS45MjY3MjQ2IFoiIGlkPSJGaWxsLTIiIGZpbGw9IiNCNTJFMzEiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLjE5MjQsMzAuNDQ4MTU5NCBMMTIuODQxNCwxMzkuNjczMTgxIEw4Mi45NzEyLDE3OC40NTAwMjkgTDgyLjk3MTIsMS4wMjg2MDE0NSBMMC4xOTI0LDMwLjQ0ODE1OTQgTDAuMTkyNCwzMC40NDgxNTk0IFoiIGlkPSJGaWxsLTMiIGZpbGw9IiNFMjMyMzciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMDAuNjgzNyw5NC4yMjY2Mzc3IEw4Mi45NzEyLDU3LjcwNTQ0OTMgTDY3LjcyNjEsOTQuMjI2NjM3NyBMMTAwLjY4MzcsOTQuMjI2NjM3NyBMMTAwLjY4MzcsOTQuMjI2NjM3NyBaIE0xMDcuMzY3LDEwOS41ODMwMjIgTDYwLjkwNSwxMDkuNTgzMDIyIEw1MC41MTE1LDEzNS41MjM3NTQgTDMxLjE3NzksMTM1Ljg3OTE1OSBMODIuOTcxMiwyMC44MTA2OTU3IEwxMzYuNjY2NCwxMzUuODc5MTU5IEwxMTguNzQ1OSwxMzUuODc5MTU5IEwxMDcuMzY3LDEwOS41ODMwMjIgTDEwNy4zNjcsMTA5LjU4MzAyMiBaIiBpZD0iRmlsbC00IiBmaWxsPSIjRkZGRkZGIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==">
````
上面的圖片`src`的部分以`data:image/svg+xml;base64`打頭,即表示使用了base64算法。簡(jiǎn)單來(lái)講,base64算法一種在加密時(shí)將二進(jìn)制串轉(zhuǎn)換為ASCII字符串(實(shí)際上只選取了部分ASCII),在解密時(shí)再將ASCII字符串轉(zhuǎn)換為二制進(jìn)的加密解密算法。由于http中的header部分只能夠攜帶ASCII編碼的字符串,所以在沒(méi)有base64算法轉(zhuǎn)換之前。將用戶名、密碼信息放到header中傳遞,則僅支持英文字符;在base64的幫助下,可以將用戶名、密碼轉(zhuǎn)換為ASCII字符串,近而可以做為header數(shù)據(jù)項(xiàng)中傳遞。
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -21,5 +21,7 @@ export class LoginComponent implements OnInit {
console.log('點(diǎn)擊了登錄按鈕');
const authString = this.teacher.username + ':' + this.teacher.password;
console.log(authString);
+ const authToken = btoa(authString); ????
+ console.log(authToken);
}
}
```
TypeScript提供了`btoa`函數(shù)來(lái)快捷的完成加密操作 ????。

### 請(qǐng)求Header
Angular提供了傳用的HttpHeaders用于構(gòu)建請(qǐng)求的header信息:
```typescript
+++ b/first-app/src/app/login/login.component.ts
-import {HttpHeaders} from '@angular/common/http';
+import {HttpClient, HttpHeaders} from '@angular/common/http';
@@ -24,5 +24,6 @@ export class LoginComponent implements OnInit {
console.log(authString);
const authToken = btoa(authString);
console.log(authToken);
+ let httpHeaders = new HttpHeaders();
+ httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken); ??
}
}
注意是`Basic `不是`Basic`, 前一個(gè)存在空格 ??
```
### 發(fā)起請(qǐng)求
然后便可以在httpClient的任意方法中加入此header請(qǐng)求信息:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -1,5 +1,5 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-login',
@@ -12,7 +12,7 @@ export class LoginComponent implements OnInit {
password: string
};
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
ngOnInit(): void {
@@ -26,5 +26,12 @@ export class LoginComponent implements OnInit {
console.log(authToken);
let httpHeaders = new HttpHeaders();
httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);
+
+ this.httpClient
+ .get(
+ 'http://angular.api.codedemo.club:81/teacher/login',
+ {headers: httpHeaders})
+ .subscribe(teacher => console.log(teacher),
+ error => console.log('發(fā)生錯(cuò)誤, 登錄失敗', error));
}
}
```
此時(shí)單元測(cè)試中將觸發(fā)一個(gè)錯(cuò)誤,相信你現(xiàn)在有足夠的能力把它解決掉,解決以后控制臺(tái)將打印以下信息:

除使用`get`方法外,還可以使用`put`、`post`、`delete`等請(qǐng)求方式,比如:
```typescript
this.httpClient.post(url, {}, {headers: httpHeaders})
```
**注意:**我們的后臺(tái)每日將清空一次數(shù)據(jù),對(duì)所有的成員開放后臺(tái)API,這意味著當(dāng)前正在有其它的學(xué)員進(jìn)行教師編輯功能的練習(xí)。這會(huì)使得用戶名`zhangsan`處于失效狀態(tài)(比如有學(xué)員將zhangsan改成了zhangsanfeng)。你可以在瀏覽器中直接訪問(wèn)[http://angular.api.codedemo.club:81/teacher](http://angular.api.codedemo.club:81/teacher)來(lái)獲取當(dāng)前有效的用戶名信息,系統(tǒng)默認(rèn)用戶的密碼均為`codedemo.club` 。
## 充分的測(cè)試
一個(gè)優(yōu)秀的項(xiàng)目離不開充分的測(cè)試,測(cè)試是保障軟件質(zhì)量最重要的一環(huán),沒(méi)有之一。在測(cè)試中,我們需要充分的站在用戶的角度,根據(jù)自己的經(jīng)驗(yàn)努力思索用戶在實(shí)際使用過(guò)程中可能會(huì)出現(xiàn)的情景,然后一一把它們模擬出來(lái)。
### 用戶名密碼錯(cuò)誤
前面我們僅驗(yàn)證了用戶名、密碼正確的情況。在實(shí)際的使用過(guò)程中顯然這是不夠的。而用戶名、密碼錯(cuò)誤時(shí)是否是按我們的預(yù)期發(fā)起的呢?與其猜、想、看、盯,不如實(shí)際用代碼測(cè)試一下:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動(dòng)自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
- component.teacher = {username: 'zhangsan', password: 'codedemo.club'};
+ component.teacher = {username: 'notzhangsan', password: 'codedemo.club'};
component.onSubmit();
});
});
```

再驗(yàn)證一下密碼錯(cuò)誤的情況:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟用自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
- component.teacher = {username: 'notzhangsan', password: 'codedemo.club'};
+ component.teacher = {username: 'zhangsan', password: 'password'};
component.onSubmit();
});
});
```

### 中文用戶名密碼
雖然我個(gè)人沒(méi)有將中文做為用戶名密碼的習(xí)慣,但是部分用戶的確有這個(gè)需求,那么我們當(dāng)前代碼是否能夠很好的處理這種情況呢?
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動(dòng)自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
- component.teacher = {username: 'zhangsan', password: 'password'};
+ component.teacher = {username: '中文用戶名', password: 'codedemo.club'};
component.onSubmit();
});
});
```

我們得到了一個(gè)錯(cuò)誤,該錯(cuò)誤表明當(dāng)前代碼在處理中文用戶名時(shí)會(huì)發(fā)生異常。那么處理中文密碼是否會(huì)發(fā)生異常呢,請(qǐng)先給出自己的答案后驗(yàn)證。
當(dāng)前控制臺(tái)信息如下:

由以上信息我們能夠得出,上述異常發(fā)生在`btoa()`方法上,用以下關(guān)鍵字來(lái)搜索問(wèn)題,我們可以快速的找到問(wèn)題的原因及解決方案:

搜索結(jié)果為我們指引到了[https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings](https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings)一文,該文的回答又為我們指引到了權(quán)威的[https://developer.mozilla.org/en-US/docs/Glossary/Base64](https://developer.mozilla.org/en-US/docs/Glossary/Base64)在此文章中,有一行note是這么說(shuō)的:
```
Note that btoa() expects to be passed binary data, and will throw an exception if the given string contains any characters whose UTF-16 representation occupies more than one byte. For more details, see the documentation for btoa().
```
上面大概是說(shuō):
```
注意btoa()方法只能傳入二進(jìn)制數(shù)據(jù),如果傳入的參數(shù)中包含任何UTF-16的大于1個(gè)字節(jié)的字符串,將會(huì)觸發(fā)異常。
```
我們接著點(diǎn)擊函數(shù)名,查看詳情[https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa),該文中又有如下的描述:
```
The btoa() function takes a JavaScript string as a parameter. In JavaScript strings are represented using the UTF-16 character encoding: in this encoding, strings are represented as a sequence of 16-bit (2 byte) units. Every ASCII character fits into the first byte of one of these units, but many other characters don't.
```
簡(jiǎn)單翻譯下我們大概明白了,原來(lái)btoa只能接收以1個(gè)字節(jié)的字符組成的字符串。而JavaScript的string是用UTF-16來(lái)編碼的,該編碼占用了2個(gè)字節(jié)。每個(gè)ASCII編碼的字符都可以用首單元的字節(jié)來(lái)代碼,但是其它的字符就不是了(言外之意,其它字符就是2個(gè)字節(jié)了)。
這就需要我們?cè)贑語(yǔ)言、數(shù)據(jù)結(jié)構(gòu)、計(jì)算機(jī)組成原理等基礎(chǔ)課程中學(xué)習(xí)過(guò)的ASCII編碼了。ASCII編碼中,0 - 127分別代表一個(gè)字符,共128個(gè)。占用了一個(gè)字節(jié)的后7位,為:`0000 0000` 至 `0111 1111`。所以每個(gè)ASCII編碼的字符,必然可以用一個(gè)字節(jié)來(lái)表示。
在[WindowOrWorkerGlobalScope.btoa()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa)一文中,我們還可以找到相應(yīng)測(cè)試的示例代碼:
```typescript
const ok = "a";
console.log(ok.codePointAt(0).toString(16)); // 61: occupies < 1 byte
const notOK = "?"
console.log(notOK.codePointAt(0).toString(16)); // 2713: occupies > 1 byte
console.log(btoa(ok)); // YQ==
console.log(btoa(notOK)); // error
```
我們當(dāng)然也可以用中文來(lái)做下實(shí)驗(yàn):

如上所示`zhangsan`中的首字母`z`,轉(zhuǎn)換為10進(jìn)制后值為`122`,該值位于`0-255`之間,占用一個(gè)字節(jié)。當(dāng)然了,實(shí)際上我們完全可以在ASSCI編碼表中找到字母`z`的編碼:

繼續(xù)測(cè)試中文的`張`:

上述代碼分別將`張`轉(zhuǎn)換為10 16 2進(jìn)制,我們能夠由16進(jìn)制的`5f20`快速的得出`張`占用了兩個(gè)字節(jié),實(shí)際上我們還可以在字符[編碼相關(guān)的站點(diǎn)](https://www.fileformat.info/info/unicode/char/5f20/index.htm)上來(lái)快速的找到`張`的utf編碼。

錯(cuò)誤的原因找到了,解決問(wèn)題的重點(diǎn)便在于如何將UTF-16中占2個(gè)字節(jié)的編碼變換為變1個(gè)字節(jié)的ASSCI。
### 單元大小為1字節(jié)的字符串
mozilla給出了如何將多字節(jié)字符組成的字符串變?yōu)?個(gè)字符組成的字符串的方案:
```javascript
// convert a Unicode string to a string in which
// each 16-bit unit occupies only one byte
function toBinary(string) {
const codeUnits = new Uint16Array(string.length);
for (let i = 0; i < codeUnits.length; i++) {
codeUnits[i] = string.charCodeAt(i);
}
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
}
```
參數(shù)上述代碼,建立轉(zhuǎn)換方法如下:
### encodeURI
既然已經(jīng)擴(kuò)展到此程度了,我們不防再多擴(kuò)展一下。其實(shí)我們?cè)缭绲木徒佑|到了這種將非ASSCI編碼轉(zhuǎn)換為ASSCI編碼的方案。以我們用的百度翻譯(類似的例子有很多,基本上涉及到查詢都會(huì)有)為例:

請(qǐng)跟隨教程打開翻譯,然后查詢一個(gè)`你好`,請(qǐng)注意當(dāng)前的URL。接下來(lái),我們復(fù)制這個(gè)URL,然后再粘貼到任意的位置,你將得到如下鏈接:
```
https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD
```
如果你在瀏覽器中打開[https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD](https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD),同樣可以正常訪問(wèn)顯示為你好。
將這個(gè)`你好`變更為`%E4%BD%A0%E5%A5%BD`的過(guò)程稱為`encodeURI`,表示對(duì)URI進(jìn)行編碼。目的是適用于http協(xié)議中非主體部分只支持ASSCI編碼的規(guī)則。`encodeURIComponent`函數(shù)則可以實(shí)現(xiàn)此功能。
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -20,7 +20,7 @@ export class LoginComponent implements OnInit {
onSubmit(): void {
console.log('點(diǎn)擊了登錄按鈕');
- const authString = this.teacher.username + ':' + this.teacher.password;
+ const authString = encodeURIComponent(this.teacher.username) + ':' + this.teacher.password;
console.log(authString);
const authToken = btoa(authString);
console.log(authToken);
```

## 本節(jié)作業(yè)
1. 一個(gè)項(xiàng)目前后臺(tái)是統(tǒng)一的整體,我們剛剛在傳送用戶名密碼時(shí)增加了encodeURI轉(zhuǎn)碼,那么后臺(tái)是否也支持這種方式呢?請(qǐng)新創(chuàng)建一個(gè)新教師并使用`codedemo.club`做為用戶名嘗試登錄。
2. 如果我們想使密碼也支持中文的話該怎么辦呢?
3. 我們往往怕的是修改好了一個(gè)新功能,同時(shí)卻改壞了兩個(gè)老功能。中文用戶名的問(wèn)題解決了,那么是否還支持英文登錄呢?請(qǐng)測(cè)試。
4. 請(qǐng)思索:在團(tuán)隊(duì)開發(fā)中,如何保證你已有的功能不被其它團(tuán)隊(duì)成員誤殺。
| 名稱 | 地址 | 備注 |
| ---------------------- | ------------------------------------------------------------ | ---- |
| Http身份認(rèn)證 | [https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication) | |
| RFC7617 Basic認(rèn)證 | [https://tools.ietf.org/html/rfc7617](https://tools.ietf.org/html/rfc7617) | |
| Base64的編碼與解碼 | [https://developer.mozilla.org/zh-CN/docs/Glossary/Base64](https://developer.mozilla.org/zh-CN/docs/Glossary/Base64) | |
| 一個(gè)查詢字符編碼的網(wǎng)站 | [https://www.fileformat.info/info/unicode/char/68a6/index.htm](https://www.fileformat.info/info/unicode/char/68a6/index.htm) | |
| ASCII | [https://zh.wikipedia.org/wiki/ASCII](https://zh.wikipedia.org/wiki/ASCII) | |
| 本節(jié)源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step3.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step3.3.zip) | |
- 序言
- 第一章 Hello World
- 1.1 環(huán)境安裝
- 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 請(qǐng)求后臺(tái)數(shù)據(jù)
- 2.2.1 HttpClient
- 2.2.2 請(qǐng)求數(shù)據(jù)
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調(diào)函數(shù)
- 2.2.5 集成測(cè)試
- 2.2.6 本章小節(jié)
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對(duì)接后臺(tái)
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數(shù)
- 2.4.3 插值與模板表達(dá)式
- 2.4.4 初識(shí)泛型
- 2.4.5 更新教師
- 2.4.6 測(cè)試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標(biāo)庫(kù)
- 2.6.3 firefox
- 2.7 總結(jié)
- 第三章 用戶登錄
- 3.1 初識(shí)單元測(cè)試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結(jié)
- 第四章 個(gè)人中心
- 4.1 原型
- 4.2 管道
- 4.3 對(duì)接后臺(tái)
- 4.4 x-auth-token認(rèn)證
- 4.5 攔截器
- 4.6 小結(jié)
- 第五章 系統(tǒng)菜單
- 5.1 延遲及測(cè)試
- 5.2 手動(dòng)創(chuàng)建組件
- 5.3 隱藏測(cè)試信息
- 5.4 規(guī)劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結(jié)
- 第六章 班級(jí)管理
- 6.1 新增班級(jí)
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級(jí)
- 6.1.3 ApiInterceptor
- 6.1.4 數(shù)據(jù)驗(yàn)證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構(gòu)
- 6.1.8 小結(jié)
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應(yīng)式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應(yīng)用組件
- 6.2.5 小結(jié)
- 6.3 班級(jí)列表
- 6.3.1 原型設(shè)計(jì)
- 6.3.2 初始化分頁(yè)
- 6.3.3 MockApi
- 6.3.4 靜態(tài)分頁(yè)
- 6.3.5 動(dòng)態(tài)分頁(yè)
- 6.3.6 @Input()
- 6.4 編輯班級(jí)
- 6.4.1 測(cè)試模塊
- 6.4.2 響應(yīng)式表單驗(yàn)證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構(gòu)
- 6.4.7 小結(jié)
- 6.5 刪除班級(jí)
- 6.6 集成測(cè)試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉(zhuǎn)
- 6.6.4 ngStyle
- 6.7 初識(shí)Service
- 6.7.1 catchError
- 6.7.2 單例服務(wù)
- 6.7.3 單元測(cè)試
- 6.8 小結(jié)
- 第七章 學(xué)生管理
- 7.1 班級(jí)列表組件
- 7.2 新增學(xué)生
- 7.2.1 exports
- 7.2.2 自定義驗(yàn)證器
- 7.2.3 異步驗(yàn)證器
- 7.2.4 再識(shí)DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結(jié)
- 7.3 單元測(cè)試進(jìn)階
- 7.4 學(xué)生列表
- 7.4.1 JSON對(duì)象與對(duì)象
- 7.4.2 單元測(cè)試
- 7.4.3 分頁(yè)模塊
- 7.4.4 子組件測(cè)試
- 7.4.5 重構(gòu)分頁(yè)
- 7.5 刪除學(xué)生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向?qū)ο?/a>
- 7.6 集成測(cè)試
- 7.7 編輯學(xué)生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學(xué)生
- 7.7.4 集成測(cè)試
- 7.7.5 可訂閱的路由參數(shù)
- 7.7.6 小結(jié)
- 7.8 總結(jié)
- 第八章 其它
- 8.1 打包構(gòu)建
- 8.2 發(fā)布部署
- 第九章 總結(jié)