目錄
1. 登錄功能
1.1 需求
1.2 接口文檔
1.3 登錄 - 思路分析
1.4 功能開發(fā)
1.5 測試
2. 登錄校驗
2.1 問題分析
什么是登錄校驗?
我們要完成以上登錄校驗的操作,會涉及到Web開發(fā)中的兩個技術(shù):
2.2 會話技術(shù)
2.2.1 會話技術(shù)介紹
會話跟蹤?
2.2.2 會話跟蹤方案
2.2.2.1 方案一 - Cookie
Cookie這種會話跟蹤技術(shù)的優(yōu)缺點:
跨域介紹:
區(qū)分跨域的三個維度:
2.2.2.2 方案二 - Session?
Session這種會話跟蹤技術(shù)的優(yōu)缺點:
2.2.2.3 方案三 - Token令牌技術(shù)
2.3 JWT令牌
2.3.1 JWT介紹
JWT的組成:(JWT令牌由三個部分組成,三個部分之間使用了兩個英文的點來分割)?
JWT令牌的應(yīng)用場景?
2.3.2 生成和校驗JWT令牌
首先我們先來實現(xiàn)JWT令牌的生成。
生成JWT代碼實現(xiàn): ?
校驗/解析JWT令牌:?
2.3.3 登錄下發(fā)令牌
2.4 過濾器Filter
2.4.2 Filter詳解
2.4.2.1 執(zhí)行流程
2.4.2.2 攔截路徑
2.4.2.3 過濾器鏈
2.4.3 登錄校驗-Filter
2.4.3.1 分析
2.4.3.2 具體流程
2.4.3.3 代碼實現(xiàn)
2.5 攔截器Interceptor
2.5.1 快速入門
攔截器Interceptor快速入門?
2.5.2 Interceptor詳解
2.5.2.1 攔截器 - 攔截路徑
2.5.2.2 攔截器的執(zhí)行流程
2.5.3 登錄校驗- Interceptor
3. 異常處理
3.1 當(dāng)前問題
3.2 解決方案
3.3 全局異常處理器
登錄認(rèn)證,那什么是認(rèn)證呢?
- 所謂認(rèn)證指的就是根據(jù)用戶名和密碼校驗用戶身份的這個過程,認(rèn)證成功之后,我們才可以訪問系統(tǒng)當(dāng)中的信息,否則就拒絕訪問。
在前面的案例中,我們已經(jīng)實現(xiàn)了部門管理、員工管理的基本功能,但是大家會發(fā)現(xiàn),我們并沒有登錄,就直接訪問到了Tlias智能學(xué)習(xí)輔助系統(tǒng)的后臺。 這是不安全的,所以我們今天的主題就是登錄認(rèn)證。 最終我們要實現(xiàn)的效果就是用戶必須登錄之后,才可以訪問后臺系統(tǒng)中的功能。
- 在登錄頁面中,用戶要輸入用戶名,輸入密碼,然后接下來點擊登錄,如果輸入的用戶名或者密碼錯誤,此時就會停留在登錄頁面當(dāng)中,并且提示出對應(yīng)的錯誤信息;
- 如果用戶名和密碼都是正確的,我們點擊登錄按鈕,此時才會進入到系統(tǒng)當(dāng)中,進入到系統(tǒng)之后,我們就可以來操作系統(tǒng)當(dāng)中的數(shù)據(jù)了。
要想實現(xiàn)用戶登錄的功能,我們需要兩步操作來實現(xiàn):
- 首先第一步,我們要先來完成最為基礎(chǔ)的登錄功能,這步操作就是來判斷用戶輸入的用戶名和密碼是否正確;
- 第二步,我們要來完成登錄校驗操作:登錄校驗指的就是當(dāng)我們?yōu)g覽器發(fā)起一個請求之后,服務(wù)端需要判斷這個用戶是否登錄了,如果登錄了,則執(zhí)行正常的業(yè)務(wù)操作;如果沒有登錄,就需要跳轉(zhuǎn)到登錄界面,讓他完成登錄之后再來訪問這個系統(tǒng)。?
注意:登錄校驗是整個登錄功能的核心!?
1. 登錄功能
1.1 需求
在登錄界面中,我們可以輸入用戶的用戶名以及密碼,然后點擊 "登錄" 按鈕就要請求服務(wù)器,服務(wù)端判斷用戶輸入的用戶名或者密碼是否正確。如果正確,則返回成功結(jié)果,前端跳轉(zhuǎn)至系統(tǒng)首頁面。?
思考:在登錄的時候,我們需要校驗用戶名和密碼是否正確,這條SQL語句該怎么寫?
回答:其實非常簡單,逆向思考,就是根據(jù)用戶名和密碼來查詢員工,如果根據(jù)用戶名和密碼,我查詢到了員工,就說明用戶名和密碼是正確的;如果根據(jù)用戶名和密碼,我沒有查詢到員工,就說明用戶名或密碼錯誤。
SQL語句:
-- 登錄時校驗用戶名和密碼
select * from emp where username = '' and password = '';
思考:根據(jù)這條SQL語句查詢出來的員工有沒有可能是多個?
回答:不可能,因為之前我們創(chuàng)建emp員工表的時候,針對于username這個字段,我們添加的是unique唯一約束,所以username這個它是不可能重復(fù)的,因此最終我們查詢出來的數(shù)據(jù),最多只會有一條。
1.2 接口文檔
- 我們參照接口文檔來開發(fā)登錄功能
基本信息
- 請求參數(shù)
參數(shù)格式:application/json
參數(shù)說明:
名稱 | 類型 | 是否必須 | 備注 |
---|---|---|---|
username | string | 必須 | 用戶名 |
password | string | 必須 | 密碼 |
請求數(shù)據(jù)樣例:
- 響應(yīng)數(shù)據(jù)
參數(shù)格式:application/json
參數(shù)說明:
名稱 | 類型 | 是否必須 | 默認(rèn)值 | 備注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必須 | 響應(yīng)碼, 1 成功 ; 0 失敗 | ||
msg | string | 非必須 | 提示信息 | ||
data | string | 必須 | 返回的數(shù)據(jù) , jwt令牌 |
響應(yīng)數(shù)據(jù)樣例: ?
1.3 登錄 - 思路分析
說明:目前我們先不考慮返回JWT令牌,目前我們只是給前端響應(yīng)成功還是失敗。
首先第一件事,我們肯定需要在Controller當(dāng)中定義一個方法來處理這個登錄請求,此時需要思考登錄這個請求方法我們應(yīng)該定義哪一個Controller當(dāng)中?
是DeptController,還是EmpController,還是UploadController,都不是,原因:DeptController的請求路徑是/depts,EmpController的請求路徑是/emps,UploadController的請求路徑是/upload,并且UploadController是用來進行文件上傳的。?
因此,我們需要再定義一個Controller,專門用來處理登錄請求,取名叫LoginController,然后我們在LoginController當(dāng)中再來定義一個方法來處理登錄的請求,由于登錄的請求方式是一個POST請求,所以我們需要在該方法上面加上@PostMapping,而且請求格式的參數(shù)是一個JSON格式的請求參數(shù),最終服務(wù)端要把JSON格式的參數(shù)封裝到一個對象當(dāng)中,所以我們要在方法的形參上加上@RequestBody注解來接收前端傳遞過來的JSON格式的數(shù)據(jù)并填充到實體類中,
登錄服務(wù)端的核心邏輯就是:接收前端請求傳遞的用戶名和密碼 ,然后再根據(jù)用戶名和密碼查詢用戶信息,如果用戶信息存在,則說明用戶輸入的用戶名和密碼正確。如果查詢到的用戶不存在,則說明用戶輸入的用戶名和密碼錯誤。 ?
1.4 功能開發(fā)
LoginController
package com.gch.controller;
import com.gch.pojo.Emp;
import com.gch.pojo.Result;
import com.gch.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/login")
/**
登錄功能控制器
*/
public class LoginController {
@Autowired
private EmpService empService;
/**
* 處理登錄請求
* @param emp 員工對象
* @return 響應(yīng)
*/
@PostMapping
public Result login(@RequestBody Emp emp) {
// 1.記錄日志
log.info("處理該用戶登錄請求,username:{}, password:{}", emp.getUsername(), emp.getPassword());
// 2.調(diào)用service進行查詢,查詢/校驗該用戶信息是否存在
Emp e = empService.login(emp);
// 3.響應(yīng)
return e != null ? Result.success(e) : Result.error("用戶名或密碼錯誤");
}
}
EmpService
package com.gch.service;
import com.gch.pojo.Emp;
import com.gch.pojo.PageBean;
import java.time.LocalDate;
import java.util.List;
/**
員工業(yè)務(wù)規(guī)則
*/
public interface EmpService {
/**
* 處理該用戶的登錄請求
* @param emp 員工對象
* @return 根據(jù)前端傳遞的用戶信息返回查詢到的員工對象
*/
Emp login(Emp emp);
}
EmpServiceImpl
package com.gch.service.impl;
import com.gch.mapper.EmpMapper;
import com.gch.pojo.Emp;
import com.gch.pojo.PageBean;
import com.gch.service.EmpService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
員工業(yè)務(wù)實現(xiàn)類
*/
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
/**
* 處理該用戶的登錄請求
* @param emp 員工對象
* @return 根據(jù)前端傳遞的用戶信息返回查詢到的員工對象
*/
@Override
public Emp login(Emp emp) {
// 1.調(diào)用Mapper層查詢該員工信息
Emp loginEmp = empMapper.getByUsernameAndPassword(emp);
// 2.返回查詢結(jié)果給Controller
return loginEmp;
}
}
EmpMapper
package com.gch.mapper;
import com.gch.pojo.Emp;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDate;
import java.util.List;
/**
員工管理
*/
@Mapper
public interface EmpMapper {
/**
* 處理該用戶的登錄請求
* 根據(jù)用戶名和密碼查詢員工
* @param emp 員工對象
* @return 根據(jù)前端傳遞過來的請求參數(shù)中的用戶信息查詢員工是否存在
*/
@Select("select * from tlias.emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
}
1.5 測試
功能開發(fā)完畢后,我們就可以啟動服務(wù),打開Postman進行測試了。
發(fā)起POST請求,訪問:http://localhost:8080/login
Postman測試通過了,那接下來,我們就可以結(jié)合著前端工程進行聯(lián)調(diào)測試。
先退出系統(tǒng),進入到登錄頁面: ?
在登錄頁面輸入賬戶密碼: ?
故意把密碼輸錯,看登陸頁面會不會提示錯誤!
注意:提示錯誤的信息,就是在Controller中響應(yīng)給前端的信息!
登錄成功之后進入到后臺管理系統(tǒng)頁面: ?
2. 登錄校驗
2.1 問題分析
我們已經(jīng)完成了基礎(chǔ)登錄功能的開發(fā)與測試,并且完成了前后端聯(lián)調(diào),在我們登錄成功后就可以進入到后臺管理系統(tǒng)中進行數(shù)據(jù)的操作。 ?
但是當(dāng)我們在瀏覽器中新的頁面上輸入地址:http://localhost:9528/#/system/dept
,也就是復(fù)制了已經(jīng)登錄進入后臺管理系統(tǒng)的頁面地址,接著退出后臺管理系統(tǒng),然后關(guān)閉該頁面,接著打開一個新的標(biāo)簽頁,然后粘貼進入剛才已經(jīng)登錄進入后臺管理系統(tǒng)的頁面地址,發(fā)現(xiàn)沒有登錄仍然可以進入到后端管理系統(tǒng)頁面。
而真正的登錄功能應(yīng)該是:登陸后才能訪問后端系統(tǒng)頁面,不登陸則跳轉(zhuǎn)登陸頁面進行登陸。?
這是異?,F(xiàn)象!
問題:在未登錄情況下,我們也可以直接訪問部門管理、員工管理等功能。
為什么會出現(xiàn)這個問題?
- 其實原因很簡單,就是因為針對于我們當(dāng)前所開發(fā)的部門管理、員工管理以及文件上傳等相關(guān)接口來說,我們在服務(wù)器端并沒有做任何的判斷,沒有去判斷用戶是否登錄了。所以無論用戶是否登錄,都可以訪問部門管理以及員工管理的相關(guān)數(shù)據(jù)。所以我們目前所開發(fā)的登錄功能,它只是徒有其表。而我們要想解決這個問題,我們就需要完成一步非常重要的操作:登錄校驗。
什么是登錄校驗?
-
所謂登錄校驗,指的是我們在服務(wù)器端接收到瀏覽器發(fā)送過來的請求之后,首先我們要對請求進行校驗。先要校驗一下用戶登錄了沒有,如果用戶已經(jīng)登錄了,就直接執(zhí)行對應(yīng)的業(yè)務(wù)操作就可以了;如果用戶沒有登錄,此時就不允許他執(zhí)行相關(guān)的業(yè)務(wù)操作,直接給前端響應(yīng)一個錯誤的結(jié)果,最終跳轉(zhuǎn)到登錄頁面,要求他登錄成功之后,再來訪問對應(yīng)的數(shù)據(jù)。
了解完什么是登錄校驗之后,接下來我們分析一下登錄校驗大概的實現(xiàn)思路。 ?
首先我們在宏觀上先有一個認(rèn)知,然后再來逐個擊破:
前面在講解HTTP協(xié)議的時候,我們提到HTTP協(xié)議是無狀態(tài)協(xié)議。什么又是無狀態(tài)的協(xié)議?
- 所謂無狀態(tài),指的是每一次請求都是獨立的,下一次請求并不會攜帶上一次請求的數(shù)據(jù)。而瀏覽器與服務(wù)器之間進行交互,是基于HTTP協(xié)議的,也就意味著現(xiàn)在我們通過瀏覽器來訪問了登陸這個接口,實現(xiàn)了登陸的操作,接下來我們在執(zhí)行其他業(yè)務(wù)操作時,服務(wù)器也并不知道這個員工到底登陸了沒有。因為HTTP協(xié)議是無狀態(tài)的,兩次請求之間是獨立的,所以是無法判斷這個員工到底登陸了沒有。
那應(yīng)該怎么來實現(xiàn)登錄校驗的操作呢?具體的實現(xiàn)思路可以分為兩部分: ?
- 在服務(wù)端要想判斷這個員工是否已經(jīng)登錄,我們就需要在員工登錄成功之后,要存儲這么一個登錄成功的標(biāo)記,一旦員工登陸成功,那我們就存儲登錄成功的這樣一個標(biāo)記,記錄用戶已經(jīng)登錄成功的標(biāo)記;
- 然后接下來我們在每一個接口方法執(zhí)行之前,先來做一個條件判斷,來判斷一下這個員工到底登錄了沒有,如果這個員工已經(jīng)登錄了,那接下來,我們就執(zhí)行正常的業(yè)務(wù)操作就可以了;如果這個員工沒有登錄,我們在這一塊兒直接返回錯誤的信息,把這個錯誤的信息返回給前端,前端拿到這個錯誤的信息之后,它會自動的跳轉(zhuǎn)到登陸頁面。
我們程序中所開發(fā)的查詢功能、刪除功能、添加功能、修改功能,都需要使用以上套路進行登錄校驗。此時就會出現(xiàn):相同代碼邏輯,每個功能都需要編寫,就會造成代碼非常繁瑣。
為了簡化這塊操作,我們可以使用一種技術(shù):統(tǒng)一攔截技術(shù)。
- 通過統(tǒng)一攔截的技術(shù),我們可以來攔截瀏覽器發(fā)送過來的所有的請求,攔截到這個請求之后,就可以通過請求來獲取之前所存入的登錄標(biāo)記,在獲取到登錄標(biāo)記,且標(biāo)記為登錄成功,就說明員工已經(jīng)登錄了。如果已經(jīng)登錄,我們就直接放行(意思就是可以訪問正常的業(yè)務(wù)接口了)。
在員工登錄成功后,需要將用戶登錄成功的信息存起來,記錄用戶已經(jīng)登錄成功的標(biāo)記。
在瀏覽器發(fā)起請求時,需要在服務(wù)端進行統(tǒng)一攔截,攔截后進行登錄校驗。

? ?
所以要想完成這個登錄校驗的操作,主要涉及到兩個部分:
- 登錄標(biāo)記:用戶登錄成功之后,每一次請求當(dāng)中都可以獲取到該登錄標(biāo)記,這里就涉及到Web開發(fā)當(dāng)中的會話技術(shù)。
- 統(tǒng)一攔截技術(shù):要想實現(xiàn)統(tǒng)一攔截這個功能,常見的技術(shù)方案有這么兩種:一種是Servlet規(guī)范當(dāng)中的Filter過濾器,還有一種就是Spring當(dāng)中提供的攔截器Interceptor。?
我們要完成以上登錄校驗的操作,會涉及到Web開發(fā)中的兩個技術(shù):
-
會話技術(shù)
-
統(tǒng)一攔截技術(shù)
學(xué)習(xí)登錄校驗章節(jié)的四個部分:?
- 傳統(tǒng)的Web會話技術(shù)
- 當(dāng)前項目當(dāng)中主流的解決方案:令牌技術(shù)
- 兩種統(tǒng)一攔截的技術(shù):過濾器Filter、攔截器Interceptor?
2.2 會話技術(shù)
了解了登錄校驗的大概思路之后,我們先來學(xué)習(xí)下會話技術(shù)。 ? ??
2.2.1 會話技術(shù)介紹
什么是會話?
-
在我們?nèi)粘I町?dāng)中,會話指的就是談話、交談。
-
在Web開發(fā)當(dāng)中,會話指的就是瀏覽器與服務(wù)器之間的一次連接,我們就稱為一次會話。
在用戶打開瀏覽器第一次訪問Web服務(wù)器(資源)的時候,這個會話就建立了,直到有任何一方斷開連接,此時會話就結(jié)束了。
在一次會話當(dāng)中,是可以包含多次請求和響應(yīng)的。
比如:打開了瀏覽器來訪問Web服務(wù)器上的資源(瀏覽器不能關(guān)閉、服務(wù)器不能斷開)
第1次:訪問的是登錄的接口,完成登錄操作
第2次:訪問的是部門管理接口,查詢所有部門數(shù)據(jù)
第3次:訪問的是員工管理接口,查詢員工數(shù)據(jù)
只要瀏覽器和服務(wù)器都沒有關(guān)閉,以上3次請求都屬于一次會話當(dāng)中完成的。
其實,會話技術(shù)的應(yīng)用非常的常見,當(dāng)我們每天上網(wǎng),打開瀏覽器輸入域名,一敲回車之后就訪問到了對應(yīng)的服務(wù)器,此時瀏覽器與服務(wù)器就建立起了會話。??
思考:一臺服務(wù)器將來是會被很多瀏覽器同時來訪問,假如我們有三個瀏覽器都在訪問同一臺Web服務(wù)器,已經(jīng)和服務(wù)器建立好了連接,一共發(fā)起了5個請求,判斷一共是幾次會話?
回答:一共是三次會話,因為現(xiàn)在有三個客戶端瀏覽器和服務(wù)器建立連接,所以是三次會話? ? ? ? ?
- 需要注意的是:會話是和瀏覽器關(guān)聯(lián)的,當(dāng)有三個客戶端瀏覽器和服務(wù)器建立了連接時,就會有三個會話。
- 同一個瀏覽器在未關(guān)閉之前請求了多次服務(wù)器,這多次請求是屬于同一個會話。比如:1、2、3這三個請求都是屬于同一個會話。
- 當(dāng)我們關(guān)閉瀏覽器之后,這次會話就結(jié)束了。而如果我們是直接把Web服務(wù)器關(guān)了,那么所有的會話就都結(jié)束了。
知道了會話的概念了,接下來我們再來了解下會話跟蹤。
會話跟蹤?
會話跟蹤:一種維護瀏覽器狀態(tài)的方法,服務(wù)器需要識別多次請求是否來自于同一瀏覽器,以便在同一次會話的多次請求間共享數(shù)據(jù)。
- 服務(wù)器會接收很多的請求,但是服務(wù)器是需要識別出這些請求是不是同一個瀏覽器發(fā)出來的。 ?
- 如果多次請求是同一個瀏覽器發(fā)出來的,那就說明是同一次會話;
- 如果多次請求是不同瀏覽器發(fā)出來的,那就說明是不同的會話。
- 而識別多次請求是否來自于同一瀏覽器的過程,我們就稱之為會話跟蹤。
我們使用會話跟蹤技術(shù)就是要完成在同一次會話中的多個請求之間進行共享數(shù)據(jù)。 ?
為什么要共享數(shù)據(jù)呢?
- 瀏覽器與服務(wù)器之間在進行交互的時候,使用的是HTTP協(xié)議,由于HTTP是無狀態(tài)協(xié)議,下一次請求它并不會攜帶上一次請求的數(shù)據(jù),每一次請求都是相互獨立的,在后面請求中怎么拿到前一次請求生成的數(shù)據(jù)呢?所以此時就需要在一次會話的多次請求之間進行數(shù)據(jù)共享,要想進行數(shù)據(jù)共享,我們就需要用到會話跟蹤技術(shù)。
會話跟蹤技術(shù)有兩種:
- 傳統(tǒng)Web開發(fā)當(dāng)中所提供的兩種會話跟蹤技術(shù)(Cookie、Session)
- 當(dāng)前企業(yè)開發(fā)當(dāng)中最主流的會話跟蹤技術(shù) - 令牌技術(shù)?
Cookie(客戶端會話跟蹤技術(shù))
數(shù)據(jù)存儲在客戶端瀏覽器當(dāng)中
Session(服務(wù)端會話跟蹤技術(shù))
數(shù)據(jù)存儲在儲在服務(wù)端
Token令牌技術(shù)
2.2.2 會話跟蹤方案
上面我們介紹了什么是會話,什么是會話跟蹤,并且也提到了會話跟蹤 3 種常見的技術(shù)方案。接下來,我們就來對比一下這 3 種會話跟蹤的技術(shù)方案,來看一下具體的實現(xiàn)思路,以及它們之間的優(yōu)缺點。 ?
2.2.2.1 方案一 - Cookie
Cookie 是客戶端會話跟蹤技術(shù),它是存儲在客戶端瀏覽器的,我們使用 Cookie 來跟蹤會話,我們就可以在客戶端瀏覽器第一次發(fā)起請求,來請求服務(wù)器的時候,我們在服務(wù)器端來設(shè)置一個Cookie。 ?
比如第一次請求了登錄接口,登錄接口執(zhí)行完成之后,我們就可以設(shè)置一個Cookie,在 Cookie 當(dāng)中我們就可以來存儲用戶相關(guān)的一些數(shù)據(jù)信息。比如我可以在Cookie 當(dāng)中來存儲當(dāng)前登錄用戶的用戶名,用戶的ID。
服務(wù)器端在給客戶端瀏覽器在響應(yīng)數(shù)據(jù)的時候,會自動的將 Cookie 響應(yīng)給客戶端瀏覽器,客戶端瀏覽器接收到響應(yīng)回來的 Cookie 之后,會自動的將 Cookie 的值存儲在客戶端瀏覽器本地。接下來在后續(xù)的每一次請求當(dāng)中,都會將瀏覽器本地所存儲的 Cookie 自動地攜帶到服務(wù)器端。
接下來在服務(wù)端我們就可以獲取到 Cookie 的值。我們可以去判斷一下這個 Cookie 的值是否存在,如果不存在這個Cookie,就說明客戶端之前是沒有訪問登錄接口的;如果存在 Cookie 的值,就說明客戶端之前已經(jīng)登錄完成了。這樣我們就可以基于 Cookie 在同一次會話的不同請求之間來共享數(shù)據(jù)。?
我剛才在介紹流程的時候,用了 3 個自動:
-
服務(wù)器會 自動 的將 Cookie 響應(yīng)給客戶端瀏覽器。
-
客戶端瀏覽器接收到響應(yīng)回來的數(shù)據(jù)之后,會 自動 的將 Cookie 存儲在瀏覽器本地。
-
在后續(xù)的請求當(dāng)中,瀏覽器會 自動 的將 Cookie 攜帶到服務(wù)器端。
為什么這一切都是自動化進行的?
是因為 Cookie 它是 HTTP 協(xié)議當(dāng)中所支持的技術(shù),而各大瀏覽器廠商都支持了這一標(biāo)準(zhǔn)。在 HTTP 協(xié)議當(dāng)中給我們提供了一個響應(yīng)頭和請求頭:
-
響應(yīng)頭 Set-Cookie :設(shè)置 / 響應(yīng)Cookie數(shù)據(jù)的,也就是服務(wù)器端通過響應(yīng)頭Set-Cookie自動的將Cookie數(shù)據(jù)響應(yīng)給客戶端瀏覽器,服務(wù)器端向客戶端瀏覽器所發(fā)送到的Cookie數(shù)據(jù),并且瀏覽器會將Cookie,存儲在瀏覽器端。
-
請求頭 Cookie:攜帶 / 獲取Cookie數(shù)據(jù)的,也就是服務(wù)器端之前所發(fā)送回來的Cookie的信息,說白了就是客戶端瀏覽器通過請求頭Cookie自動的將Cookie數(shù)據(jù)傳遞給 / 攜帶到服務(wù)器端的。
請求頭也叫請求報頭;響應(yīng)頭也叫響應(yīng)報頭。??
提問:服務(wù)器端在給瀏覽器響應(yīng)Cookie的時候,是通過哪種方式響應(yīng)回去的?
答:服務(wù)器端在給瀏覽器響應(yīng)Cookie的時候,是通過響應(yīng)頭響應(yīng)回去的,直接設(shè)置了一個響應(yīng)頭Set-Cookie。?
Set-Cookie:name=value{前面的name就是Cookie的名稱,后面的Value就是Cookie的值}?
服務(wù)器端將響應(yīng)頭返回給客戶端瀏覽器,客戶端瀏覽器會自動的解析這個響應(yīng)頭,然后拿到響應(yīng)頭對應(yīng)的數(shù)據(jù)部分,也就是這個Cookie,然后將Cookie的值存儲在客戶端瀏覽器本地;接下來在后續(xù)的每次請求當(dāng)中,都會將客戶端瀏覽器本地所存儲的對應(yīng)的Cookie的值通過請求頭攜帶到服務(wù)端。?
以上就是基于Cookie這種會話跟蹤的技術(shù)方案進行會話跟蹤的整個流程。??
總結(jié):Cookie在進行會話跟蹤的時候,最為核心的就是一個請求頭Cookie和一個響應(yīng)頭Set-Cookie。?
而在Tomcat這一類的Web服務(wù)器當(dāng)中,也提供了Cookie操作的API,可以很方便的來設(shè)置Cookie以及獲取Cookie。
@Slf4j
@RestController
public class SessionController {
//設(shè)置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //設(shè)置Cookie/響應(yīng)Cookie
return Result.success();
}
//獲取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //輸出name為login_username的cookie
}
}
return Result.success();
}
}
Cookie這種會話跟蹤技術(shù)的優(yōu)缺點:
-
優(yōu)點:Cookie是HTTP協(xié)議中支持的技術(shù)(像Set-Cookie 響應(yīng)頭的解析以及 Cookie 請求頭數(shù)據(jù)的攜帶,都是瀏覽器自動進行的,是無需我們手動操作的)
-
缺點:
-
移動端APP(Android安卓端、IOS端)中無法使用Cookie
-
Cookie它是存儲在客戶端瀏覽器的,這也就導(dǎo)致了Cookie當(dāng)中所存儲的數(shù)據(jù)是不安全的,一些敏感的信息在進行明文傳輸時是不安全的,因為我們在客戶端瀏覽器當(dāng)中通過F12在開發(fā)者工具當(dāng)中是能看到Cookie的,所以,在Cookie當(dāng)中我們只能存儲一些不敏感的數(shù)據(jù),而且用戶還可以自己禁用瀏覽器的Cookie,瀏覽器的Cookie一旦禁用,那么Cookie這種會話跟蹤技術(shù)也就失效了,沒法使用了
-
Cookie不能跨域
-
Cookie的請求大小是有限制的,一個單獨的Cookie的大小不能超過4KB大小(4096個字節(jié))。
-
跨域介紹:
現(xiàn)在的項目,大部分都是前后端分離的,前后端最終也會分開部署,前端程序和服務(wù)端程序是要獨立部署的,假設(shè)前端部署在服務(wù)器 192.168.150.200 上,端口 80 => Nginx服務(wù)器的端口號,后端部署在 192.168.150.100上,端口 8080 => Tomcat服務(wù)器的端口號
此時,要想訪問當(dāng)前項目,我們打開瀏覽器要訪問的是在前端服務(wù)器當(dāng)中所部署的前端工程,我們訪問到前端工程之后就可以看到前端頁面,接下來在前端頁面我們要開始執(zhí)行登錄操作,此時就要在瀏覽器當(dāng)中發(fā)起一個異步請求來訪問服務(wù)器端的接口,也就是:
-
我們打開瀏覽器直接訪問前端工程,訪問url:http://192.168.150.200/login.html
-
然后在該頁面發(fā)起請求到服務(wù)端,而服務(wù)端所在地址不再是localhost,而是服務(wù)器的IP地址192.168.150.100,假設(shè)訪問接口地址為:http://192.168.150.100:8080/login
-
那此時就存在跨域操作 / 跨域請求了,因為我們是在 http://192.168.150.200/login.html 這個頁面上請求訪問了http://192.168.150.100:8080/login 接口
-
此時如果服務(wù)器設(shè)置了一個Cookie,這個Cookie是不能使用的,因為Cookie無法跨域
區(qū)分跨域的三個維度:
協(xié)議
IP地址/域名/主機(說的是一個東西)
端口號
- 協(xié)議、主機和端口號都是用來確定一個資源的唯一標(biāo)識符。?
只要上述的三個維度有任何一個維度不同,那就是跨域操作!??
舉例:
http://192.168.150.200/login.html ----------> https://192.168.150.200/login [協(xié)議不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]
2.2.2.2 方案二 - Session?
Session,它是服務(wù)器端會話跟蹤技術(shù),所以它是存儲在服務(wù)器端的。而 Session 的底層其實就是基于Cookie來實現(xiàn)的。 ?
- 獲取Session ??
如果我們現(xiàn)在要基于 Session 來進行會話跟蹤,瀏覽器在第一次請求服務(wù)器的時候,我們就可以直接在服務(wù)器當(dāng)中來獲取到會話對象Session。
如果是第一次請求Session ,會話對象是不存在的,這個時候服務(wù)器會自動的創(chuàng)建一個會話對象Session ;如果存在,它會獲取到當(dāng)前這次請求對應(yīng)的Session。
而每一個會話對象Session ,它都有一個ID(示意圖中Session后面括號中的1,就表示ID),我們稱之為 Session 的ID。
- 響應(yīng)Cookie (JSESSIONID) ?
接下來,服務(wù)器端在給瀏覽器響應(yīng)數(shù)據(jù)的時候,它會將 Session 的 ID 通過 Cookie 響應(yīng)給瀏覽器。其實在響應(yīng)頭當(dāng)中增加了一個 Set-Cookie 響應(yīng)頭。這個 Set-Cookie 響應(yīng)頭對應(yīng)的值就是Cookie。 Cookie 的名字是固定的 - JSESSIONID, 代表的服務(wù)器端會話對象 Session 的 ID。瀏覽器接收到響應(yīng)數(shù)據(jù)之后,瀏覽器會自動識別這個響應(yīng)頭,然后自動將Cookie存儲在瀏覽器本地。
- 查找Session ?
接下來,在后續(xù)的每一次請求當(dāng)中,都會將 Cookie 的數(shù)據(jù)獲取出來,并且攜帶到服務(wù)端。
接下來服務(wù)器拿到JSESSIONID這個 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就會從眾多的 Session 當(dāng)中來找到當(dāng)前請求對應(yīng)的會話對象Session。 ?
Session依賴于名為JESSIONID的Cookie。
-
這樣我們就可以通過 Session 會話對象在同一次會話的多次請求之間來共享數(shù)據(jù)了。
-
好,這就是基于 Session 進行會話跟蹤的流程。
HttpSesion其實指的就是會話對象Session。
代碼測試
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存儲數(shù)據(jù)
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //從session中獲取數(shù)據(jù)
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
Session這種會話跟蹤技術(shù)的優(yōu)缺點:
優(yōu)點:Session它是服務(wù)器端的會話跟蹤技術(shù),所以Session的數(shù)據(jù)都是存儲在服務(wù)器端的,服務(wù)器端普通人是獲取不到的,是比較安全的。Session本身沒有嚴(yán)格的大小限制。
缺點:
- 分布式 / 服務(wù)器集群環(huán)境下無法直接使用Session
移動端APP(Android、IOS)中無法使用Cookie
用戶可以自己禁用Cookie
Cookie不能跨域
PS:Session 底層是基于Cookie實現(xiàn)的會話跟蹤,如果Cookie不可用,則該方案,也就失效了。
分布式 / 服務(wù)器集群環(huán)境為何無法使用Session?
-
首先第一點,我們現(xiàn)在所開發(fā)的項目,一般都不會只部署在一臺服務(wù)器上,因為一臺服務(wù)器會存在一個很大的問題,就是單點故障。所謂單點故障,指的就是一旦這臺服務(wù)器掛了,整個應(yīng)用都沒法訪問了。
-
所以在現(xiàn)在的企業(yè)項目開發(fā)當(dāng)中,最終部署的時候都是以集群的形式來進行部署,也就是同一個項目它會部署多份。比如這個項目我們現(xiàn)在就部署了 3 份。
而用戶在訪問的時候,到底訪問這三臺其中的哪一臺?
-
其實用戶在訪問的時候,他會訪問一臺前置的服務(wù)器,我們叫負(fù)載均衡服務(wù)器,我們在后面項目當(dāng)中會詳細講解。負(fù)載均衡服務(wù)器,它的作用就是將前端發(fā)起的請求均勻的分發(fā)給后面的這三臺服務(wù)器。
此時假如我們通過 Session 來進行會話跟蹤,可能就會存在這樣一個問題。用戶打開瀏覽器要進行登錄操作,此時會發(fā)起登錄請求。登錄請求到達負(fù)載均衡服務(wù)器,將這個請求轉(zhuǎn)給了第一臺 Tomcat 服務(wù)器。
Tomcat 服務(wù)器接收到請求之后,要獲取到會話對象Session。獲取到會話對象 Session 之后,要給瀏覽器響應(yīng)數(shù)據(jù),最終在給瀏覽器響應(yīng)數(shù)據(jù)的時候,就會攜帶這么一個 Cookie 的名字,就是 JSESSIONID ,下一次再請求的時候,是不是又會將 Cookie 攜帶到服務(wù)端?
好。此時假如又執(zhí)行了一次查詢操作,要查詢部門的數(shù)據(jù)。這次請求到達負(fù)載均衡服務(wù)器之后,負(fù)載均衡服務(wù)器將這次請求轉(zhuǎn)給了第二臺 Tomcat 服務(wù)器,此時他就要到第二臺 Tomcat 服務(wù)器當(dāng)中。根據(jù)JSESSIONID 也就是對應(yīng)的 Session 的 ID 值,要找對應(yīng)的 Session 會話對象。
我想請問在第二臺服務(wù)器當(dāng)中有沒有這個ID的會話對象 Session, 是沒有的。此時是不是就出現(xiàn)問題了?我同一個瀏覽器發(fā)起了 2 次請求,結(jié)果獲取到的不是同一個會話對象,這就是Session這種會話跟蹤方案它的缺點,在服務(wù)器集群環(huán)境下無法直接使用Session。
總結(jié):以上就是基于服務(wù)器端會話跟蹤技術(shù)Session來進行會話跟蹤它的流程以及它的優(yōu)缺點。?
面試題:Cookie與Session的區(qū)別?
- 除了上面總結(jié)的,還有一點:
?服務(wù)器的開銷
- 由于Session是保存在服務(wù)器端的,每個用戶都會產(chǎn)生一個Session,如果并發(fā)訪問的用戶非常多,會產(chǎn)生很多的Session,消耗大量的內(nèi)存。
- 而Cookie由于保存在客戶端瀏覽器上,所以不占用服務(wù)器資源。
大家會看到上面這兩種傳統(tǒng)的會話技術(shù),在現(xiàn)在的企業(yè)開發(fā)當(dāng)中是不是會存在很多的問題。 為了解決這些問題,在現(xiàn)在的企業(yè)開發(fā)當(dāng)中,基本上都會采用第三種方案,通過令牌技術(shù)來進行會話跟蹤。接下來我們就來介紹一下令牌技術(shù),來看一下令牌技術(shù)又是如何跟蹤會話的。 ?
2.2.2.3 方案三 - Token令牌技術(shù)
這里我們所提到的令牌,其實它就是一個用戶身份的標(biāo)識,看似很高大上,很神秘,其實本質(zhì)就是一個字符串。
如果通過令牌技術(shù)來跟蹤會話,我們就可以在瀏覽器發(fā)起請求。在請求登錄接口的時候,如果登錄成功,我就可以生成一個令牌,令牌就是用戶的合法身份憑證。接下來我在響應(yīng)數(shù)據(jù)的時候,我就可以直接將令牌響應(yīng)給前端。
接下來我們在前端程序當(dāng)中接收到令牌之后,就需要將這個令牌存儲起來。這個存儲可以存儲在 cookie 當(dāng)中,也可以存儲在其他的存儲空間(比如:存儲在瀏覽器當(dāng)中本地的存儲空間Local Storage當(dāng)中,Local Storage是瀏覽器的本地存儲,不僅是PC端,在移動端也是支持的,Local Storage的存儲格式是Key-Value鍵值對,Key就是當(dāng)前系統(tǒng)的Token令牌的名字,Value就是JWT令牌)當(dāng)中。
接下來,在后續(xù)的每一次請求當(dāng)中,都需要將令牌攜帶到服務(wù)端。攜帶到服務(wù)端之后,接下來我們就需要來校驗令牌的有效性。如果令牌是有效的,就說明用戶已經(jīng)執(zhí)行了登錄操作;如果令牌是無效的,就說明用戶之前并未執(zhí)行登錄操作。
此時,如果是在同一次會話的多次請求之間,我們想共享數(shù)據(jù),我們就可以將共享的數(shù)據(jù)存儲在令牌當(dāng)中就可以了。
通過令牌技術(shù)進行會話跟蹤的優(yōu)缺點:
優(yōu)點:
支持PC端、支持移動端,甚至小程序端都是支持的(因為現(xiàn)在并不需要將這個令牌必須保存在Cookie當(dāng)中,其它任何的存儲空間當(dāng)中都是可以的,你只需要在客戶端當(dāng)中將這個令牌存儲起來就可以了)
解決分布式集群環(huán)境下的認(rèn)證問題(即使你服務(wù)器端搭建的是一個集群,我通過這種方案也是OK的,因為我在服務(wù)器端并不需要存儲任何的數(shù)據(jù))
正是因為我在服務(wù)器端并不需要存儲任何的數(shù)據(jù),所以減輕了服務(wù)器端的存儲壓力(無需在服務(wù)器端存儲)
缺點:需要自己實現(xiàn)(包括令牌的生成、令牌的傳遞、令牌的校驗)我們怎么樣生成這個令牌?我們怎么樣將令牌存儲在客戶端瀏覽器以及我們怎么樣將令牌攜帶到服務(wù)端?這些,都是需要我們自己來實現(xiàn)的。在實際的開發(fā)當(dāng)中,也需要前端的開發(fā)人員,配合來實現(xiàn)。
當(dāng)我們把Token生成并且發(fā)送給客戶端之后,Token的生命周期就不由服務(wù)端掌控了,不由服務(wù)器端去控制了。
思考:這里大家會看到令牌是存儲在客戶端的,存儲在客戶端會不會不安全?用戶是不是就可以偽造令牌了?
回答:這個其實不用擔(dān)心,因為一旦令牌偽造了,那我們在服務(wù)器端校驗令牌的時候就會報錯,是會檢測到的。
針對于這三種方案,現(xiàn)在企業(yè)開發(fā)當(dāng)中使用的最多的就是第三種令牌技術(shù)進行會話跟蹤。而前面的這兩種傳統(tǒng)的方案,現(xiàn)在企業(yè)項目開發(fā)當(dāng)中已經(jīng)很少使用了。所以在我們的學(xué)習(xí)當(dāng)中,我們也將會采用令牌技術(shù)來解決案例項目當(dāng)中的會話跟蹤問題。
我們只需要在用戶登陸完成之后,我們生成一個JWT令牌,然后將這個JWT令牌下發(fā)給客戶端,客戶端將這個令牌存儲起來,然后在以后的每一次請求當(dāng)中,將這個令牌攜帶到服務(wù)端,服務(wù)器端接收到這個請求之后,對這個請求進行統(tǒng)一攔截,獲取到請求當(dāng)中攜帶到的令牌,然后校驗令牌的真?zhèn)?,看一下令牌是否是有效的,如果令牌是無效的,直接響應(yīng)錯誤結(jié)果;如果令牌是有效的,我們再讓它去訪問對應(yīng)的業(yè)務(wù)接口。?
2.3 JWT令牌
前面我們介紹了基于令牌技術(shù)來實現(xiàn)會話追蹤。這里所提到的令牌就是用戶身份的標(biāo)識,其本質(zhì)就是一個字符串。令牌的形式有很多,我們使用的是功能強大的 JWT令牌。
2.3.1 JWT介紹
JWT全稱:JSON Web Token,簡稱JWT?(官網(wǎng):)JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).https://jwt.io/
- 通過名字我們就可以看出來JWT一定和JSON格式的數(shù)據(jù)是有關(guān)系的,Token指的就是令牌。

