token的問題解決后本節展示如何將新令牌添加到請求的header中。
上節中學習過了使用`request.getHeader(String key);`來獲取header中某個key的值。通常來說則還會有個類似的`request.setHeader(String key, String value);`的方法用于設置header中的某個key的值。
很遺憾,出于某些方面的考慮spring中(確認說是servlet)中并沒有提供設置header的方法。

request只提供了上述兩個set方法,也就是說我們沒有辦法通過request來直接設置其`header`信息。既然無法設置其值,那么是否可以在獲取`header`的值的方法上下功夫呢?獲取某個`header`時則必然調用`getHeader`方法;其它人獲取令牌時,則必然調用`getHeader("auth-token")`。在編程的世界中,有一種叫做裝飾器模式的方法恰好能夠做到變更`getHeader("auth-token")`的返回值的目的。被改變后的`getHeader("auth-token")`可以指定的返回我們為其指定的`auth-token`信息,從而間接的**達到**設置header信息的目的。
# 裝飾器模式
alphago前幾年風靡全球,但你一定想不到的是:早上18世紀晚期的1770年,土耳其便已經擁有了一臺自動下棋(應該是國際象棋)裝置,比2016年3月alphago成名足足早了240年!該下棋裝置在當時擊敗了大多數挑戰者,這其中還包括了拿破侖。此裝置不止能夠計算出下一個落棋點,而且還可以自動拿著棋子下棋,甚至于還會與人交流說"將軍"。

在那個只有機器裝置的年代,完成這項工作簡直不可思議。那么當時人們是怎么做到的呢?

重點就在下棋裝置里面的"人",實際上在下棋裝置中偷偷藏了一個下棋的高手。而下棋裝置走的每一步棋,都是由該下棋高手在內容操作的(當然這種操作本身使用機器裝置傳遞到機器臂也很復雜)。
但筆者想說的是:這種在下棋裝置中將下棋高手包裝(裝飾)起來,以使得下棋傀儡擁有下棋高手的下棋能力的模式,被稱為"裝飾器模式"。
## DEMO
下面以一個小的demo來講下如何使用"裝飾器模式"達到設置header中的auth-token的目的。
```java
class Request {
String getHeader(String key) {
// 獲取header值的真實代碼略
}
}
class TokenFilterTest {
/**在此傳入auth-token的值*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
this.getAuthUser(request);
}
/** 在此獲取auth-token的值 */
void getAuthUser(Request request) {
// 獲取auth-token
request.getHeader("auth-token");
// 根據auth-token獲取當前登錄用戶
}
}
```
上述代碼首先執行doFilter方法,然后再執行`getAuthUser`方法,在`getAuthUser`方法中調用了`request`的`getHeader`方法近而獲取到`auth-token`的值。
接下來使用裝飾器模式來簡單處理一下:
```
class Request {
...
}
class TokenFilterTest {
...
}
/**
* Request傀儡
*/
class RequestWrapper extends Request? {
Request request; ?
private RequestWrapper() { ?
}
public RequestWrapper(Request request) { ?
this.request = request;
}
@Override
String getHeader(String key) { ?
return this.request.getHeader(key); ?
}
}
```
* ? 繼承了Request,說明Request有的方法本傀儡都有。指定需要Request的地方,本傀儡都能代替。
* ? 本傀儡之所有這么有底氣,是由于本傀儡內部擁有一個真正的Request(下棋高手)
* ? 本傀儡也會下棋
* ? 實際上下的每一步棋都是真正的Request來完成的
* ? 若想啟動本傀儡,**必須**給我裝入一個下棋高手
有了這個傀儡以后,doFilter方法便可以改成這樣:
```
/**在此傳入auth-token的值*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request); ?
this.getAuthUser(request); ? ?
this.getAuthUser(requestWrapper); ?
}
```
* ? 將下棋高手裝入
* ? 由于下棋傀儡requestWrapper擁有下棋高手request的全部功能,所以此時將下棋傀儡requestWrapper傳入getAuthUser也是完全符合要求的。
此時程序執行的時序圖如下:

## 重寫裝飾器中的方法
此時只要稍稍的對裝飾器的getHeader方法進行改行,便能達到:如果傳入的key為`auth-token`便返回新的令牌的目的:
```java
/**
* Request傀儡
*/
class RequestWrapper extends Request {
...
@Override
String getHeader(String key) {
if ("auth-token".equals(key)) {
// 在此返回新的auth-token值
return "456789";
}
return this.request.getHeader(key);
}
}
```
對應實序圖如下:

## 在外部向裝飾器中設置token
有了裝飾器,也能夠在裝飾器中按特定的思想傳值了,但這仍然滿足不了初始的要求,即:
```java
class TokenFilterTest {
/**在此傳入auth-token的值 ?*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
this.getAuthUser(request);
}
/** 在此獲取auth-token的值 ?*/
void getAuthUser(Request request) {
// 獲取auth-token
request.getHeader("auth-token");
// 根據auth-token獲取當前登錄用戶
}
}
```
將方法?中設置的authToken的值通過request?帶入getAuthUser中。剛剛改寫getHeader的方法卻是傳回了固定的"456789",而非"654321"。繼續如下改造RequestWrapper
```java
class RequestWrapper extends Request {
Request request;
String token; ?
private RequestWrapper() {
}
private RequestWrapper(Request request) { ?
this.request = request;
}
public RequestWrapper(Request request, String token) {
this(request); ?
this.token = token; ?
}
@Override
String getHeader(String key) {
if ("auth-token".equals(key)) {
return this.token; ?
}
return this.request.getHeader(key);
}
...
```
* ? 增加token,并做為auth-token返回
* ? 將原構造函數設置為私有,并在內部進行調用
此時doFilter隨之改造為:
```
@Test
public void doFilter() {
String authToken = "654321";
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request, authToken);
this.getAuthUser(requestWrapper);
}
```
加入測試信息后的最終代碼如下:
filter/TokenFilterTest.java
```java
public class TokenFilterTest {
@Test
public void doFilter() {
String authToken = new RandomString(6).nextString();
System.out.println("authToken傳入值為" + authToken);
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request, authToken);
this.getAuthUser(requestWrapper);
}
/** 在此獲取auth-token的值 */
void getAuthUser(Request request) {
// 獲取auth-token
System.out.println("獲取到的auth-token值為:" + request.getHeader("auth-token"));
// 根據auth-token獲取當前登錄用戶
}
}
class Request {
private String authToken = "123456";
String getHeader(String key) {
System.out.println("調用了Request中的getHeader方法");
if ("auth-token".equals(key)) {
return this.authToken;
}
// 其它的IF條件
return null;
}
}
/**
* Request傀儡
*/
class RequestWrapper extends Request {
Request request;
String token;
private RequestWrapper() {
}
private RequestWrapper(Request request) {
this.request = request;
}
public RequestWrapper(Request request, String token) {
this(request);
this.token = token;
}
@Override
String getHeader(String key) {
System.out.println("調用了RequestWrapper中的getHeader方法");
if ("auth-token".equals(key)) {
return this.token;
}
return this.request.getHeader(key);
}
}
```
執行結果如下:
```
authToken傳入值為X0QR0u
調用了RequestWrapper中的getHeader方法
獲取到的auth-token值為:X0QR0u
```
結果符合預期。達到了在不改變request中的header值的前提下,將token在A方法中傳入,然后在B方法中獲取的目的。
# HttpServletRequestWrapper
spring已經提供了用于裝飾request的類 ---- HttpServletRequestWrapper。

此類提供了構造函數:`HttpServletRequestWrapper(request)`方法,但未提供所需要的`HttpServletRequestWrapper(request, token)`構造函數。為此,在原有HttpServletRequestWrapper的基礎上,做一下**繼承**并將其命名為HttpServletRequestTokenWrapper:

