前言:Servlet
【登錄校驗】這個功能技術的基礎是【會話技術】,那么在講【會話技術】的時候必然要談到【Cookie】和【Session】這兩個東西,那么在這之前必須要先講一下一個很重要但是很多人都會忽略的一個知識點:【Servlet】
什么是Servlet?
????????Servlet是用java編寫的應用在服務器端的程序;對于它的定義,“廣義上”是一個個很大的【類】,“狹義上”是【接口】
.
Servlet容器是什么?
????????我們經常聽說的Tomcat、Weblogic......這些都是各種【Servlet容器】,而【Servlet容器】就是Servlet的運行環境,也可以理解是Servlet的引擎,為請求和響應的這些操作提供網絡服務。
????????那么我們知道,我們運行網絡服務的時候都要啟動Tomcat服務器,Tomcat來解析處理【請求】、【響應】,并處理成報文信息,但是這些服務器的缺點是底層代碼寫死了,很不靈活,而且有時處理完的數據格式也不夠規范;那么這時候就誕生了Servlet,它被用來“擴展服務器的性能”,它能夠靈活的處理請求、響應數據并以規范形式返回。
????????那么說回Servlet,其實最簡單最簡單的理解,就是一個很大很規范的【類】,它里面包含了很多其他【子類】(準確來說是接口),這些子類包括:Cookie、Session、HttpServletRequest、HttpServletResponse、ServletConfig、ServletContext......
。
????????那么這些【子類】里也寫好了很詳細、規范的各種處理網絡服務的方法,當我們需要處理一些網絡服務邏輯的時候,就需要調用Servlet里的【子類】的【實例化對象】的【方法】
????????打個比方:處理前端客戶端發送請求的數據并生成對應的請求頭報文時,就需要調用【HttpServletRequest】的實例化對象的方法;? 處理服務器端返回回去的響應頭報文的時候,就需要調用【HttpServletResponse】的實例化對象的方法......等等
。
那么現在我們重新來看一下(B/S架構)瀏覽器客戶端與服務器端傳輸的流程:
1、瀏覽器客戶端發送請求到服務器端
2、服務器端接收到信息,交給Servlet
3、Servlet(通過調用里面的一些子類的方法)處理邏輯,然后生成響應信息,給回服務器
4、服務器將響應結果返回瀏覽器客戶端
一、會話技術
1、何為會話?
瀏覽器與服務器之間的一次連接就是一次會話
2、會話跟蹤
【會話跟蹤】就是:識別多個請求是否來自于同一個瀏覽器,然后在同一個瀏覽器的多個請求之間共享數據
3、會話跟蹤方案:
瀏覽器與服務器之間的交互使用的是【http】協議,但是【http】協議是無狀態的,也就是所有請求都是相互獨立的,并不能在多個請求之間共享數據
那么就有了這么幾種【會話跟蹤技術】:
1、客戶端會話跟蹤技術:Cookie
.
2、服務端會話跟蹤技術:Session
。
3、token令牌技術:JWT
這里不講解HTTP是啥,這里有一篇講的比較詳細:HTTP 協議詳解(史上最全)-CSDN博客
HTTP協議里的【請求頭】和【響應頭】的文章:Request Headers 和Response Headers——請求頭和響應頭-CSDN博客
只需要知道HTTP協議是一個瀏覽器與服務器聯系傳輸數據的超文本傳輸協議,它兩規定了一種傳輸數據的格式,然后里面有【響應報文Response Headers】和【請求報文Request Headers】這兩部分,下面是簡單講解:
【請求報文Request Headers】:
前端通過【瀏覽器/客戶端】發送請求傳給【服務器】的數據信息
。
一個【請求報文Request Headers】里包括了【請求行】【請求頭】【請求體】
【請求體】就是前端傳過來的具體的數據信息
【請求頭】是服務器獲取客戶端信息的一些依據(用什么格式獲取?從哪個地址獲取?哪個瀏覽器發的?......)
【響應頭報文Reponse Headers】:
【服務器】返回給【瀏覽器/客戶端】的響應數據信息
一樣,一個【響應報文Response Headers】里也包括了【響應行】【響應頭】【響應體】
【響應體】就是針對前端發來的請求數據而相應回去的具體數據值
【響應頭】包含了服務器的響應訊息,如http版本,壓縮方式,響應文件類型,文件編碼等
(1)Cookie技術(Cookie是一個Servlet里的“子類”)
雖然每個請求和響應之間的Http協議是獨立的,但是Http協議的【請求頭】和【響應頭】支持攜帶【Cookie】這個信息,那多個請求間就可以通過Cookie這個標識來獲取用戶信息數據
Cookie簡單說:就是存放在【客戶端(瀏覽器)】的會話信息
—— Cookie是通過在【請求報文Request Headers】里的【請求頭】處傳遞的
—— 傳遞關系是:【客戶端(瀏覽器)】——傳Cookie——>【服務器】
—— Set-Cookie是通過【響應報文Response Headers】里的【響應頭】處傳遞的
—— 傳遞關系是:【服務器】——返回Set-Cookie——>【客戶端(瀏覽器)】
簡單用代碼直觀一點展示Cookie和Set-Cookie:(切記別記憶這些代碼,簡單了解即可)
Set-Cookie:
【HttpServletResponse】這個接口的實現類是專門設置【響應報文】信息的(切記:要導入的包一定要選這個【javax.servlet.http】)
.
然后【HttpServletResponse】的實現類對象的【.addCookie( )】方法能設置一個Cookie的值,需要往里面傳一個【Cookie對象】(切記:這個【Cookie對象】對應的類型必須也是【javax.servlet.http】)
.
最后【Cookie對象】里數據形式是“鍵值對Key=value”:【name=value】,所以要傳一個name參數、一個value值,分別代表 “鍵” 和 “值”
/*** 模擬【服務器】設置【用戶的Cookie】的操作* @param response* @return*/ @GetMapping("/setCookie") public Result setCookie(HttpServletResponse response){//調用這個響應方法就能設置一個Cookie值,Cookie對象里是【鍵值對】信息:name=valueresponse.addCookie(new javax.servlet.http.Cookie("userName","岑梓銘"));return Result.success(); }
怎么看效果?
1、首先輸入網址,千萬先別回車!
2、摁F12打開網頁檢查,然后再在網址那回車,就會看到一個網絡響應
3、單擊它,然后就能在【響應報文Response Header】處看到【Set-Cookie】
Cookie:
【HttpServletRequest】這個接口的實現類是專門設置【請求報文】信息的(切記:要導入的包一定要選這個【javax.servlet.http】)
.
然后【HttpServletRequest】的實現類對象的【.getCookies( )】方法能返回所有Cookie的值,返回值是一個數組;需要用一個【Cookie對象類型的數組】接收(切記:這個【Cookie對象】類型對應的類型必須也是【javax.servlet.http】)
.
最后【Cookie對象】里數據形式是“鍵值對Key=value”:【name=value】,所以要獲取Cookie數組里每一個Cookie的 “鍵”,就要調用【Cookie對象】的【.getName( )】方法
@GetMapping("/getCookie") public Result getCookie(HttpServletRequest request){//發送請求后,獲取返回的【所有的Cookies】javax.servlet.http.Cookie[] cookies = request.getCookies();//遍歷所有Cookie,如果有對應這個【鍵(name)】的,就返回對應的【值(value)】for(javax.servlet.http.Cookie cookie : cookies){if(cookie.getName().equals( "userName" )){System.out.println("userName: " + "【" + cookie.getValue() + "】");}}return Result.success(); }
?怎么看效果?還是一樣
1、首先輸入網址,千萬先別回車!
2、摁F12打開網頁檢查,然后再在網址那回車,就會看到一個網絡響應
3、單擊它,然后就能在【響應報文Response Header】處看到【Set-Cookie】
缺點
1、移動端環境不是瀏覽器,瀏覽器才有Cookie這玩意
.
2、用戶可以隨意自己設置禁用Cookie,會用電腦的應該不用我解釋,瀏覽器設置那里有
.
3、Cookie不能跨域(跨域就是【協議、IP/域名、端口】至少其中一樣不一樣,就是兩個域,就存在跨域訪問,那么我們都知道前端、后端是分別部署到兩個不同的服務器的,不同服務器的地址肯定是不一樣的,瀏覽器在發請求、返回響應時必然會要訪問前端和后端的兩個服務器,就會跨域)
總結
(2)Session技術(Session也是Servlet里的一個“子類”)
Session的本質其實就是對Cookie的優化,是存放在【服務器端】的會話信息
為什么說是Cookie的優化?因為它的邏輯其實是這樣:
。
????????首先瀏覽器發請求,產生一個會話,然后服務器這邊就立刻產生一個【Session會話信息】和一個對應這個Session會話信息的【sessionId】,然后把【Session會話信息】存在服務器,只會把【sessionId】存進Cookie,通過Set-Cookie響應回給瀏覽器
。。
????????然后下次瀏覽器再次發送請求想獲取這個【Session會話信息】的時候,傳遞過去的是【裝著sessionId的Cookie】,然后服務器檢查Cookie里的【sessionId】,再到session里找有沒有對應這個【sessionId】的【Session會話信息】,找到了的話,把【sessionId】和【Session會話信息】一起塞進【Cookie】返回給瀏覽器
模擬服務器【保存session】并【生成sessionId】的邏輯
/*** 模擬【服務器端】生成并響應回【session】的操作* @param session* @return*/ @GetMapping("/setSession") public Result setSession(HttpSession session){//打印一下當前session的哈希碼值,這個哈希碼值代表指向了哪一個session會話,是【整數】//但是注意區分,這個不是sessionId,sessionId是HttpSession對象的唯一標識符,是【字符串】log.info("session_hashCode: {}",session.hashCode());//調用這個方法可以往session會話里存入一個數據,然后對應生成一個sessionID并塞進Cookie里session.setAttribute("LoginName","岑梓銘");return Result.success(); }
模擬瀏覽器通過sessionId【接收session信息】的邏輯
/*** 模擬【瀏覽器】發請求后獲得服務器生成的【session】的操作* @param request* @return*/ @GetMapping("/getSession") public Result getSession(HttpServletRequest request){//調用HttpServletRequest對象的getSession()方法可以獲取到session會話對象HttpSession session = request.getSession();//打印一下當前session的哈希碼值,這個哈希碼值代表指向了哪一個session會話,是【整數】log.info("session_hashCode: {}",session.hashCode());//Session的getAttribute方法,里面傳入“鍵”參數,就能返回對應的seesion會話對象里的具體值//邏輯是瀏覽器這邊Cookie里只有sessionId,然后通過Session的getAttribute方法把Cookie里的sessionId給到服務器端//最終經過服務器檢測sessionId,然后將session會話里對應“userName”的會話具體數據再塞進Cookie,返回給瀏覽器Object userInfo = session.getAttribute("LoginName");log.info("userInfo: {}",userInfo);return Result.success(userInfo); }
?缺點
1、服務器集群情況下(也就是連接多個服務器的情況下),不同的服務器之間存著不同的會話信息,那就算瀏覽器的Cookie里有sessionId,那第二臺服務器那里能靠第一臺服務器seesion的 “鑰匙” 來 “開“ 第二臺服務器seesion的 “大門” 呢?
.
2、session會話信息都存在服務器,隨著瀏覽器請求增多,服務器內存越來越不夠用
.
3、既然它還是基于Cookie優化而來的,那必然也繼承了Cookie的缺點
總結
(3)JWT令牌技術
——概念:
JWT,全稱:【Json?Web?Token】,簡單來說就是一長串帶有【數字簽名、簽名算法、具體自定義信息......等等】json字符串,每一次請求響應都會帶著它,通過計算來校驗、得出其中的身份信息
一個JWT字符串主要包括三大部分:
????????第一部分:Header(頭),記錄令牌類型、名算法等。例如:{"alg":"HS256","type":"JWT"}
????????第二部分:Payload(有效載荷),攜帶一些自定義信息、默認信息等。例如:{"id":"1","username":"Tom"}
????????第三部分:Siqnature(簽名),防止Token被篡改、確保安全性。將header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。
而組成這個字符串的原理是:
1、前面兩部分是【Base64編碼】,是一種由【A-Z? a-z? 1-9? 還有/】組成的來表示二進制數據的編碼
。
2、最后一部分那一段是根據前面指定的一種【簽名算法】計算后得到的編碼
應用場景:
其實很簡單,流程就是:
1、瀏覽器發送請求到服務器
2、服務器攔截請求,查看有沒有token?沒有就拒絕訪問數據,并生成一個JWT令牌
3、下一次再來檢查到有JWT令牌了,那就校驗JWT令牌對不對,不對就拒絕訪問;對就開放訪問權限。
生成JWT令牌的方法:
1、引入依賴
在pom.xml文件引入下面依賴
<!-- JWT令牌 --> <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>
注意,如果點了右上角的【maven刷新】按鈕只后還是爆紅,又可能只是連接中央庫下載安裝這個依賴包的時候網絡不好,畢竟這些依賴都是在國外的公司的中央庫,那么控制臺那會有一個 “藍色” 的提示——“嘗試使用 -U 標記(強制更新快照)運行 Maven導入”,直接點它讓Maven幫我們換個方案下載安裝就行了
2、在Test類測試一下生成JWT令牌
只需要記住6步:
1、先設置一個哈希表集合,因為jwt令牌的【有效信息部分】要用【哈希表集合類型】接收
;
2、創建一個jwt的方法是【Jwts.builder( )】
;
3、一個Jwt令牌的第一部分是指定 “簽名算法類型”、“簽名密鑰”;那么【.signWith( )方法】就是設置jwt令牌第一部分。以我個人理解,“簽名算法類型” 就是指根據不同類型的不同算法,“簽名密鑰” 就是以你自定義輸入的一串字符串作為一個 “密鑰”,用你的這個 “簽名密鑰”?才能在解析jwt令牌時知道你要解析的是哪一個(至于“簽名算法類型”具體哪些類型有啥區別我也不知道,盡量先都用HS256這個類型就行)
;
4、一個Jwt令牌的第二部分是【有效荷載】,也就是用戶的一些【有效信息部分】,【Jwts.addClaims( )】方法則是在創建一個jwt對象之后接收這個【有效信息部分】的方法,接收【哈希表集合】類型數據
;
5、jwt令牌要有個【有效時間】,就跟你們平時登錄時的驗證碼一樣,不然的話沒時間限制那不是留夠了時間給黑客破解嗎。【.setExiration( )】方法就是設置【有效時間】,需要接受的是Date時間類型(System.currentTimeMillis()是目前的系統時間,加一個有效時間期限就行)
;
6、jwt是一個對象,要用【.compact( )】方法才能轉化成【字符串】
;
@Test void testGenJWT(){//先設置一個哈希表集合,因為jwt令牌的【有效信息部分】要用【哈希表集合類型】接收Map<String , Object> claims = new HashMap<>(); //值用Object因為可能是數字、可能是字符串claims.put("id",1);claims.put("name","岑梓銘");String jwt = Jwts.builder() //builder就是創建一個JWT令牌.signWith(SignatureAlgorithm.HS256,"yjtlwkbz") //設置【簽名算法的類型(比如HS256)】、【簽名內容(比如yjtlwkbz)】.addClaims(claims) //有效荷載部分,接收哈希表集合類型,存入用戶有效信息.setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //設置有效時間1h(3600秒 * 1000毫秒).compact(); //把結果生成字符串System.out.println(jwt); }
然后提示幾點:
1、因為頂上的【@SpringBootTest】注解會影響整個項目,直接注釋了,然后點對應這個當前這個【@Test】測試方法運行最快
.
2、如果剛剛編寫生成jwt令牌代碼時爆紅爆錯,檢查這幾個問題:
—— 報錯classNotFoundException的下載jaxb-api依賴,2.1版本(版本別填錯了)
—— 使用Base64編碼字符串長度至少為43位,位數報錯的可以把 “簽名內容” 改成任意的大于等于43位的字符串(比如我代碼里的"yjtlwkbz"改成"ahjahsdgaysdgkuywdgjwdbasbcjhcjasyasgkjjsh")
。
3、還有有的人可能會出現【test】包下的test類(class文件)全變成java文件了,沒法運行,右鍵也不能新建class類文件,那可能是IDE出了點問題,清楚IDE緩存再重新啟動一次就行,見下圖
然后運行完成后,我們把控制臺生成的【jwt令牌】復制,到這個網站可以查看解析我們的【jwt令牌】的信息:JSON Web Tokens - jwt.io
解析JWT令牌的方法
更簡單,三步:
1、【Jwts.parser( )】方法解析jwt令牌
;
2、【.setSigningKey( )】方法就是根據你前面生成jwt令牌時的那個【簽名密鑰】來 “打開解密大門”
;
3、【.parseClaimsJws( )】把剛剛生成的【jwt令牌】整個塞進去就能被解析了
注意,別選成【.parseClaimsJwt( )】了,這兩是兩個東西,選下圖這個
,
4、【.getBody( )】能夠獲取出【有效荷載】部分那些具體信息,并封裝在一個Claims類型對象
@Test void testGetJWT(){Claims claims = Jwts.parser().setSigningKey("yjtlwkbz") //對應生成jwt的.signWith(SignatureAlgorithm.HS256,"yjtlwkbz")那個密鑰//對應剛剛運行生成的jwt令牌.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5bKR5qKT6ZOtIiwiaWQiOjEsImV4cCI6MTcyMDQwOTg1Nn0.OmpQBJoP50BjjPnItLBGCmhgAVTcDzYGsTBMT7qohoE").getBody(); //獲取有效荷載部分System.out.println(claims); }
注意幾點:
1、有效信息最后要用一個Claims對象來接收
;
2、一個jwt令牌有時效和使用次效,你如果超過了你設置的失效期限、或者已經運行執行了一次解析jwt令牌,那么這串jwt令牌就作廢了,需要你再次運行【生成jwt令牌】,然后再運行【解析jwt令牌】獲取信息,否則會報錯
;
3、【.parseClaimsJws( )】別寫成了【.parseClaimsJwt( )】
二、利用jwt令牌技術校驗身份
現在我們學完了最先進的jwt令牌技術,那么就來實踐一下如何運用它。
1、先為了前后端請求響應方便,封裝好一個jwt令牌工具類
很簡單,我們前面已經知道怎么【生成】和【解析】jwt令牌了,那么在封裝工具類里只要改幾點:
/
1、【生成jwt令牌】的時候首先要接收一個前端傳過來的裝著用戶有效信息的【哈希表集合】參數;并最后要把生成的令牌字符串return出去
.
2、【解析jwt令牌】的時候需要接收生成的jwt字符串;并把解析獲得【有效荷載信息】return回前端
package com.czm.tliaswebmanagement.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;
import java.util.HashMap;
import java.util.Map;//別忘了加這個注解,讓類放入IOC容器
@Component
public class JwtUtils {private static String signKey = "yjtlwkbz"; //定義【簽名密鑰】是“yjtlwkbz”private static Long time = (long)60*5 * 1000; //定義有效時間是5分鐘/*** 接收前端有效信息,生成jwt令牌并返回給前端* @param claims* @return*/public String generateJWT(Map<String,Object> claims){String jwt = Jwts.builder() //builder就是創建一個JWT令牌.signWith(SignatureAlgorithm.HS256,signKey) //設置【簽名算法的類型】、【簽名內容】.addClaims(claims) //有效荷載部分,接收哈希表集合類型,存入用戶有效信息.setExpiration(new Date(System.currentTimeMillis() + time)) //設置有效時間.compact(); //把結果生成字符串return jwt;}/*** 接收前端傳來的jwt令牌,解析并返回有效荷載* @param jwt* @return*/public Claims parseJWT(String jwt){Claims claims = Jwts.parser().setSigningKey(signKey).parseClaimsJws(jwt).getBody();return claims;}
}
2、然后完成登錄接口(三層架構)代碼編寫
1、首先根據接口文檔規定,來確定前端傳入的是什么格式數據
比如這個文檔,以它為例子,那么確定前端傳入【JSON格式】的【用戶登錄信息】,那么就要用一個Emp對象(我前幾篇一直用的案例,員工對象)來接收這些參數值,然后用【@RequestBody】解析JSON成對象。然后post請求跟接口是“/login”,那就【@PostMapping("/login")】
(contrller)
。
2、第二步,在controller層調用service、并把剛剛解析的參數Emp傳給service,service調用mapping進行sql查詢,根據這個【用戶登錄信息】參數查詢完數據庫之后返回結果,再一級一級返回controller,老生常談的流程我就不細說了。
(controllerr)
(service )
(mapping)????????
。
3、然后在controller層再用一個【新的Emp對象】接收【查詢完返回的結果】,如果查詢到結果就說明數據庫有這個賬戶,那么調用【JWT工具類】為這個賬戶【生成一個jwt令牌】(生成jwt的邏輯,在JWT工具類已經幫我們做好了,我們只需要傳一個【裝有用戶信息】的【Map哈希表集合類的參數】給JWT工具類就行了)
。
4、最后,在查詢到賬戶的情況下,將生成含有用戶信息的JWT令牌返回給前端即可;如果查不到信息,就說明賬戶密碼有誤,查無此人,那就直接返回失敗。
controller的完整代碼:(其他的層的就不展示了)
package com.czm.tliaswebmanagement.controller;import com.czm.tliaswebmanagement.pojo.Emp;
import com.czm.tliaswebmanagement.pojo.Result;
import com.czm.tliaswebmanagement.service.EmpService;
import com.czm.tliaswebmanagement.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.HashMap;
import java.util.Map;@Slf4j
//@RequestMapping("/emps")
@RestController
public class EmpController {@Autowiredprivate EmpService empService;//獲取JWT令牌工具類@Autowiredprivate JwtUtils jwtUtils;/*** 登錄接口*/@PostMapping("/login")public Result login(@RequestBody Emp emp){log.info("傳過來的員工賬號密碼信息:{}",emp);//先調用service查找數據庫有無此賬戶Emp e = empService.login(emp);//判斷能否根據用戶名、密碼在數據庫查到此人//有的話,生成屬于它的令牌,并返回給他if(e != null){//因為生成jwt令牌需要的是【哈希表集合】類型,所以用一個【哈希表集合】裝查到的員工信息Map<String , Object> claims = new HashMap<>();//這里經過service、mapper查詢回來的員工信息e,獲取出他的id、name、username作為有效荷載信息claims.put("id",e.getId());claims.put("name",e.getName());claims.put("username",e.getUsername());//調用jwt工具類生成jwt方法獲取jwt令牌String jwt = jwtUtils.generateJWT(claims);//然后把jwt令牌返回給前端return Result.success(jwt);}//那么如果數據庫都沒查到這個賬戶,就說明賬號密碼輸入錯誤,返回錯誤就行了return Result.error("登陸失敗,查無此人");}
}
然后我要解釋一下這個jwt令牌到底在哪里傳輸?
就在一個叫【token】的玩意里存著,你可以理解為【token】是一張磁卡,然后?jwt?就是類似這個磁卡的信號信息,你用【token】這個磁卡刷門禁、刷刷卡機的時候,把 jwt 信息傳過去驗證。
然后這個【token】可以放請求體、也可以是請求頭,不過一般都是放請求頭