JWT定義了一種簡潔的、自包含的格式,用于在通信雙方以JSON數(shù)據(jù)格式安全的傳輸信息。由于數(shù)字簽名的存在,這些信息是可靠的。 ?
- 簡潔:是指jwt就是一個簡單的字符串,可以在請求參數(shù)或者是請求頭當(dāng)中直接傳遞。
- 自包含:指的是jwt令牌,看似是一個隨機的字符串,但是我們是可以根據(jù)自身的需求在jwt令牌中存儲自定義的數(shù)據(jù)內(nèi)容。如:可以直接在jwt令牌中存儲用戶的相關(guān)信息。
- 簡單來講,jwt就是將原始的json數(shù)據(jù)格式進行了安全的封裝,這樣就可以直接基于jwt在通信雙方安全的進行信息傳輸了。
JWT的組成:(JWT令牌由三個部分組成,三個部分之間使用了兩個英文的點來分割)?
-
第一部分:Header(標(biāo)頭),頭部區(qū)域, 記錄令牌類型以及簽名算法等。而它的數(shù)據(jù)格式就是JSON數(shù)據(jù)格式。比如type指的就是令牌的類型為JWT,簽名算法就是alg,HS256就是簽名的算法,將來就會根據(jù)簽名算法對JWT令牌進行數(shù)字簽名。?例如:{"alg":"HS256","type":"JWT"},這是原始的JSON數(shù)據(jù)格式,進行了Base64編碼。
-
第二部分:Payload(有效載荷),攜帶一些自定義信息、默認(rèn)信息等。 比如我們可以根據(jù)自身的需求在JWT令牌當(dāng)中存儲自定義的數(shù)據(jù)內(nèi)容。當(dāng)然里面可能還會有一些默認(rèn)的信息,比如這個令牌的簽發(fā)日期,令牌的有效期/過期時間exp等等...第二個部分原始數(shù)據(jù)依然是JSON格式的數(shù)據(jù),也是進行了base64編碼。例如:{"id":"1","username":"Tom"}
-
第三部分:Signature(數(shù)字簽名),簽名的目的就是為了防止令牌Token被篡改、確保令牌的安全性。在進行數(shù)字簽名的時候,它會基于前面所指定的簽名算法來融入前面的header部分和payload部分,并且還要加入指定的密鑰,然后再通過指定的簽名算法來計算這個簽名。再次強調(diào):這個簽名是通過前面指定的簽名算法自動計算出來的,并不是Base64編碼,而且在這個簽名當(dāng)中,還會融入前面的header和payload部分的內(nèi)容。將header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。
- 簽名的目的就是為了防止jwt令牌被篡改,而正是因為jwt令牌最后一個部分?jǐn)?shù)字簽名的存在,所以整個jwt 令牌是非常安全可靠的。
- 一旦jwt令牌當(dāng)中任何一個部分、任何一個字符被篡改了,整個令牌在校驗的時候都會失敗,所以它是非常安全可靠的。
JWT是如何將原始的JSON格式數(shù)據(jù),轉(zhuǎn)變?yōu)樽址哪兀?/strong>
- 其實在生成JWT令牌時,會對JSON格式的數(shù)據(jù)進行一次編碼:進行base64編碼。
- Base64:是一種基于64個可打印的字符(A-Z a-z 0-9 + /)來表示二進制數(shù)據(jù)的編碼方式。既然能編碼,那也就意味著也能解碼。所使用的64個字符分別是A到Z、a到z、 0- 9,一個加號,一個正斜杠,加起來就是64個字符。任何數(shù)據(jù)經(jīng)過base64編碼之后,最終就會通過這64個字符來表示。當(dāng)然還有一個符號,那就是等號,等號它是一個補位的符號。
- 需要注意的是Base64是編碼方式,而不是加密方式。
JWT令牌的應(yīng)用場景?
- JWT令牌最典型的應(yīng)用場景就是登錄認(rèn)證。
-
在瀏覽器發(fā)起請求來執(zhí)行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
-
前端拿到j(luò)wt令牌之后,會將jwt令牌存儲起來。在后續(xù)的每一次請求中都會將jwt令牌攜帶到服務(wù)端。
-
服務(wù)端統(tǒng)一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗一下令牌是否是有效。如果有效,就直接放行進行請求的處理。
在JWT登錄認(rèn)證的場景中我們發(fā)現(xiàn),整個流程當(dāng)中涉及到兩步操作:
-
在登錄成功之后,要生成令牌。
-
每一次請求當(dāng)中,要接收令牌并對令牌進行校驗。
稍后我們再來學(xué)習(xí)如何來生成jwt令牌,以及如何來校驗jwt令牌。
2.3.2 生成和校驗JWT令牌
那簡單介紹了什么是JWT令牌以及JWT令牌的組成之后,接下來我們就來學(xué)習(xí)如何基于Java代碼來生成和校驗JWT令牌。?
首先我們先來實現(xiàn)JWT令牌的生成。
- 要想使用JWT令牌,需要先引入JWT的依賴: ?
<!-- JWT依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 在引入完JWT依賴后,就可以調(diào)用工具包中提供的API來完成JWT令牌的生成和校驗
- 工具類:Jwts
- 無論是JWT令牌的生成還是校驗,都需要用到工具類Jwts
生成JWT代碼實現(xiàn): ?
/**
生成JWT令牌
*/
@Test
public void testGenerateJwt() {
// 封裝JWT令牌當(dāng)中所存儲的自定義數(shù)據(jù)
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("name","Tom");
// 構(gòu)建JWT令牌,通過該方法可以讓你通過鏈?zhǔn)骄幊虂砼渲肑WT的各個部分,來設(shè)置JWT令牌在生成的時候所需要設(shè)置的一些參數(shù)
// 比如設(shè)置數(shù)字簽名對應(yīng)的算法,以及生成數(shù)字簽名時指定的密鑰,以及在JWT令牌當(dāng)中要存儲的一些自定義的數(shù)據(jù),都是需要在生成令牌的時候來指定的
String jwt = Jwts.builder()
// 設(shè)置簽名算法:指定數(shù)字簽名的算法 在進行數(shù)字簽名時指定的密鑰,密鑰就是一個字符串
.signWith(SignatureAlgorithm.HS256,"success")
// 設(shè)置自定義的數(shù)據(jù),JWT令牌的第二個部分:Payload有效載荷,指定在生成JWT令牌時JWT令牌當(dāng)中所存儲的內(nèi)容,也就是我們自定義的數(shù)據(jù)
// 在Java程序當(dāng)中可以把自定義的數(shù)據(jù)封裝到Map集合當(dāng)中,也就是Key-Value形式的鍵值對
.setClaims(claims)
// 設(shè)置JWT令牌的有效期為1個小時:令牌的有效期指的就是這個令牌在什么時間范圍內(nèi)有效,一旦超過這個有效期,令牌就會失效
// 拿到當(dāng)前時間的毫秒值
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
// 調(diào)用compact()方法之后就可以拿到一個字符串類型的返回值,這個字符串類型的返回值就是我們所生成的JWT令牌
// 說白了就是調(diào)用compact()方法來獲取最終的JWT字符串
.compact();
// 將令牌輸出到控制臺
System.out.println(jwt);
}
運行測試方法: ?
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTY5MzEwOTQ3OX0.FzJbatpMm2FjiqTRDU_JC2RXSkZdvwonVknPjGOxG6c?
- 輸出的結(jié)果就是生成的JWT令牌,,通過英文的點對三個部分進行分割,我們可以將生成的令牌復(fù)制一下,然后打開JWT的官網(wǎng),將生成的令牌直接放在Encoded位置,此時就會自動的將令牌解析出來。 ?
- 第一部分解析出來,看到JSON格式的原始數(shù)據(jù),所使用的簽名算法為HS256。
- 第二個部分是我們自定義的數(shù)據(jù),之前我們自定義的數(shù)據(jù)就是id,還有一個exp代表的是我們所設(shè)置的JWT令牌的過期時間 / 有效期。
- 由于前兩個部分是base64編碼,所以是可以直接解碼出來。但最后一個部分并不是base64編碼,是經(jīng)過簽名算法計算出來的,所以最后一個部分是不會解析的。
實現(xiàn)了JWT令牌的生成,下面我們接著使用Java代碼來校驗JWT令牌(解析生成的令牌): ?
校驗/解析JWT令牌:?
/**
* 校驗/解析JWT令牌
*/
@Test // 進行單元測試的注解
public void testParseJwt() {
Claims claims = Jwts.parser()
// 指定簽名密鑰(必須保證和生成令牌時使用的簽名密鑰相同)
.setSigningKey("success")
// 傳遞要解析的JWT令牌
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTY5MzEwOTQ3OX0.FzJbatpMm2FjiqTRDU_JC2RXSkZdvwonVknPjGOxG6c")
// 獲取JWT令牌中自定義的內(nèi)容
.getBody();
System.out.println(claims);
}
運行測試方法后得到解析結(jié)果:
結(jié)論:如果篡改令牌中的任何一個字符,在對令牌進行解析時都會報錯,所以JWT令牌是非常安全可靠的。 ?
通過以上測試,我們在使用JWT令牌時需要注意:
-
JWT校驗時使用的簽名秘鑰,必須和生成JWT令牌時使用的秘鑰是配套的。
-
如果JWT令牌解析校驗時報錯,則說明 JWT令牌被篡改 或 失效了,令牌非法。
2.3.3 登錄下發(fā)令牌
JWT令牌的生成和校驗的基本操作我們已經(jīng)學(xué)習(xí)完了,接下來我們就需要在案例當(dāng)中通過JWT令牌技術(shù)來跟蹤會話。具體的思路我們前面已經(jīng)分析過了,主要就是兩步操作: ?
-
生成令牌
-
在登錄成功之后來生成一個JWT令牌,并且把這個令牌直接返回給前端,前端就需要將該令牌存儲起來,然后在后續(xù)的請求當(dāng)中,每一次請求時都需要將這個令牌攜帶到服務(wù)端
-
-
校驗令牌(解析令牌)
-
服務(wù)端攔截前端請求,從請求中獲取到令牌,對令牌進行解析校驗:如果令牌不存在或者是令牌解析錯誤(令牌被篡改),直接給前端響應(yīng)一個未登錄的錯誤結(jié)果,然后前端會自動地跳轉(zhuǎn)到登錄頁面;如果說令牌存在,并且令牌校驗也通過了,就說明這個令牌時有效的,然后我們直接放行,讓它去執(zhí)行對應(yīng)的業(yè)務(wù)操作即可
-
那我們首先來完成:登錄成功之后生成JWT令牌(令牌的生成),并且把JWT令牌返回給前端(令牌的下發(fā))。
JWT令牌怎么返回給前端呢?
- 此時我們就需要再來看一下接口文檔當(dāng)中關(guān)于登錄接口的描述(主要看響應(yīng)數(shù)據(jù)): ?
響應(yīng)數(shù)據(jù)
- 參數(shù)格式:application/json
參數(shù)說明:
名稱 | 類型 | 是否必須 | 默認(rèn)值 | 備注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必須 | 響應(yīng)碼, 1 成功 ; 0 失敗 | ||
msg | string | 非必須 | 提示信息 | ||
data | string | 必須 | 返回的數(shù)據(jù) , jwt令牌 |
響應(yīng)數(shù)據(jù)樣例:
3.1.4 備注說明
解讀完接口文檔中的描述了,目前我們先來完成令牌的生成和令牌的下發(fā),我們只需要生成一個令牌返回給前端就可以了。 ?
實現(xiàn)步驟:
-
引入JWT令牌操作的工具類
-
在項目工程下創(chuàng)建com.gch.utils包,并把提供JWT工具類復(fù)制到該包下
-
-
登錄成功后,調(diào)用工具類生成JWT令牌,并返回
JWT工具類
package com.gch.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
/**
* JWT令牌操作的工具類
*/
public class JwtUtils {
/** 簽名密鑰 */
private static String signKey = "itheima";
/** JWT令牌的有效期/有效時間 */
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @param claims JWT第二部分負(fù)載 payload 中存儲的內(nèi)容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
// 自定義信息(有效載荷)
.addClaims(claims)
// 簽名算法
.signWith(SignatureAlgorithm.HS256, signKey)
// 過期時間
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析/校驗JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分負(fù)載 payload 中存儲的內(nèi)容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
// 指定簽名密鑰
.setSigningKey(signKey)
// 指定令牌Token
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
改造之前的Controller代碼:
package com.gch.controller;
@Slf4j
@RestController
@RequestMapping("/login")
/**
登錄功能控制器
*/
public class LoginController {
@Autowired
private EmpService empService;
/**
* 處理登錄請求
* @param emp 員工對象
* @return 響應(yīng)
*/
@PostMapping
public Result login(@RequestBody Emp emp) {
// 1.記錄日志
log.info("處理該用戶登錄請求,username:{}, password:{}", emp.getUsername(), emp.getPassword());
// 2.調(diào)用service進行查詢,查詢/校驗該用戶信息是否存在
Emp e = empService.login(emp);
// 登錄成功 => 生成令牌并下發(fā)令牌
if(e != null){
// 封裝JWT令牌當(dāng)中所存儲的自定義數(shù)據(jù)
Map<String,Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
// 生成JWT令牌,jwt當(dāng)中就包含了當(dāng)前登錄的員工信息
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
// 登錄失敗 => 返回錯誤信息
return Result.error("用戶名或密碼錯誤");
}
}
重啟服務(wù),打開Postman測試登錄接口:
登錄請求完成后,可以看到服務(wù)端已經(jīng)生成了JWT令牌并且將JWT令牌已經(jīng)響應(yīng)給了前端,此時前端就會將JWT令牌存儲在瀏覽器本地。
接下來在后續(xù)的請求當(dāng)中,前端都會在請求頭當(dāng)中來攜帶JWT令牌到服務(wù)端,而服務(wù)端需要來統(tǒng)一攔截所有的請求,來判斷是否攜帶的有合法的JWT令牌。?
怎樣來統(tǒng)一攔截到所有的請求,來驗證令牌的有效性?
統(tǒng)一攔截的兩種解決方案:
-
Filter過濾器
-
Interceptor攔截器
我們首先來學(xué)習(xí)過濾器Filter。
2.4 過濾器Filter
什么是Filter?
-
Filter表示過濾器,是 JavaWeb三大組件(Servlet、Filter過濾器、Listener監(jiān)聽器)之一。對于這三大組件來說,Servlet以及Listener現(xiàn)在已經(jīng)很少使用了,而現(xiàn)在唯一使用比較多的就是Filter過濾器。
-
過濾器可以把對資源的請求攔截下來,從而實現(xiàn)一些特殊的功能
-
使用了過濾器之后,要想訪問Web服務(wù)器上的資源,必須先經(jīng)過濾器,過濾器處理完畢之后,才可以訪問對應(yīng)的資源。資源訪問完畢之后,它還會再回到過濾器,然后再給瀏覽器響應(yīng)對應(yīng)的數(shù)據(jù)。
-
-
過濾器一般完成一些通用的操作,比如:登錄校驗、統(tǒng)一編碼處理、敏感字符處理等。
- 我們拿登錄校驗來說,如果沒有過濾器,?我們是需要在每一個接口方法當(dāng)中都需要來編寫登錄校驗的邏輯,導(dǎo)致這一部分的邏輯重復(fù)編寫多次,代碼繁瑣,可讀性變差。
- 而現(xiàn)在有了過濾器就不需要那么繁瑣了,此時我們就可以直接將登錄校驗的邏輯直接定義在過濾器Filter當(dāng)中,我只需要定義這么一次就可以了。
下面我們通過Filter快速入門程序掌握過濾器的基本使用操作:
-
第1步,定義過濾器 :1.定義一個類,實現(xiàn) Filter 接口,并重寫其所有方法。
-
第2步,配置過濾器:Filter類上加 @WebFilter 注解,配置攔截資源的路徑。引導(dǎo)類上加 @ServletComponentScan 開啟Servlet組件支持。
定義過濾器 ?
@WebFilter(urlPatterns = "/*") //配置過濾器要攔截的請求路徑( /* 表示攔截瀏覽器的所有請求 )
//定義一個類,實現(xiàn)一個標(biāo)準(zhǔn)的Filter過濾器的接口
public class DemoFilter implements Filter {
@Override //初始化方法, Web服務(wù)器啟動,創(chuàng)建Filter時調(diào)用,只調(diào)用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法執(zhí)行了");
}
@Override //攔截到請求之后調(diào)用, 調(diào)用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 攔截到了請求...放行前邏輯");
//放行操作
chain.doFilter(request,response);
}
@Override //銷毀方法, 服務(wù)器關(guān)閉時調(diào)用,只調(diào)用一次
public void destroy() {
System.out.println("destroy 銷毀方法執(zhí)行了");
}
}
init方法:過濾器的初始化方法。在web服務(wù)器啟動的時候會自動的創(chuàng)建Filter過濾器對象,在創(chuàng)建過濾器對象的時候會自動調(diào)用init初始化方法,這個方法只會被調(diào)用一次。
doFilter方法:這個方法是在每一次攔截到請求之后都會被調(diào)用,所以這個方法是會被調(diào)用多次的,每攔截到一次請求就會調(diào)用一次doFilter()方法。
destroy方法: 與init對應(yīng)的另外一種方法,是銷毀的方法。當(dāng)我們關(guān)閉服務(wù)器的時候,它會自動的調(diào)用銷毀方法destroy,而這個銷毀方法也只會被調(diào)用一次。
說明:IDEA之所以默認(rèn)只選擇了doFilter()方法,是因為init初始化以及的destory銷毀這兩個方法并不常用,所以在Filter接口當(dāng)中,針對于這兩個方法已經(jīng)提供了默認(rèn)實現(xiàn),所以在這里我們可以不用實現(xiàn)init和的destory這兩個方法。
- 在定義完Filter之后,F(xiàn)ilter其實并不會生效,還需要完成Filter的配置,F(xiàn)ilter的配置非常簡單,只需要在Filter類上添加一個注解:@WebFilter,通過該注解來標(biāo)識當(dāng)前是一個過濾器組件,并指定屬性urlPatterns,通過這個屬性指定過濾器要攔截哪些請求。??
當(dāng)我們在Filter類上面加了@WebFilter注解之后,接下來我們還需要在啟動類上面加上一個注解@ServletComponentScan,因為Filter是Java Web三大組件之一,并不是SpringBoot當(dāng)中提供的,通過這個@ServletComponentScan注解來開啟SpringBoot項目對于Servlet組件的支持。 ?
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
重新啟動服務(wù),打開瀏覽器,執(zhí)行部門管理的請求,可以看到控制臺輸出了過濾器中的內(nèi)容: ?
一旦攔截到請求,它就會自動的調(diào)用doFilter()方法。?
注意事項:
- 在過濾器Filter中,如果不執(zhí)行放行操作,將無法訪問后面的資源。
- 放行操作:調(diào)用FilterChain當(dāng)中的chain.doFilter(request, response);參數(shù):請求對象,響應(yīng)對象
總結(jié):?
2.4.2 Filter詳解
Filter過濾器的快速入門程序我們已經(jīng)完成了,接下來我們就要詳細的介紹一下過濾器Filter在使用中的一些細節(jié)。主要介紹以下3個方面的細節(jié):
-
過濾器的執(zhí)行流程
-
過濾器的攔截路徑配置
-
過濾器鏈
2.4.2.1 執(zhí)行流程
首先我們先來看下過濾器的執(zhí)行流程:
過濾器當(dāng)中我們攔截到了請求之后,如果希望繼續(xù)訪問后面的web資源,就要執(zhí)行放行操作,放行就是調(diào)用 FilterChain對象當(dāng)中的doFilter()方法,在調(diào)用doFilter()這個方法之前所編寫的代碼屬于放行之前的邏輯。 ?
在放行后訪問完 web 資源之后還會回到過濾器當(dāng)中,回到過濾器之后如有需求還可以執(zhí)行放行之后的邏輯,放行之后的邏輯我們寫在doFilter()這行代碼之后。
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override //初始化方法, 只調(diào)用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法執(zhí)行了");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("DemoFilter 放行前邏輯.....");
//放行請求
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("DemoFilter 放行后邏輯.....");
}
@Override //銷毀方法, 只調(diào)用一次
public void destroy() {
System.out.println("destroy 銷毀方法執(zhí)行了");
}
}
2.4.2.2 攔截路徑
執(zhí)行流程我們搞清楚之后,接下來再來介紹一下過濾器的攔截路徑,Filter可以根據(jù)需求,配置不同的攔截資源路徑:
攔截路徑 | urlPatterns值 | 含義 |
---|---|---|
攔截具體路徑 | /login | 只有訪問 /login 路徑時,才會被攔截 |
目錄攔截 | /emps/* | 訪問/emps下的所有資源,都會被攔截 |
攔截所有 | /* | 訪問所有資源,都會被攔截 |
下面我們來測試"攔截具體路徑":
@WebFilter(urlPatterns = "/login") ?//攔截/login具體路徑
@WebFilter(urlPatterns = "/depts/*") //攔截所有以/depts開頭,后面是什么無所謂?
2.4.2.3 過濾器鏈
最后我們在來介紹下過濾器鏈 ?
什么是過濾器鏈呢?
- 所謂過濾器鏈指的是在一個web應(yīng)用程序當(dāng)中,可以配置多個過濾器,多個過濾器就形成了一個過濾器鏈。 ?
比如:在我們web服務(wù)器當(dāng)中,定義了兩個過濾器,這兩個過濾器就形成了一個過濾器鏈。
- 而這個鏈上的過濾器在執(zhí)行的時候會一個一個的執(zhí)行,會先執(zhí)行第一個Filter,放行之后再來執(zhí)行第二個Filter,如果執(zhí)行到了最后一個過濾器放行之后,才會訪問對應(yīng)的web資源。
- 訪問完web資源之后,按照我們剛才所介紹的過濾器的執(zhí)行流程,還會回到過濾器當(dāng)中來執(zhí)行過濾器放行后的邏輯。
- 而在執(zhí)行放行后的邏輯的時候,順序是反著的,先要執(zhí)行過濾器2放行之后的邏輯,再來執(zhí)行過濾器1放行之后的邏輯,最后在給瀏覽器響應(yīng)數(shù)據(jù)。
說明:?
通過控制臺日志的輸出,大家發(fā)現(xiàn)AbcFilter先執(zhí)行,DemoFilter后執(zhí)行,這是為什么呢?
- 其實是和過濾器的類名有關(guān)系。
- 順序:以注解方式配置的Filter過濾器,它的執(zhí)行優(yōu)先級是按過濾器類名(字符串)的自動排序確定的,類名排名越靠前,優(yōu)先級越高。
- 假如我們想讓DemoFilter先執(zhí)行,怎么辦呢?答案就是修改類名。
- 修改AbcFilter類名為XbcFilter
2.4.3 登錄校驗-Filter
2.4.3.1 分析
過濾器Filter的快速入門以及使用細節(jié)我們已經(jīng)介紹完了,接下來最后一步,我們需要使用過濾器Filter來完成案例當(dāng)中的登錄校驗功能。
我們先來回顧下前面分析過的登錄校驗的基本流程:
要進入到后臺管理系統(tǒng),我們必須先完成登錄操作,此時就需要訪問登錄接口login。
登錄成功之后,我們會在服務(wù)端生成一個JWT令牌,并且把JWT令牌返回給前端,前端會將JWT令牌存儲下來。
在后續(xù)的每一次請求當(dāng)中,都會將JWT令牌攜帶到服務(wù)端,請求到達服務(wù)端之后,要想去訪問對應(yīng)的業(yè)務(wù)功能,此時我們必須先要校驗令牌的有效性。
對于校驗令牌的這一塊操作,我們使用登錄校驗的過濾器,在過濾器當(dāng)中來校驗令牌的有效性。如果令牌是無效的,就響應(yīng)一個錯誤的信息,也不會再去放行訪問對應(yīng)的資源了。如果令牌存在,并且它是有效的,此時就會放行去訪問對應(yīng)的web資源,執(zhí)行相應(yīng)的業(yè)務(wù)操作。
大概清楚了在Filter過濾器的實現(xiàn)步驟了,那在正式開發(fā)登錄校驗過濾器之前,我們思考兩個問題:
所有的請求,攔截到了之后,都需要校驗令牌嗎?
答案:登錄請求例外
攔截到請求后,什么情況下才可以放行,執(zhí)行業(yè)務(wù)操作?
答案:有令牌,且令牌校驗通過(合法);否則都返回未登錄錯誤結(jié)果
2.4.3.2 具體流程
我們要完成登錄校驗,主要是利用Filter過濾器實現(xiàn),而登錄校驗Filter過濾器的流程步驟:
基于上面的業(yè)務(wù)流程,我們分析出具體的操作步驟:
-
獲取請求url
-
判斷請求url中是否包含login,如果包含,說明是登錄操作,放行
-
獲取請求頭中的令牌(token)
-
判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(未登錄)
-
解析token,如果解析失敗,返回錯誤結(jié)果(未登錄)
-
放行
2.4.3.3 代碼實現(xiàn)
分析清楚了以上的問題后,我們就參照接口文檔來開發(fā)登錄功能了,登錄接口描述如下:
- 基本信息 ?
- 請求參數(shù)
參數(shù)格式:application/json
參數(shù)說明:
名稱 | 類型 | 是否必須 | 備注 |
---|---|---|---|
username | string | 必須 | 用戶名 |
password | string | 必須 | 密碼 |
請求數(shù)據(jù)樣例:
- 響應(yīng)數(shù)據(jù)
參數(shù)格式:application/json
參數(shù)說明:
名稱 | 類型 | 是否必須 | 默認(rèn)值 | 備注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必須 | 響應(yīng)碼, 1 成功 ; 0 失敗 | ||
msg | string | 非必須 | 提示信息 | ||
data | string | 必須 | 返回的數(shù)據(jù) , jwt令牌 |
響應(yīng)數(shù)據(jù)樣例:
- 備注說明
用戶登錄成功后,系統(tǒng)會自動下發(fā)JWT令牌,然后在后續(xù)的每次請求中,都需要在請求頭header中攜帶到服務(wù)端,請求頭的名稱為 token ,值為登錄時下發(fā)的JWT令牌。 => 前端
如果檢測到用戶未登錄,則會返回如下固定錯誤信息:
登錄校驗過濾器:LoginCheckFilter ?
package com.gch.filter;
import com.alibaba.fastjson2.JSONObject;
import com.gch.pojo.Result;
import com.gch.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*") //攔截所有請求
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//前置:強制轉(zhuǎn)換為http協(xié)議的請求對象、響應(yīng)對象 (轉(zhuǎn)換原因:要使用子類中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.獲取請求url
String url = request.getRequestURL().toString();
log.info("請求路徑url:{}", url); //請求路徑:http://localhost:8080/login
//2.判斷請求url中是否包含login,如果包含,說明是登錄操作,則執(zhí)行放行操作
if(url.contains("/login")){
log.info("登錄操作,放行...");
chain.doFilter(request, response);//放行請求
return;//結(jié)束當(dāng)前方法的執(zhí)行
}
//3.獲取請求頭中的令牌(token) 為什么填token,因為接口文檔中已經(jīng)說明了請求頭名稱為token
String token = request.getHeader("token");
log.info("從請求頭中獲取的令牌:{}",token);
//4.判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(未登錄)
if(!StringUtils.hasLength(token)){ // StringUtils是Spring當(dāng)中提供的一個工具類
log.info("請求頭token為空,Token不存在");
Result responseResult = Result.error("NOT_LOGIN");
//響應(yīng)數(shù)據(jù)要為JSON數(shù)據(jù)格式,由于之前都是在Controller當(dāng)中操作的,加了@RestController注解后會自動的將方法的返回值轉(zhuǎn)為JSON格式
//但是我們現(xiàn)在是在過濾器當(dāng)中,并不是在Controller當(dāng)中,所以我們現(xiàn)在要手動轉(zhuǎn)換 對象 => JSON
//把Result對象轉(zhuǎn)換為JSON格式字符串 ===> 阿里巴巴fastJSON(fastjson是阿里巴巴提供的用于實現(xiàn)對象和json的轉(zhuǎn)換工具類)
String json = JSONObject.toJSONString(responseResult);
//將響應(yīng)的內(nèi)容類型設(shè)置為JSON格式,并且使用UTF-8字符編碼。
response.setContentType("application/json;charset=utf-8");
//響應(yīng)
response.getWriter().write(json);
return;
}
//5.校驗JWT令牌,解析token,如果解析失敗,返回錯誤結(jié)果(未登錄)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){ // jwt令牌解析失敗
log.info("令牌解析失敗!");
Result responseResult = Result.error("NOT_LOGIN");
//把Result對象轉(zhuǎn)換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實現(xiàn)對象和json的轉(zhuǎn)換工具類)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//響應(yīng)
response.getWriter().write(json);
return;
}
//6.放行
log.info("令牌合法,放行");
chain.doFilter(request, response);
}
}
在上述過濾器的功能實現(xiàn)中,我們使用到了一個第三方j(luò)son處理的工具包fastjson。我們要想使用,需要引入如下依賴: ?
<!-- 阿里巴巴fastJSON,用來進行JSON格式轉(zhuǎn)換的工具包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
登錄校驗的過濾器我們編寫完成了,接下來我們就可以重新啟動服務(wù)來做一個測試:
測試前先把之前所編寫的測試使用的過濾器,暫時注釋掉。直接將@WebFilter注解給注釋掉即可。
2.5 攔截器Interceptor
學(xué)習(xí)完了過濾器Filter之后,接下來我們繼續(xù)學(xué)習(xí)攔截器Interseptor。
攔截器我們主要分為三個方面進行講解:
-
介紹下什么是攔截器,并通過快速入門程序上手?jǐn)r截器
-
攔截器的使用細節(jié)
-
通過攔截器Interceptor完成登錄校驗功能
我們先學(xué)習(xí)第一塊內(nèi)容:攔截器快速入門
2.5.1 快速入門
什么是攔截器?
-
攔截器是一種動態(tài)攔截方法調(diào)用的機制,類似于過濾器Filter。
-
攔截器是Spring框架中提供的,用來動態(tài)攔截控制器方法的執(zhí)行,也就是Controller方法的執(zhí)行。
攔截器的作用:
-
攔截請求,攔截到請求之后就可以在指定方法調(diào)用前后,根據(jù)業(yè)務(wù)需要執(zhí)行預(yù)先設(shè)定的代碼。
在攔截器當(dāng)中,我們通常也是做一些通用性的操作,比如:我們可以通過攔截器來攔截前端發(fā)起的請求,將登錄校驗的邏輯全部編寫在攔截器當(dāng)中。在校驗的過程當(dāng)中,如發(fā)現(xiàn)用戶登錄了(攜帶了JWT令牌且是合法令牌),就可以直接放行,去訪問Spring當(dāng)中的資源。如果校驗時發(fā)現(xiàn)并沒有登錄或是非法令牌,就可以直接給前端響應(yīng)未登錄的錯誤信息。
攔截器Interceptor快速入門?
下面我們通過快速入門程序,來學(xué)習(xí)下攔截器的基本使用。攔截器的使用步驟和過濾器類似,也分為兩步:
-
定義攔截器,實現(xiàn)攔截器的標(biāo)準(zhǔn)接口 - HandlerInterceptor接口,并重寫其中所有方法(一共有三個方法,并且這三個方法都有默認(rèn)實現(xiàn),我們可以根據(jù)自己的需要來重寫其中的方法)。
-
注冊配置攔截器:首先自定義一個類,我們稱之為配置類,讓配置類去實現(xiàn)接口WebMvcConfigurer,重寫addInterceotor()方法,并且在配置類上加上注解@Configuration,來標(biāo)識當(dāng)前類是Spring當(dāng)中的一個配置類。
自定義攔截器:實現(xiàn)HandlerInterceptor接口,并重寫其所有方法 ?
package com.gch.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定義登錄校驗攔截器
*/
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
/**
* 目標(biāo)資源方法執(zhí)行前執(zhí)行,這里的目標(biāo)資源方法指的就是Controller當(dāng)中的方法,會在Controller方法運行之前運行
* @return 返回值類型為boolean 返回true:代表放行,就代表它可以去運行Controller當(dāng)中的方法了
* 返回false:不放行,代表攔截住了,不允許執(zhí)行Controller當(dāng)中的方法
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
//true表示放行
return true;
}
/**
* 目標(biāo)資源方法執(zhí)行后執(zhí)行,也就是Controller方法運行完成之后來運行
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}
/**
* 視圖渲染完畢后執(zhí)行,最后執(zhí)行的一個方法
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}
注冊配置攔截器:實現(xiàn)WebMvcConfigurer接口,并重寫addInterceptors方法?
package com.gch.config;
import com.gch.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 自定義配置類,注冊配置攔截器
*/
@Configuration // 該注解標(biāo)識當(dāng)前類是Spring當(dāng)中的配置類
public class WebConfig implements WebMvcConfigurer {
/** 自定義的攔截器對象 */
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
/**
* 注冊攔截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注冊自定義攔截器對象 設(shè)置攔截器攔截的請求路徑( /** 表示攔截所有請求)
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
重新啟動SpringBoot服務(wù),打開Postman測試:
2.5.2 Interceptor詳解
攔截器的入門程序完成之后,接下來我們來介紹攔截器的使用細節(jié)。攔截器的使用細節(jié)我們主要介紹兩個部分:
-
攔截器的攔截路徑配置
-
攔截器的執(zhí)行流程
2.5.2.1 攔截器 - 攔截路徑
首先我們先來看攔截器的攔截路徑的配置,在注冊配置攔截器的時候,我們要指定攔截器的攔截路徑,通過addPathPatterns("要攔截路徑")
方法,就可以指定要攔截哪些資源。
在入門程序中我們配置的是/**
,表示攔截所有資源,而在配置攔截器時,不僅可以指定要攔截哪些資源,還可以指定不攔截哪些資源,只需要調(diào)用excludePathPatterns("不攔截路徑")
方法,指定哪些資源不需要攔截。?

2.5.2.2 攔截器的執(zhí)行流程
介紹完攔截路徑的配置之后,接下來我們再來介紹攔截器的執(zhí)行流程。
通過執(zhí)行流程,大家就能夠清晰的知道過濾器與攔截器的執(zhí)行時機。
-
當(dāng)我們打開瀏覽器來訪問部署在web服務(wù)器當(dāng)中的web應(yīng)用時,此時我們所定義的過濾器會攔截到這次請求。攔截到這次請求之后,它會先執(zhí)行放行前的邏輯,然后再執(zhí)行放行操作。而由于我們當(dāng)前是基于springboot開發(fā)的,所以放行之后是進入到了spring的環(huán)境當(dāng)中,也就是要來訪問我們所定義的controller當(dāng)中的接口方法。
-
Tomcat并不識別所編寫的Controller程序,但是它識別Servlet程序,所以在Spring的Web環(huán)境中提供了一個非常核心的Servlet:DispatcherServlet(前端控制器),所有請求都會先進行到DispatcherServlet,再將請求轉(zhuǎn)給Controller。
-
當(dāng)我們定義了攔截器后,會在執(zhí)行Controller的方法之前,請求被攔截器攔截住。執(zhí)行
preHandle()
方法,這個方法執(zhí)行完成后需要返回一個布爾類型的值,如果返回true,就表示放行本次操作,才會繼續(xù)訪問controller中的方法;如果返回false,則不會放行(controller中的方法也不會執(zhí)行)。 -
在controller當(dāng)中的方法執(zhí)行完畢之后,再回過來執(zhí)行
postHandle()
這個方法以及afterCompletion()
方法,然后再返回給DispatcherServlet,最終再來執(zhí)行過濾器當(dāng)中放行后的這一部分邏輯的邏輯。執(zhí)行完畢之后,最終給瀏覽器響應(yīng)數(shù)據(jù)。
接下來我們就來演示下過濾器和攔截器同時存在的執(zhí)行流程: ?
以上就是攔截器的執(zhí)行流程。通過執(zhí)行流程分析,大家應(yīng)該已經(jīng)清楚了過濾器和攔截器之間的區(qū)別,其實它們之間的區(qū)別主要是兩點:
-
接口規(guī)范不同:過濾器需要實現(xiàn)Filter接口,而攔截器需要實現(xiàn)HandlerInterceptor接口。
-
攔截范圍不同:過濾器Filter會攔截所有的資源,而Interceptor攔截器它是Spring當(dāng)中提供的,它只會攔截Spring環(huán)境中的資源。
2.5.3 登錄校驗- Interceptor
通過攔截器來完成案例當(dāng)中的登錄校驗功能。
登錄校驗的業(yè)務(wù)邏輯以及操作步驟我們前面已經(jīng)分析過了,和登錄校驗Filter過濾器當(dāng)中的邏輯是完全一致的?,F(xiàn)在我們只需要把這個技術(shù)方案由原來的過濾器換成攔截器Interceptor就可以了。 ?
登錄校驗攔截器 ?
package com.gch.interceptor;
import com.alibaba.fastjson2.JSONObject;
import com.gch.pojo.Result;
import com.gch.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.event.WindowFocusListener;
/**
* 自定義登錄校驗攔截器
*/
@Slf4j
@Component // 當(dāng)前攔截器對象由Spring創(chuàng)建和管理
public class LoginCheckInterceptor implements HandlerInterceptor {
/**
* 目標(biāo)資源方法執(zhí)行前執(zhí)行,這里的目標(biāo)資源方法指的就是Controller當(dāng)中的方法,會在Controller方法運行之前運行
* @return 返回值類型為boolean 返回true:代表放行,就代表它可以去運行Controller當(dāng)中的方法了
* 返回false:不放行,代表攔截住了,不允許執(zhí)行Controller當(dāng)中的方法
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
// 1.獲取請求url
String url = request.getRequestURL().toString();
log.info("攔截到了請求,請求路徑url:{}",url);
// 2.判斷請求url中是否包含login,如果包含,說明是登錄操作,則執(zhí)行放行操作
if(url.contains("/login")){
log.info("登錄操作,放行...");
// true表示放行
return true;
}
// 3.獲取請求頭中的Token令牌
String token = request.getHeader("token");
log.info("從請求頭中獲取的令牌:{}",token);
// 4.判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(說明未登錄)
if(!StringUtils.hasLength(token)){
log.info("請求頭token令牌為空,Token不存在");
// 響應(yīng)錯誤結(jié)果
Result responseResult = Result.error("NOT_LOGIN");
// 將響應(yīng)結(jié)果的數(shù)據(jù)格式轉(zhuǎn)為JSON數(shù)據(jù)格式
String json = JSONObject.toJSONString(responseResult);
// 將響應(yīng)的內(nèi)容類型設(shè)置為JSON格式,并且使用UTF-8編碼
response.setContentType("application/json;charset=utf-8");
// 響應(yīng)
response.getWriter().write(json);
// 不放行
return false;
}
// 5.校驗JWT令牌,解析Token,如果解析失敗,返回錯誤結(jié)果
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("Token解析失敗...");
// 響應(yīng)錯誤結(jié)果
Result responseResult = Result.error("NOT_LOGIN");
// 將響應(yīng)結(jié)果的數(shù)據(jù)格式轉(zhuǎn)為JSON數(shù)據(jù)格式
String json = JSONObject.toJSONString(responseResult);
// 將響應(yīng)的內(nèi)容類型設(shè)置為JSON格式,并且使用UTF-8編碼
response.setContentType("application/json;charset=utf-8");
// 響應(yīng)
response.getWriter().write(json);
// 不放行
return false;
}
// 6.校驗JWT令牌成功,放行
log.info("Token合法,放行...");
//true表示放行
return true;
}
}
注冊配置攔截器 ?
package com.gch.config;
import com.gch.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 自定義配置類,注冊配置攔截器
*/
@Configuration // 該注解標(biāo)識當(dāng)前類是Spring當(dāng)中的配置類
public class WebConfig implements WebMvcConfigurer {
/** 自定義的攔截器對象 */
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
/**
* 注冊攔截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注冊自定義攔截器對象
registry.addInterceptor(loginCheckInterceptor)
// 設(shè)置攔截器攔截的請求路徑( /** 表示攔截所有請求)
.addPathPatterns("/**")
// 設(shè)置不攔截的請求路徑
.excludePathPatterns("/login");
}
}
說明:登錄校驗的過濾器和攔截器,我們只需要使用其中的一種就可以了。 ?
3. 異常處理
3.1 當(dāng)前問題
登錄功能和登錄校驗功能我們都實現(xiàn)了,下面我們學(xué)習(xí)下今天最后一塊技術(shù)點:異常處理。首先我們先來看一下系統(tǒng)出現(xiàn)異常之后會發(fā)生什么現(xiàn)象,再來介紹異常處理的方案。
我們打開瀏覽器,訪問系統(tǒng)中的新增部門操作,系統(tǒng)中已經(jīng)有了 "就業(yè)部" 這個部門,我們再來增加一個就業(yè)部,看看會發(fā)生什么現(xiàn)象。 ?
點擊確定之后,窗口關(guān)閉了,頁面沒有任何反應(yīng),就業(yè)部也沒有添加上。 而此時,大家會發(fā)現(xiàn),網(wǎng)絡(luò)請求報錯了。 ?
狀態(tài)碼為500,表示服務(wù)器端異常,我們打開idea,來看一下,服務(wù)器端出了什么問題。 ?
上述錯誤信息的含義是,dept部門表的name字段的值就業(yè)部重復(fù)了,因為在數(shù)據(jù)庫表dept中已經(jīng)有了就業(yè)部,我們之前設(shè)計這張表時,為name字段建議了唯一約束,所以該字段的值是不能重復(fù)的。
而當(dāng)我們再添加就業(yè)部,這個部門時,就違反了唯一約束,此時就會報錯。
我們來看一下出現(xiàn)異常之后,最終服務(wù)端給前端響應(yīng)回來的數(shù)據(jù)長什么樣。 ?
響應(yīng)回來的數(shù)據(jù)是一個JSON格式的數(shù)據(jù)。但這種JSON格式的數(shù)據(jù)還是我們開發(fā)規(guī)范當(dāng)中所提到的統(tǒng)一響應(yīng)結(jié)果Result嗎?
- 顯然并不是。由于返回的數(shù)據(jù)不符合開發(fā)規(guī)范,所以前端并不能解析出響應(yīng)的JSON數(shù)據(jù)。 ?
接下來我們需要思考的是出現(xiàn)異常之后,當(dāng)前案例項目的異常是怎么處理的?
-
答案:沒有做任何的異常處理
當(dāng)我們沒有做任何的異常處理時,我們?nèi)龑蛹軜?gòu)處理異常的方案:
-
Mapper接口在操作數(shù)據(jù)庫的時候出錯了,此時異常會往上拋(誰調(diào)用Mapper就拋給誰),會拋給Service。
-
Service 中也存在異常了,會拋給Controller。
-
而在Controller當(dāng)中,我們也沒有做任何的異常處理,所以最終異常會再往上拋。最終拋給框架之后,框架就會返回一個JSON格式的數(shù)據(jù),里面封裝的就是錯誤的信息,但是框架返回的JSON格式的數(shù)據(jù)并不符合我們的開發(fā)規(guī)范。
3.2 解決方案
那么在三層構(gòu)架項目中,出現(xiàn)了異常,該如何處理?
-
方案一:在所有Controller的所有方法中進行try…catch處理
-
缺點:雖然實現(xiàn)簡單,但是操作繁瑣,代碼臃腫(不推薦)
-
-
方案二:全局異常處理器
-
定義一個全局異常處理器來捕獲整個項目當(dāng)中所有的異常
-
好處:簡單、優(yōu)雅(推薦)
-
有了全局異常處理器之后,如果Mapper當(dāng)中遇到異常不用處理,直接拋給Service,Service當(dāng)中遇到異常不用處理,拋給Controller,Controller也不用處理,最終該異常就會交給全局異常處理器來處理,全局異常處理器處理完該異常之后,再給前端響應(yīng)標(biāo)準(zhǔn)的統(tǒng)一響應(yīng)結(jié)果Result,Result當(dāng)中來封裝錯誤的信息。
-
3.3 全局異常處理器
我們該怎么樣定義全局異常處理器?
-
定義全局異常處理器非常簡單,就是定義一個類,在類上加上一個注解@RestControllerAdvice,加上這個注解就代表我們定義了一個全局異常處理器。
-
在全局異常處理器當(dāng)中,需要定義一個方法來捕獲異常,在這個方法上需要加上注解@ExceptionHandler。通過@ExceptionHandler注解當(dāng)中的value屬性來指定我們要捕獲的是哪一類型的異常。
-
Exception.class就代表我們當(dāng)前要捕獲所有的異常,捕獲到異常之后,我們就可以在該方法中來處理異常。
提問: 我們響應(yīng)回去的是一個對象Result,而前端需要的是一個JSON格式的數(shù)據(jù),那這個Result對象是怎樣轉(zhuǎn)換成JSON格式的呢?
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody{將方法的返回值轉(zhuǎn)換為JSON然后再響應(yīng)回去給前端}
package com.gch.exception;
import com.gch.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局異常處理器
*/
@RestControllerAdvice // 代表我們定義一個全局異常處理器
public class GolobalExceptionHandler {
/**
* 捕獲并處理異常
* @param ex 異常對象
* @return
* @ExceptionHandler注解用于指定處理特定類型異常的方法,通過value屬性來指定我們要捕獲的是哪一類型的異常來進行處理
*/
@ExceptionHandler(Exception.class) // 捕獲所有異常
public Result handleException(Exception ex) {
// 打印堆棧中的異常對象
ex.printStackTrace();
// 捕獲到異常之后,響應(yīng)一個標(biāo)準(zhǔn)的Result
return Result.error("對不起,操作失敗,請聯(lián)系管理員");
}
}
以上就是全局異常處理器的使用,主要涉及到兩個注解:
-
@RestControllerAdvice //表示當(dāng)前類為全局異常處理器文章來源:http://www.zghlxwxcb.cn/news/detail-828157.html
-
@ExceptionHandler //指定可以捕獲哪種類型的異常進行處理文章來源地址http://www.zghlxwxcb.cn/news/detail-828157.html
到了這里,關(guān)于SpringBootWeb 登錄認(rèn)證[Cookie + Session + Token + Filter + Interceptor]的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!