文章目錄
- 案例-登錄認證
- 1. 登錄功能
- 1.1 需求
- 1.2 接口文檔
- 1.3 思路分析
- 1.4 功能開發
- 1.5 測試
- 2. 登錄校驗
- 2.1 問題分析
- 2.2 會話技術
- 2.2.1 會話技術介紹
- 2.2.2 會話跟蹤方案
- 2.2.2.1 方案一 - Cookie
- 2.2.2.2 方案二 - Session
- 2.2.2.3 方案三 - 令牌技術
- 2.2.3 JWT令牌(Token)
- 2.2.3.1 介紹
- 2.2.3.2 生成和校驗
- 2.2.3.3 登錄下發令牌
- 2.3 統一攔截技術
- 2.3.1 方案一:過濾器Filter
- 2.3.1.1 快速入門
- 2.3.1.2 Filter詳解
- 2.3.1.2.1 執行流程
- 2.3.1.2.2 攔截路徑
- 2.3.1.2.3 過濾器鏈
- 2.3.1.3 登錄校驗-Filter
- 2.3.1.3.1 分析
- 2.3.1.3.2 具體流程
- 2.3.1.3.3 代碼實現
- 2.3.2 方案二:攔截器Interceptor(SpringBoot框架提供的)
- 2.3.2.1 快速入門
- 2.3.2.2 Interceptor詳解
- 2.3.2.2.1 攔截路徑(可以指定攔截路徑,也可以指定哪些路徑不攔截)
- 2.3.2.2.2 執行流程
- 2.3.2.3 登錄校驗- Interceptor
- 3 異步任務和多線程(重要!!!)
- 1 多線程(這里主要是講的是SpringBoot項目部署后服務端的線程池問題)
- 2 異步任務和同步任務
- 3 異步任務和多線程的區別
- 4. 事務管理(重要!!!)
- 4.1 事務回顧
- 4.2 Spring事務管理
- 4.2.1 案例
- 4.2.2 原因分析
- 4.2.3 Transactional注解
- 4.3 事務進階
- 4.3.1 rollbackFor屬性(很重要這個屬性一定要加上):配置事務的回滾
- 4.3.3 propagation屬性:配置事務的傳播行為
- 4.3.3.1 介紹
- 4.3.3.2 應用場景
- 4.3.3.2.1 案例1 :轉賬操作(REQUIRED)?扣款和入賬只要有一個出錯,因異常導致整個事務都要回滾
- 4.3.3.2.2 案例2:用戶注冊,異步郵件通知(重要!!)
- 4.3.3.2.3 案例3: propagation = Propagation.REQUIRES_NEW在日志操作上的應用(重要!!)
- 4.3.3.2.3.1 默認情況:propagation = Propagation.REQUIRED(不符合需求要求)
- 4.3.3.2.3.2 使用Propagation.REQUIRES_NEW:@Transactional(propagation = Propagation.REQUIRES_NEW)
案例-登錄認證
在前面的課程中,我們已經實現了部門管理、員工管理的基本功能,但是大家會發現,我們并沒有登錄,就直接訪問到了Tlias智能學習輔助系統的后臺。 這是不安全的,所以我們今天的主題就是登錄認證。 最終我們要實現的效果就是用戶必須登錄之后,才可以訪問后臺系統中的功能。
1. 登錄功能
1.1 需求
在登錄界面中,我們可以輸入用戶的用戶名以及密碼,然后點擊 “登錄” 按鈕就要請求服務器,服務端判斷用戶輸入的用戶名或者密碼是否正確。如果正確,則返回成功結果,前端跳轉至系統首頁面。
1.2 接口文檔
我們參照接口文檔來開發登錄功能
-
基本信息
請求路徑:/login請求方式:POST接口描述:該接口用于員工登錄Tlias智能學習輔助系統,登錄完畢后,系統下發JWT令牌。
-
請求參數
參數格式:application/json
參數說明:
名稱 類型 是否必須 備注 username string 必須 用戶名 password string 必須 密碼 請求數據樣例:
{"username": "jinyong","password": "123456" }
-
響應數據
參數格式:application/json
參數說明:
名稱 類型 是否必須 默認值 備注 其他信息 code number 必須 響應碼, 1 成功 ; 0 失敗 msg string 非必須 提示信息 data string 必須 返回的數據 , jwt令牌 響應數據樣例:
{"code": 1,"msg": "success","data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo" }
1.3 思路分析
登錄服務端的核心邏輯就是:接收前端請求傳遞的用戶名和密碼 ,然后再根據用戶名和密碼查詢用戶信息,如果用戶信息存在,則說明用戶輸入的用戶名和密碼正確。如果查詢到的用戶不存在,則說明用戶輸入的用戶名和密碼錯誤。
1.4 功能開發
LoginController
@RestController
public class LoginController {@Autowiredprivate EmpService empService;@PostMapping("/login")public AjaxResult login(@RequestBody Emp emp){Emp e = empService.login(emp);return e != null ? AjaxResult.success():AjaxResult.error("用戶名或密碼錯誤");}
}
EmpService
public interface EmpService {/*** 用戶登錄* @param emp* @return*/public Emp login(Emp emp);//省略其他代碼...
}
EmpServiceImpl
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {@Autowiredprivate EmpMapper empMapper;@Overridepublic Emp login(Emp emp) {//調用dao層功能:登錄Emp loginEmp = empMapper.getByUsernameAndPassword(emp);//返回查詢結果給Controllerreturn loginEmp;} //省略其他代碼...
}
EmpMapper
@Mapper
public interface EmpMapper {@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +"from emp " +"where username=#{username} and password =#{password}")public Emp getByUsernameAndPassword(Emp emp);//省略其他代碼...
}
1.5 測試
功能開發完畢后,我們就可以啟動服務,打開postman進行測試了。
發起POST請求,訪問:http://localhost:8080/login
postman測試通過了,那接下來,我們就可以結合著前端工程進行聯調測試。
先退出系統,進入到登錄頁面:
在登錄頁面輸入賬戶密碼:
登錄成功之后進入到后臺管理系統頁面:
2. 登錄校驗
2.1 問題分析
我們已經完成了基礎登錄功能的開發與測試,在我們登錄成功后就可以進入到后臺管理系統中進行數據的操作。
但是當我們在瀏覽器中新的頁面上輸入地址:http://localhost:9528/#/system/dept
,發現沒有登錄仍然可以進入到后端管理系統頁面。
而真正的登錄功能應該是:登陸后才能訪問后端系統頁面,不登陸則跳轉登陸頁面進行登陸。
為什么會出現這個問題?其實原因很簡單,就是因為針對于我們當前所開發的部門管理、員工管理以及文件上傳等相關接口來說,我們在服務器端并沒有做任何的判斷,沒有去判斷用戶是否登錄了。所以無論用戶是否登錄,都可以訪問部門管理以及員工管理的相關數據。所以我們目前所開發的登錄功能,它只是徒有其表。而我們要想解決這個問題,我們就需要完成一步非常重要的操作:登錄校驗。
什么是登錄校驗?
- 所謂登錄校驗,指的是我們在服務器端接收到瀏覽器發送過來的請求之后,首先我們要對請求進行校驗。先要校驗一下用戶登錄了沒有,如果用戶已經登錄了,就直接執行對應的業務操作就可以了;如果用戶沒有登錄,此時就不允許他執行相關的業務操作,直接給前端響應一個錯誤的結果,最終跳轉到登錄頁面,要求他登錄成功之后,再來訪問對應的數據。
了解完什么是登錄校驗之后,接下來我們分析一下登錄校驗大概的實現思路。
首先我們在宏觀上先有一個認知:
前面在講解HTTP協議的時候,我們提到HTTP協議是無狀態協議。什么又是無狀態的協議?
所謂無狀態,指的是每一次請求都是獨立的,下一次請求并不會攜帶上一次請求的數據。而瀏覽器與服務器之間進行交互,基于HTTP協議也就意味著現在我們通過瀏覽器來訪問了登陸這個接口,實現了登陸的操作,接下來我們在執行其他業務操作時,服務器也并不知道這個員工到底登陸了沒有。因為HTTP協議是無狀態的,兩次請求之間是獨立的,所以是無法判斷這個員工到底登陸了沒有。
那應該怎么來實現登錄校驗的操作呢?具體的實現思路可以分為兩部分:
- **會話技術:**在員工登錄成功后,需要將用戶登錄成功的信息存起來,記錄用戶已經登錄成功的標記。參考視頻
簡單來說就是,前端瀏覽器第一次向服務端發送了登錄的請求,服務端存一個已經登錄的標記。然后,前端在發送任何請求時,先會要求后端先返回登錄標記,如果有登錄標記那就放行正常訪問,如果登錄標記,就會提示請登錄并退回到登錄界面。 - 統一攔截技術:在瀏覽器發起請求時,需要在服務端進行統一攔截,攔截后進行登錄校驗。
想要判斷員工是否已經登錄,我們需要在員工登錄成功之后,存儲一個登錄成功的標記,接下來在每一個接口方法執行之前,先做一個條件判斷,判斷一下這個員工到底登錄了沒有。如果是登錄了,就可以執行正常的業務操作,如果沒有登錄,會直接給前端返回一個錯誤的信息,前端拿到這個錯誤信息之后會自動的跳轉到登錄頁面。
我們程序中所開發的查詢功能、刪除功能、添加功能、修改功能,都需要使用以上套路進行登錄校驗。此時就會出現:相同代碼邏輯,每個功能都需要編寫,就會造成代碼非常繁瑣。
為了簡化這塊操作,我們可以使用一種技術:統一攔截技術。
通過統一攔截的技術,我們可以來攔截瀏覽器發送過來的所有的請求,攔截到這個請求之后,就可以通過請求來獲取之前所存入的登錄標記,在獲取到登錄標記且標記為登錄成功,就說明員工已經登錄了。如果已經登錄,我們就直接放行(意思就是可以訪問正常的業務接口了)。
我們要完成以上操作,會涉及到web開發中的兩個技術:
- 會話技術
- 統一攔截技術
而統一攔截技術現實方案也有兩種:
- Servlet規范中的Filter過濾器
- Spring提供的interceptor攔截器
下面我們先學習會話技術,然后再學習統一攔截技術。
2.2 會話技術
介紹了登錄校驗的大概思路之后,我們先來學習下會話技術。
2.2.1 會話技術介紹
參考視頻
什么是會話?
-
在我們日常生活當中,會話指的就是談話、交談。
-
在web開發當中,會話指的就是瀏覽器與服務器之間的一次連接,我們就稱為一次會話。
在用戶打開瀏覽器第一次訪問服務器的時候,這個會話就建立了,直到有任何一方斷開連接,此時會話就結束了。在一次會話當中,是可以包含多次請求和響應的。
比如:打開了瀏覽器來訪問web服務器上的資源(瀏覽器不能關閉、服務器不能斷開)
- 第1次:訪問的是登錄的接口,完成登錄操作
- 第2次:訪問的是部門管理接口,查詢所有部門數據
- 第3次:訪問的是員工管理接口,查詢員工數據
只要瀏覽器和服務器都沒有關閉,以上3次請求都屬于一次會話當中完成的。
需要注意的是:會話是和瀏覽器關聯的,當有三個瀏覽器客戶端和服務器建立了連接時,就會有三個會話。同一個瀏覽器在未關閉之前請求了多次服務器,這多次請求是屬于同一個會話。比如:1、2、3這三個請求都是屬于同一個會話。當我們關閉瀏覽器之后,這次會話就結束了。而如果我們是直接把web服務器關了,那么所有的會話就都結束了。
知道了會話的概念了,接下來我們再來了解下會話跟蹤。
會話跟蹤:一種維護瀏覽器狀態的方法,服務器需要識別多次請求是否來自于同一瀏覽器,以便在同一次會話的多次請求間共享數據。
服務器會接收很多的請求,但是服務器是需要識別出這些請求是不是同一個瀏覽器發出來的。比如:1和2這兩個請求是不是同一個瀏覽器發出來的,3和5這兩個請求不是同一個瀏覽器發出來的。如果是同一個瀏覽器發出來的,就說明是同一個會話。如果是不同的瀏覽器發出來的,就說明是不同的會話。而識別多次請求是否來自于同一瀏覽器的過程,我們就稱為會話跟蹤。
我們使用會話跟蹤技術就是要完成在同一個會話中,多個請求之間進行共享數據。
為什么要共享數據呢?
由于HTTP是無狀態協議,在后面請求中怎么拿到前一次請求生成的數據呢?此時就需要在一次會話的多次請求之間進行數據共享
這里我們就可以總結出會話技術的關鍵作用:共享一次會話中所有請求都會用到的通用數據(比較典型的就是用戶的身份信息用于身份效驗)
會話跟蹤技術有三種:
- Cookie(客戶端會話跟蹤技術)
- 數據存儲在客戶端瀏覽器當中
- Session(服務端會話跟蹤技術)
- 數據存儲在儲在服務端
- 令牌技術(目前最主流的)
2.2.2 會話跟蹤方案
上面我們介紹了什么是會話,什么是會話跟蹤,并且也提到了會話跟蹤 3 種常見的技術方案。接下來,我們就來對比一下這 3 種會話跟蹤的技術方案,來看一下具體的實現思路,以及它們之間的優缺點。
2.2.2.1 方案一 - Cookie
參考視頻
cookie 是客戶端會話跟蹤技術,它是存儲在客戶端瀏覽器的,我們使用 cookie 來跟蹤會話,我們就可以在瀏覽器第一次發起請求來請求服務器的時候,我們在服務器端來設置一個cookie。
·
比如第一次請求了登錄接口,登錄接口執行完成之后,我們就可以設置一個cookie,在 cookie 當中我們就可以來存儲用戶相關的一些數據信息。比如我可以在 cookie 當中來存儲當前登錄用戶的用戶名,用戶的ID。
服務器端在給客戶端在響應數據的時候,會自動的將 cookie 響應給瀏覽器,瀏覽器接收到響應回來的 cookie 之后,會自動的將 cookie 的值存儲在瀏覽器本地。接下來在后續的每一次請求當中,都會將瀏覽器本地所存儲的 cookie 自動地攜帶到服務端。
接下來在服務端我們就可以獲取到 cookie 的值。我們可以去判斷一下這個 cookie 的值是否存在,如果不存在這個cookie,就說明客戶端之前是沒有訪問登錄接口的;如果存在 cookie 的值,就說明客戶端之前已經登錄完成了。這樣我們就可以基于 cookie 在同一次會話的不同請求之間來共享數據。
我剛才在介紹流程的時候,用了 3 個自動:
-
服務器會 自動 的將 cookie 響應給瀏覽器。
-
瀏覽器接收到響應回來的數據之后,會 自動 的將 cookie 存儲在瀏覽器本地。
-
在后續的請求當中,瀏覽器會 自動 的將 cookie 攜帶到服務器端。
Cookie總結:
- cookie是存在客戶端(注意不是服務端,f服務端只是在第一次登錄時生成cookie,不會保存)
- 步驟:一次會話中
- step1: 客戶端第一次向發送用戶登錄請求,服務端生成一個 cookie(比如往這個cookie里面封裝一些用戶信息)返回給前端;前端將這個cookie存在客戶端的(內存或者硬盤中)
- step2: 今后客戶端在這次會話中發送任何請求,就會自動將保存下來的這次會話的cookie封裝在HTTP 協議的請求頭中,服務端就可以通過這個解析拿到cookie里面的信息進行邏輯處理
為什么這一切都是自動化進行的?
是因為 cookie 它是 HTP 協議當中所支持的技術,而各大瀏覽器廠商都支持了這一標準。在 HTTP 協議官方給我們提供了一個響應頭和請求頭:
-
響應頭 Set-Cookie :設置Cookie數據的
-
請求頭 Cookie:攜帶Cookie數據的
代碼測試
@Slf4j
@RestController
public class SessionController {//設置Cookie@GetMapping("/c1")public AjaxResult cookie1(HttpServletResponse response){response.addCookie(new Cookie("login_username","itheima")); //設置Cookie/響應Cookiereturn AjaxResult.success();}//獲取Cookie@GetMapping("/c2")public AjaxResult 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 AjaxResult.success();}
}
A. 訪問c1接口,設置Cookie,http://localhost:8080/c1
我們可以看到,后端服務器生成的的cookie,通過響應頭Set-Cookie響應給瀏覽器,并且瀏覽器會將Cookie,存儲在瀏覽器端。
B. 訪問c2接口 http://localhost:8080/c2,此時瀏覽器會自動的將Cookie攜帶到服務端,是通過請求頭Cookie,攜帶的。
補充:在瀏覽器端可以看到客戶端將cookie存在了這個地方
優缺點
- 優點:HTTP協議中支持的技術(像Set-Cookie 響應頭的解析以及 Cookie 請求頭數據的攜帶,都是瀏覽器自動進行的,是無需我們手動操作的)
- 缺點:
- 移動端APP(Android、IOS)中無法使用Cookie
- 不安全,用戶可以自己禁用Cookie
- Cookie不能跨域
跨域介紹:
?
- 現在的項目,大部分都是前后端分離的,前后端最終也會分開部署,前端部署在服務器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
- 我們打開瀏覽器直接訪問前端工程,訪問url:http://192.168.150.200/login.html
- 然后在該頁面發起請求到服務端,而服務端所在地址不再是localhost,而是服務器的IP地址192.168.150.100,假設訪問接口地址為:http://192.168.150.100:8080/login
- 那此時就存在跨域操作了,因為我們是在 http://192.168.150.200/login.html 這個頁面上訪問了http://192.168.150.100:8080/login 接口
- 此時如果服務器設置了一個Cookie,這個Cookie是不能使用的,因為Cookie無法跨域
區分跨域的維度:
- 協議
- IP/協議
- 端口
只要上述的三個維度有任何一個維度不同,那就是跨域操作
舉例:
? http://192.168.150.200/login.html ----------> https://192.168.150.200/login [協議不同,跨域]
? 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和Cookie基本差不多,也是Cookie那個邏輯
前面介紹的時候,我們提到Session,它是服務器端會話跟蹤技術,所以它是存儲在服務器端的。而 Session 的底層其實就是基于我們剛才所介紹的 Cookie 來實現的。
- 獲取Session
如果我們現在要基于 Session 來進行會話跟蹤,瀏覽器在第一次請求服務器的時候,我們就可以直接在服務器當中來獲取到會話對象Session。如果是第一次請求Session ,會話對象是不存在的,這個時候服務器會自動的創建一個會話對象Session 。而每一個會話對象Session ,它都有一個ID(示意圖中Session后面括號中的1,就表示ID),我們稱之為 Session 的ID。
-
響應Cookie (JSESSIONID)
接下來,服務器端在給瀏覽器響應數據的時候,它會將 Session 的 ID 通過 Cookie 響應給瀏覽器。其實在響應頭當中增加了一個 Set-Cookie 響應頭。這個 Set-Cookie 響應頭對應的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服務器端會話對象 Session 的 ID。瀏覽器會自動識別這個響應頭,然后自動將Cookie存儲在瀏覽器本地。
-
查找Session
接下來,在后續的每一次請求當中,都會將 Cookie 的數據獲取出來,并且攜帶到服務端。接下來服務器拿到JSESSIONID這個 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就會從眾多的 Session 當中來找到當前請求對應的會話對象Session。
Session總結:
-
Session存儲在服務器,攜帶Session id的Cookie 存儲在客戶端
-
步驟:一次會話中
- step1: 客戶端第一次向發送用戶登錄請求,服務端生成一個會生成一個Session對象(可以往里面自定義封裝一些數據,例如用戶信息),注意每一個Session對象都有一個Session_id;服務器端會干兩件事情,一是將這個生成的Session對象存儲到服務器上,二是將這個Session對象的id封裝在cookie里面返回給前端,前端會自動在客戶端保存這個cookie
- step2: 今后客戶端在這次會話中發送任何請求,就會自動將保存下來的這次會話的cookie封裝在HTTP 協議的請求頭中,服務端就可以通過這個解析拿到cookie里面的Session_id信息進行邏輯處理,通過Session_id找到服務器上保存的Session對象中的封裝的數據進行操作,例如身份驗證這些
這樣我們是不是就可以通過 Session 會話對象在同一次會話的多次請求之間來共享數據了?好,這就是基于 Session 進行會話跟蹤的流程。
代碼測試
@Slf4j
@RestController
public class SessionController {@GetMapping("/s1")public AjaxResult session1(HttpSession session){log.info("HttpSession-s1: {}", session.hashCode());session.setAttribute("loginUser", "tom"); //往session中存儲數據return AjaxResult.success();}@GetMapping("/s2")public AjaxResult session2(HttpServletRequest request){HttpSession session = request.getSession();log.info("HttpSession-s2: {}", session.hashCode());Object loginUser = session.getAttribute("loginUser"); //從session中獲取數據log.info("loginUser: {}", loginUser);return AjaxResult.success(loginUser);}
}
A. 訪問 s1 接口,http://localhost:8080/s1
請求完成之后,在響應頭中,就會看到有一個Set-Cookie的響應頭,里面響應回來了一個Cookie,就是JSESSIONID,這個就是服務端會話對象 Session 的ID。
B. 訪問 s2 接口,http://localhost:8080/s2
接下來,在后續的每次請求時,都會將Cookie的值,攜帶到服務端,那服務端呢,接收到Cookie之后,會自動的根據JSESSIONID的值,找到對應的會話對象Session。
那經過這兩步測試,大家也會看到,在控制臺中輸出如下日志:
兩次請求,獲取到的Session會話對象的hashcode是一樣的,就說明是同一個會話對象。而且,第一次請求時,往Session會話對象中存儲的值,第二次請求時,也獲取到了。 那這樣,我們就可以通過Session會話對象,在同一個會話的多次請求之間來進行數據共享了。
優缺點
- 優點:Session是存儲在服務端的,安全
- 缺點:
- 服務器集群環境下無法直接使用Session
- 移動端APP(Android、IOS)中無法使用Cookie
- 用戶可以自己禁用Cookie
- Cookie不能跨域
PS:Session 底層是基于Cookie實現的會話跟蹤,如果Cookie不可用,則該方案,也就失效了。
服務器集群環境為何無法使用Session?
?
- 首先第一點,我們現在所開發的項目,一般都不會只部署在一臺服務器上,因為一臺服務器會存在一個很大的問題,就是單點故障。所謂單點故障,指的就是一旦這臺服務器掛了,整個應用都沒法訪問了。
?
所以在現在的企業項目開發當中,最終部署的時候都是以集群的形式來進行部署,也就是同一個項目它會部署多份。比如這個項目我們現在就部署了 3 份。
而用戶在訪問的時候,到底訪問這三臺其中的哪一臺?其實用戶在訪問的時候,他會訪問一臺前置的服務器,我們叫負載均衡服務器,我們在后面項目當中會詳細講解。目前大家先有一個印象負載均衡服務器,它的作用就是將前端發起的請求均勻的分發給后面的這三臺服務器。
此時假如我們通過 session 來進行會話跟蹤,可能就會存在這樣一個問題。用戶打開瀏覽器要進行登錄操作,此時會發起登錄請求。登錄請求到達負載均衡服務器,將這個請求轉給了第一臺 Tomcat 服務器。
Tomcat 服務器接收到請求之后,要獲取到會話對象session。獲取到會話對象 session 之后,要給瀏覽器響應數據,最終在給瀏覽器響應數據的時候,就會攜帶這么一個 cookie 的名字,就是 JSESSIONID ,下一次再請求的時候,是不是又會將 Cookie 攜帶到服務端?
好。此時假如又執行了一次查詢操作,要查詢部門的數據。這次請求到達負載均衡服務器之后,負載均衡服務器將這次請求轉給了第二臺 Tomcat 服務器,此時他就要到第二臺 Tomcat 服務器當中。根據JSESSIONID 也就是對應的 session 的 ID 值,要找對應的 session 會話對象。
我想請問在第二臺服務器當中有沒有這個ID的會話對象 Session, 是沒有的。此時是不是就出現問題了?我同一個瀏覽器發起了 2 次請求,結果獲取到的不是同一個會話對象,這就是Session這種會話跟蹤方案它的缺點,在服務器集群環境下無法直接使用Session。
大家會看到上面這兩種傳統的會話技術,在現在的企業開發當中是不是會存在很多的問題。 為了解決這些問題,在現在的企業開發當中,基本上都會采用第三種方案,通過令牌技術來進行會話跟蹤。接下來我們就來介紹一下令牌技術,來看一下令牌技術又是如何跟蹤會話的。
2.2.2.3 方案三 - 令牌技術
這里我們所提到的令牌,其實它就是一個用戶身份的標識,看似很高大上,很神秘,其實本質就是一個字符串。
如果通過令牌技術來跟蹤會話,我們就可以在瀏覽器發起請求。在請求登錄接口的時候,如果登錄成功,我就可以生成一個令牌,令牌就是用戶的合法身份憑證。接下來我在響應數據的時候,我就可以直接將令牌響應給前端。
接下來我們在前端程序當中接收到令牌之后,就需要將這個令牌存儲起來。這個存儲可以存儲在 cookie 當中,也可以存儲在其他的存儲空間(比如:localStorage)當中。
接下來,在后續的每一次請求當中,都需要將令牌攜帶到服務端。攜帶到服務端之后,接下來我們就需要來校驗令牌的有效性。如果令牌是有效的,就說明用戶已經執行了登錄操作,如果令牌是無效的,就說明用戶之前并未執行登錄操作。
此時,如果是在同一次會話的多次請求之間,我們想共享數據,我們就可以將共享的數據存儲在令牌當中就可以了。
優缺點
- 優點:
- 支持PC端、移動端
- 解決集群環境下的認證問題
- 減輕服務器的存儲壓力(無需在服務器端存儲)
- 缺點:需要自己實現(包括令牌的生成、令牌的傳遞、令牌的校驗)
針對于這三種方案,現在企業開發當中使用的最多的就是第三種令牌技術進行會話跟蹤。而前面的這兩種傳統的方案,現在企業項目開發當中已經很少使用了。所以在我們的課程當中,我們也將會采用令牌技術來解決案例項目當中的會話跟蹤問題。
2.2.3 JWT令牌(Token)
前面我們介紹了基于令牌技術來實現會話追蹤。這里所提到的令牌就是用戶身份的標識,其本質就是一個字符串。令牌的形式有很多,我們使用的是功能強大的 JWT令牌。
2.2.3.1 介紹
參考視頻
JWT全稱:JSON Web Token (官網:https://jwt.io/)
-
定義了一種簡潔的、自包含的格式,用于在通信雙方以json數據格式安全的傳輸信息。由于數字簽名的存在,這些信息是可靠的。
簡潔:是指jwt就是一個簡單的字符串。可以在請求參數或者是請求頭當中直接傳遞。
自包含:指的是jwt令牌,看似是一個隨機的字符串,但是我們是可以根據自身的需求在jwt令牌中存儲自定義的數據內容。如:可以直接在jwt令牌中存儲用戶的相關信息。
簡單來講,jwt就是將原始的json數據格式進行了安全的封裝,這樣就可以直接基于jwt在通信雙方安全的進行信息傳輸了。
JWT的組成: (JWT令牌由三個部分組成,三個部分之間使用英文的點來分割)
-
第一部分:Header(頭), 記錄令牌類型、簽名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}
-
第二部分:Payload(有效載荷),攜帶一些自定義信息、默認信息等。 例如:{“id”:“1”,“username”:“Tom”}
-
第三部分:Signature(簽名),防止Token被篡改、確保安全性。將header、payload,并加入指定秘鑰(secret),通過指定簽名算法計算而來。
簽名的目的就是為了防jwt令牌被篡改,而正是因為jwt令牌最后一個部分數字簽名的存在,所以整個jwt 令牌是非常安全可靠的。一旦jwt令牌當中任何一個部分、任何一個字符被篡改了,整個令牌在校驗的時候都會失敗,所以它是非常安全可靠的。
JWT是如何將原始的JSON格式數據,轉變為字符串的呢?
其實在生成JWT令牌時,會對JSON格式的數據進行一次編碼:進行base64編碼Base64:是一種基于64個可打印的字符來表示二進制數據的編碼方式。既然能編碼,那也就意味著也能解碼。所使用的64個字符分別是A到Z、a到z、 0- 9,一個加號,一個斜杠,加起來就是64個字符。任何數據經過base64編碼之后,最終就會通過這64個字符來表示。當然還有一個符號,那就是等號。等號它是一個補位的符號
需要注意的是Base64是編碼方式,而不是加密方式。
JWT令牌最典型的應用場景就是登錄認證:
- 在瀏覽器發起請求來執行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
- 前端拿到jwt令牌之后,會將jwt令牌存儲起來。在后續的每一次請求中都會將jwt令牌攜帶到服務端。
- 服務端統一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗一下令牌是否是有效。如果有效,就直接放行進行請求的處理。
在JWT登錄認證的場景中我們發現,整個流程當中涉及到兩步操作:
- 在登錄成功之后,要生成令牌。
- 每一次請求當中,要接收令牌并對令牌進行校驗。
稍后我們再來學習如何來生成jwt令牌,以及如何來校驗jwt令牌。
2.2.3.2 生成和校驗
參考視頻
簡單介紹了JWT令牌以及JWT令牌的組成之后,接下來我們就來學習基于Java代碼如何生成和校驗JWT令牌。
首先我們先來實現JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依賴:
<!-- JWT依賴-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
在引入完JWT來賴后,就可以調用工具包中提供的API來完成JWT令牌的生成和校驗
工具類:Jwts
生成JWT代碼實現:
@Test
public void genJwt(){Map<String,Object> claims = new HashMap<>();claims.put("id",1);claims.put("username","Tom");String jwt = Jwts.builder().setClaims(claims) //自定義內容(載荷),想共享的數據往這個里面放就可以了.signWith(SignatureAlgorithm.HS256, "itheima456352wfesfsF") //簽名算法,第二個參數就是我們指定的 秘鑰 secret,隨便寫(一般情況下這個秘鑰不泄露,jwt令牌就很安全).setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //設置該令牌有效期為 1小時.compact();System.out.println(jwt);
}
運行測試方法:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzQwNTgwNDc1LCJ1c2VybmFtZSI6IlRvbSJ9._1fZ9OukvSs5YldnzIaMYkRBHeYKptcssxzjnePXblI
輸出的結果就是生成的JWT令牌,,通過英文的點分割對三個部分進行分割,我們可以將生成的令牌復制一下,然后打開JWT的官網,將生成的令牌直接放在Encoded位置,此時就會自動的將令牌解析出來。
第一部分解析出來,看到JSON格式的原始數據,所使用的簽名算法為HS256。
第二個部分是我們自定義的數據,之前我們自定義的數據就是id,還有一個exp代表的是我們所設置的過期時間。
由于前兩個部分是base64編碼,所以是可以直接解碼出來。但最后一個部分并不是base64編碼,是經過簽名算法計算出來的,所以最后一個部分是不會解析的。
實現了JWT令牌的生成,下面我們接著使用Java代碼來校驗JWT令牌(解析生成的令牌):
@Test
public void parseJwt(){Claims claims = Jwts.parser().setSigningKey("itheima456352wfesfsF")//指定簽名密鑰(必須保證和生成令牌時使用相同的簽名密鑰) .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzQwNTgwNDc1LCJ1c2VybmFtZSI6IlRvbSJ9._1fZ9OukvSs5YldnzIaMYkRBHeYKptcssxzjnePXblI").getBody();System.out.println(claims);
}
運行測試方法:
{id=1, exp=1740580475, username=Tom}
令牌解析后,我們可以看到id和過期時間,如果在解析的過程當中沒有報錯,就說明解析成功了。
下面我們做一個測試:把令牌header中的數字9變為8,運行測試方法后發現報錯:
原header: eyJhbGciOiJIUzI1NiJ9
修改為: eyJhbGciOiJIUzI1NiJ8
結論:篡改令牌中的任何一個字符,在對令牌進行解析時都會報錯,所以JWT令牌是非常安全可靠的。
我們繼續測試:修改生成令牌的時指定的過期時間,修改為1分鐘
@Test
public void genJwt(){Map<String,Object> claims = new HashMap<>();claims.put(“id”,1);claims