本節開始時以**一卡通**為例進行認證的簡單講解。在使用**一卡通**時關鍵的一環在于:**一卡通**與學生的綁定及解綁。
## 用戶綁定
用戶綁定實際上是建立一種映射關系。在校園生活中它是一種**一卡通**與**學生**的映射關系,由學生的學號具有唯一性,所以還可以說是一種**一卡通**與**學號**的映射關系;在前后端分離的應用中它是一種**auth-token**與數據表中**teacher**的映射關系,由于teacher的id具有唯一性,所以還可以說是一種**auth-token**與**teacherId**之間的映射關系。
在java的世界里HashMap這種數據類型便是為了解決此類的映射問題而生的。下面開始打開后臺來到TeacherController的Login方法查看其實現的邏輯,并對其調用的服務層進行升級。以使得服務層提供:當用戶名與密碼校驗成功后,對應的建立**auth-token**與**teacherId**之間的關系,從而達到用戶綁定的目的。
controller/TeacherController.java
```java
@PostMapping("login")
public boolean login(@RequestBody Teacher teacher) {
return this.teacherService.login(teacher.getUsername(), teacher.getPassword());
}
```
查看C層調用獲取知其調用的是M層的login方法,于是找到對應的service/TeacherServiceImpl.java對應方法
service/TeacherServiceImpl.java
```java
@Override
public boolean login(String username, String password) {
Teacher teacher = this.teacherRepository.findByUsername(username);
return this.validatePassword(teacher, password);
}
```
升級如下:
```java
@Service
public class TeacherServiceImpl implements TeacherService {
/** auth-token與teacherId的映射 */
private HashMap<String, Long> authTokenTeacherIdHashMap = new HashMap<>(); ?
...
@Override
public boolean login(String username, String password) {
Teacher teacher = this.teacherRepository.findByUsername(username);
if (!this.validatePassword(teacher, password)) {
// 認證不成功直接返回
return false;
}
// 認證成功,進行auth-token與teacherId的綁定綁定
return true;
}
```
* ? HashMap理解為鍵值對的存儲類型。HashMap<String, Long>表示鍵的類型為String,值的類型為Long。存儲示例:`{"abcd" -> 123, "bcd" -> 456}`。
向hashMap中存數據使用`put`方法,比如預將相應的`auth-token`與`teacherId`進行綁定,代碼如下:
```java
...
// 認證成功,進行auth-token與teacherId的綁定綁定
this.authTokenTeacherIdHashMap.put("header中auth-token的值★"?, teacher.getId()?);
return true;
}
```
* put(鍵, 值);
* ? 類型是String,表示鍵
* ? 類型為Long,表示值
## 獲取header
認證的代碼準備好后,是否可以在TeacherServiceImpl中獲取header中的auth-token的值成為了是否能夠成功完成認證功能的關鍵因素。回想下前面在過濾器中已經成功的獲取過header中的token信息:
filter/TokenFilter.java
```java
String token = request.getHeader(this.TOKEN_KEY);
```
觀察上述的代碼發現只要能夠獲取相應的request,便能夠獲取對應的token。在spring中,可以像注入其它的服務一樣注入HttpServletRequest:
service/TeacherServiceImpl.java
```java
public class TeacherServiceImpl implements TeacherService {
...
private final? HttpServletRequest request;
private TeacherRepository teacherRepository;
...
@Autowired
public TeacherServiceImpl(TeacherRepository teacherRepository, HttpServletRequest? request) {
this.teacherRepository = teacherRepository;
this.request = request;
}
```
* ? 使用final關鍵字聲明該變量不可變,這有一些當前還看不到的優點
* ? 將HttpServletRequest聲明到以@Autowired注解的構造函數中。spring會自動將需要的HttpServletRequest注入
有了request后獲取header是一件非常輕松的事情。
service/TeacherServiceImpl.java
```java
...
// 認證成功,進行auth-token與teacherId的綁定綁定
this.authTokenTeacherIdHashMap.put(this.request.getHeader("auth-token"), teacher.getId());
return true;
}
```
### 測試一下
service/TeacherServiceImpl.java
```java
public class TeacherServiceImpl implements TeacherService {
private final static Logger logger = LoggerFactory.getLogger(TeacherServiceImpl.class);
...
// 認證成功,進行auth-token與teacherId的綁定綁定
logger.info("獲取到的auth-token為" + this.request.getHeader("auth-token"));
this.authTokenTeacherIdHashMap.put(this.request.getHeader("auth-token"), teacher.getId());
return true;
}
```
嘗試啟動后臺后得到如下錯誤:
```
Error:(21, 45) java: 無法將類 com.mengyunzhi.springbootstudy.service.TeacherServiceImpl中的構造器 TeacherServiceImpl應用到給定類型;
需要: com.mengyunzhi.springbootstudy.repository.TeacherRepository,javax.servlet.http.HttpServletRequest
找到: com.mengyunzhi.springbootstudy.repository.TeacherRepository
原因: 實際參數列表和形式參數列表長度不同
```
提示說:單元測試文件實例化TeacherServiceImpl時,僅接收到了一個參數,還差一個。這是由于在構造函數中加入了新的`HttpServletRequest`造成的,對應修正如下:
serivce/TeacherServiceImplTest.java
```java
public class TeacherServiceImplTest {
private TeacherServiceImpl teacherService;
private TeacherRepository teacherRepository;
private HttpServletRequest httpServletRequest; ?
@Before
public void before() {
this.teacherRepository = Mockito.mock(TeacherRepository.class);
this.httpServletRequest = Mockito.mock(HttpServletRequest.class); ?
TeacherServiceImpl teacherService = new TeacherServiceImpl(this.teacherRepository, this.httpServletRequest?);
this.teacherService = Mockito.spy(teacherService);
}
```
再次嘗試運行后臺成功。在數據庫中維護一個測試教師,并在IDEA中新建http request如下:
```
POST http://localhost:8080/Teacher/login
Content-type: application/json;
{"username": "panjie", "password":"yunzhi"}
```
請求結果:
```
POST http://localhost:8080/Teacher/login
HTTP/1.1 200
auth-token: 0076f815-2b8b-4c66-8df0-48fac5a4104e
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 17 Feb 2020 10:02:14 GMT
true
Response code: 200; Time: 159ms; Content length: 4 bytes
```
日志情況:
```
2020-02-17 18:02:13.903 INFO 59059 --- [nio-8080-exec-4] c.m.springbootstudy.filter.TokenFilter : 原token無效,發布的新的token為0076f815-2b8b-4c66-8df0-48fac5a4104e
...
2020-02-17 18:02:14.056 INFO 59059 --- [nio-8080-exec-4] c.m.s.service.TeacherServiceImpl : 獲取到的auth-token為0076f815-2b8b-4c66-8df0-48fac5a4104e
```
進行用戶綁定時,過濾器首先為沒有推帶auth-token的請求分發auth-token。login方法隨后將其分發的auth-token與當前的認證用戶進行綁定。符合預期。
## 用戶解綁
用戶解綁的流程與用戶綁定流程稍有不同,體現在:
一、用戶綁定時需要首先驗證用戶名密碼是否正確,只有當用戶名密碼正確的時候,才進行綁定。而用戶解除綁定不需要,只要用戶攜帶了分發給他的**一卡通**,后臺便能夠對應的解除**一卡通**與原有用戶的綁定。
二、用戶綁定時使用的是hashMap的put方法先添加String及Long的映射,而解綁中對應使用hashMap的remove方法。
與login方法相對應,解綁方法命名為logout:
service/TeacherService.java
```java
/**
* 用戶注銷
* 系統可以根據HttpServletRequest獲取到header中的令牌令牌
* 所以注銷方法不需要傳入任何參數
*/
void logout();
```
實現類:
service/TeacherServiceImpl.java
```java
@Override
public void logout() {
// 獲取auth-token
// 刪除hashMap中對應auth-token的映射
}
```
補充代碼:
service/TeacherServiceImpl.java
```java
@Override
public void logout() {
// 獲取auth-token
String authToken = this.request.getHeader("auth-token");
logger.info("獲取到的auth-token為" + this.request.getHeader("auth-token"));
// 刪除hashMap中對應auth-token的映射
this.authTokenTeacherIdHashMap.remove(authToken);
}
```
### C層
增加數據轉發的C層,為下一步的測試做準備:
controller/TeacherController.java
```java
@GetMapping("logout")
public void login() {
this.teacherService.logout();
}
```
### 測試
成功測試解除綁定方法的前提先進行用戶的綁定。用戶綁定成功后,再使用其對應的**auth-token**來進行解綁操作的測試。
重新啟動后臺,先發起用戶綁定請求,獲取對應的auth-token,請求結果如下:
```java
POST http://localhost:8080/Teacher/login
HTTP/1.1 200
auth-token: 4048387e-3d87-4553-862e-37fb2c2a81cf ★
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Feb 2020 01:19:41 GMT
true
```
* ★ 記錄后臺下發的auth-token
接著使用得到的auth-token發起解綁操作:
```
GET http://localhost:8080/Teacher/logout
auth-token: 4048387e-3d87-4553-862e-37fb2c2a81cf
```
響應信息:
```
GET http://localhost:8080/Teacher/logout
HTTP/1.1 200
auth-token: 4048387e-3d87-4553-862e-37fb2c2a81cf
Content-Length: 0
Date: Tue, 18 Feb 2020 01:25:07 GMT
<Response body is empty>
```
結果返回了200,說明響應信息正常。
## 我是誰
雖然已經成功的完成了用戶綁定與解綁功能。看總感覺心里面少點什么,原因是當前的測試僅僅能夠證明綁定與解綁兩個操作是可用的,但是否真正的完成用戶綁定與解綁的實際工作卻不得而知(通過觀察源代碼來判斷功能是否正常是最不負責的行為)。若想驗證綁定與解綁是否成功,則還需要一個"我是誰"的接口來進行驗證。
* 在進行用戶綁定前,訪問"我是誰"接口應該獲取到null信息,表示:當前沒有進行用戶認證,所以你誰也是不是。
* 用戶綁定成功后,訪問"我是誰"接口應該返回綁定的用戶信息。
* 重新使用其它用戶綁定后,訪問"我是誰"接口應該返回剛剛綁定的用戶
* 解除綁定后,訪問"我是誰"接口應該返回null信息
service/TeacherService.java
```java
/**
* 我是誰
* @return 當前登錄用戶。用戶未登錄則返回null
*/
Teacher me();
```
實現類:
service/TeacherServiceImpl.java
```java
@Override
public Teacher me() {
// 獲取authToken
// 獲取authToken映射的teacherId
// 未獲取到teacherId,說明該auth-token未與用戶進行綁定,返回null
// 如獲取到teacherId,則由數據庫中獲取teacher并返回
}
```
補充功能性代碼:
service/TeacherServiceImpl.java
```java
@Override
public Teacher me() {
// 獲取authToken
String authToken = this.request.getHeader("auth-token");
// 獲取authToken映射的teacherId
Long teacherId = this.authTokenTeacherIdHashMap.get(authToken);
if (teacherId == null) {
// 未獲取到teacherId,說明該auth-token未與用戶進行綁定,返回null
return null;
}
// 如獲取到teacherId,則由數據庫中獲取teacher并返回
Optional<Teacher> teacherOptional = this.teacherRepository.findById(teacherId);
return teacherOptional.get();
}
```
### C層數據轉發
controller/TeacherController.java
```java
@GetMapping("me")
public Teacher me() {
return this.teacherService.me();
}
```
### 測試
重新啟動后臺后依次做如下測試:
<hr>
測試一:在進行用戶綁定前,訪問"我是誰"接口應該獲取到null信息,表示:當前沒有進行用戶認證,所以你誰也是不是。
```
GET http://localhost:8080/Teacher/me
```
請求結果:未返回任何信息。
```
GET http://localhost:8080/Teacher/me
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0 ★
Content-Length: 0
Date: Tue, 18 Feb 2020 01:43:46 GMT
<Response body is empty>
```
* ★ 記錄分發的令牌,在以后的測試請求中均使用該令牌
<hr>
測試二:用戶綁定成功后,訪問"我是誰"接口應該返回綁定的用戶信息。
先訪問login接口,然后再請求me接口
login
```
POST http://localhost:8080/Teacher/login
Content-type: application/json;
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0 ?
{"username": "panjie", "password":"yunzhi"}
```
* ? 使用上一步接收到的令牌發起訪問。
返回信息:
```
POST http://localhost:8080/Teacher/login
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0 ?
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Feb 2020 01:46:07 GMT
true ?
```
* ? 返回了原令牌,符合預期
* ? 返回結果為true,說明登錄成功符合預期
發起對me接口的請求:
```
GET http://localhost:8080/Teacher/me
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
```
請求結果:
```
GET http://localhost:8080/Teacher/me
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Feb 2020 01:48:19 GMT
{
"id": 1,
"name": null,
"sex": true,
"username": "panjie",
"email": null,
"createTime": null,
"updateTime": null,
"password": "yunzhi"
}
```
返回了登錄用戶的信息,符合預期。
<hr>
測試三: 重新使用其它用戶綁定后,訪問"我是誰"接口應該返回剛剛綁定的用戶
在數據庫中新建測試教師:用戶名liuyuxuan ,密碼:yunzhi,然后訪問login接口發起認證。
```
POST http://localhost:8080/Teacher/login
Content-type: application/json;
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
{"username": "liuyuxuan", "password":"yunzhi"}
```
接著訪問me接口,驗證是否重新綁定成功。
```
GET http://localhost:8080/Teacher/me
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
```
響應結果:
```
GET http://localhost:8080/Teacher/me
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 18 Feb 2020 01:51:59 GMT
{
"id": 2,
"name": null,
"sex": true,
"username": "liuyuxuan", ?
"email": null,
"createTime": null,
"updateTime": null,
"password": "yunzhi"
}
```
* ? 當前登錄用戶名由panjie變更為liuyuxuan,說明重新綁定成功
<hr>
測試四:解除綁定后,訪問"我是誰"接口應該返回null信息。
首先發起logout接口訪問:
```
GET http://localhost:8080/Teacher/logout
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
```
響應信息:
```
GET http://localhost:8080/Teacher/logout
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
Content-Length: 0
Date: Tue, 18 Feb 2020 01:53:35 GMT
<Response body is empty>
```
再發起me接口訪問:
```
GET http://localhost:8080/Teacher/me
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
```
響應信息為空,說明解除綁定成功。
```
GET http://localhost:8080/Teacher/me
HTTP/1.1 200
auth-token: 8f03f582-4284-4b4e-85f2-258f30f016c0
Content-Length: 0
Date: Tue, 18 Feb 2020 01:54:05 GMT
<Response body is empty>
```
至此一個基本的登錄、注銷、我是誰的功能開發完畢。
## 重構代碼
重構代碼是提高代碼質量的重要一環,好的代碼講求的是:易閱讀、易維護、對擴展開放、對修改關閉。本例中在獲取認證令牌時,使用了大量的字符串`auth-token`。這為日志修改該認識關鍵字挖下了坑:我們希望在日后的維護過程中,可以很輕松的修改該字符串,比如將`auth-token`修正為`x-auth=token`、`web-auth-token`或`app-auth-token`等。而大量字符串出現在源代碼中將要求以后完成該字符串修改工作的成員對項目極其熟悉,以致于不會漏掉任何一個`auth-token`(雖然這可以使用編輯器的查找替換工作完成,但此時我們更關注是一種編程的習慣)。
解決該問題的方法很簡單:`auth-token`在整個項目中只出現一次,只為其它使用到該值的地方,全部引用該變量。比如本項目中`auth-token`首次出現在過濾器`TokenFilter`中,則在`TeacherServiceImpl`中所有使用字符串`auth-token`地方,均應該使用`TokenFilter`中的`auth-token`。為此,首先將`TokenFilter`中記錄`auth-token`的屬性聲明為公有靜態的,然后在`TeacherServiceImpl`中引用其值。
filter/TokenFilter.java
```java
public class TokenFilter extends HttpFilter {
private String TOKEN_KEY = "auth-token"; ?
public static String TOKEN_KEY = "auth-token"; ?
...
String token = request.getHeader(this.TOKEN_KEY); ?
String token = request.getHeader(TOKEN_KEY); ?
```
然后將`TeacherServiceImpl`所有使用`auth-token`字符串的地方替換為`TokenFilter.TOKEN_KEY`,比如:
service/TeacherServiceImpl.java
```java
String authToken = this.request.getHeader(TokenFilter.TOKEN_KEY);
```
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.6) | - |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用