代碼如下:
```java
@WebFilter
public class TokenFilter extends HttpFilter {
...
/**
* 帶有請求token的Http請求
*/
class HttpServletRequestTokenWrapper extends HttpServletRequestWrapper { ?
HttpServletRequestWrapper httpServletRequestWrapper;
String token;
private HttpServletRequestTokenWrapper(HttpServletRequest request) { ?
super(request);
}
public HttpServletRequestTokenWrapper(HttpServletRequest request, String token) { ?
this(request);
this.token = token;
}
@Override
public String getHeader(String name) { ?
if (TOKEN_KEY.equals(name)) {
return this.token;
}
return super.getHeader(name);
}
}
}
```
* ? 該類定義于TokenFilter內部
* ? 實現父類的構造函數,為防止其被外部使用從而聲明為私有
* ? 新增構造函數,接收request及token
* ? 重寫getHeader方法
使用該HttpServletRequestTokenWrapper在header中設置auth-token的代碼如下:
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 有效性判斷
if (!this.validateToken(token)) {
// 如果無效則分發送的token
token = this.makeToken();
logger.info("原token無效,發布的新的token為" + token);
// 設置header中的auth-token
request = new HttpServletRequestTokenWrapper(request, token); ?
}
```
* ? 此時的request已經是那個被重寫過getHeader方法的HttpServletRequestTokenWrapper了。
# 設置響應的令牌
在響應令牌的header中設置令牌非常簡單:只需要調用response中的setHeader方法即可。
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 轉發數據。spring開始調用控制器中的特定方法
chain.doFilter(request, response);
logger.info("在控制器被調用以后執行");
// 為http響應加入新token后返回
response.setHeader(TOKEN_KEY, token);
```
至此,使用過濾器在請求的header信息中傳遞令牌的過程便完整的實現了。雖然此方法沒有考慮到生產環境下令牌過期以及令牌更新的問題,但在當前系統中已經完全夠用了。
>[info] 在生產環境中會采用更加優秀、集成度更高的spring security來替代令牌交互及后面章節中將到學習到的用戶認證過程。
# 測試
一切都不能想當然,一切都不能"我認為"。保證代碼質量最好的方法便是使用單元測試,如果單元測試不適用則么最少也要看看日志及返回值。再次新建http request測試如下:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8 ?
Transfer-Encoding: chunked ?
Date: Tue, 11 Feb 2020 05:22:36 GMT ?
[]
Response code: 200; Time: 121ms; Content length: 2 bytes
```
* ? 很遺憾:在響應的header并未獲取到令牌auth-token
筆者猜想原因如下
1. 之所以沒有在request上提供setHeader方法,官方是這么考慮的:一旦請求信息確立但不能夠被再次修改以保證數據在傳輸過程中不失真。也就是說過濾器的設計之初并不是用來變更請求信息的(比如進行訪問日志的記錄)。
2. `response.setHeader(TOKEN_KEY, token);`未生效的原因也是如此:一旦響應信息確立則不能夠對其進行修改,所以即使調用了setHeader也不會生效。
3. 確認request信息取決于前臺,前臺請求時帶有什么值request就有什么值。所以在request不提供setHeader方法。而response信息取決于后臺,在沒有為response定稿前,是需要setHeader方法來設置header信息的。所以response中提供了setHeader方法供調用。
基于此,若想改變response響應中的header信息,那么應該在響應信息被確立以前。所以代碼最終應該被修正為:
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 在確立響應信息前,設置響應的header值 ?
response.setHeader(TOKEN_KEY, token); ?
// 轉發數據。spring開始調用控制器中的特定方法
chain.doFilter(request, response);
logger.info("在控制器被調用以后執行");
// 為http響應加入新token后返回 ?
response.setHeader(TOKEN_KEY, token); ?
```
重新啟動應用后再次測試:
<hr>
請求時未傳入token:
```
GET http://localhost:8080/Teacher
```
測試結果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: e2047302-2c07-474a-9066-6404bc3b4534 ?
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:34:44 GMT
[]
Response code: 200; Time: 122ms; Content length: 2 bytes
```
* ? 未傳入token時,系統分發新token
<hr>
請求時傳入有效token:
```
GET http://localhost:8080/Teacher
auth-token: d31e0d8a-470d-413a-beda-23885f4551f2 ?
```
測試結果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: d31e0d8a-470d-413a-beda-23885f4551f2 ?
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:36:46 GMT
[]
Response code: 200; Time: 12ms; Content length: 2 bytes
```
* ? 傳入有效token時,系統回傳有效token
<hr>
請求時傳入無效token
```
GET http://localhost:8080/Teacher
auth-token: d31e0d8a-470d-413a-beda ?
```
測試結果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: e71dd873-0f0b-428b-b202-aa1b359fe3ff ?
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:38:04 GMT
[]
Response code: 200; Time: 30ms; Content length: 2 bytes
```
* ? 傳入無效token時,系統分發新的有效token
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.3) | - |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用