前言
目前在開發的小組結課專案中想試試JWT認證,簡單分享一下,並看看與Session認證的異同。
登入認證(Authentication)的概念非常簡單,就是透過一定手段對使用者的身份進行確認。
我們都知道 HTTP 是無狀態的,伺服器接收的每一次請求,對它來說都是 “新來的”,並不知道客戶端來過。
舉個例子:
客戶端A: 我是A, 給我一瓶水。
服務端B: 好,給你。
客戶端A: 再給我來個麵包。
服務端B: 啥,你是誰?
即每一個請求對伺服器來說都是新的。
我們不可能每次操作都讓使用者輸入使用者名稱和密碼,那麼我們如何讓伺服器記住我們登入過了呢?
那就是憑證。即每次請求都給伺服器一個憑證告訴伺服器我是誰。
現在一般使用比較多的認證方式有四種:
- Session
- Token
- SSO單點登入
- OAtuth登入
下面就來說說比較常用的前兩種。
Session
Cookie + Session
最常見的就是 Cookie + Session 認證。
Session,是一種有狀態的會話管理機制,其目的就是為了解決HTTP無狀態請求帶來的問題。
當使用者登入認證請求透過時,服務端會將使用者的資訊儲存起來,並生成一個 SessionId
傳送給前端,前端將這個 SessionId
儲存起來。之後前端再傳送請求時都攜帶 SessionId
,伺服器端再根據這個 SessionId
來檢查該使用者有沒有登入過。
這個 SessionId
, 一般是儲存在Cookie中。
如果使用者第一次訪問某個伺服器時,伺服器響應資料時會在響應頭的 Set-Cookie 標識裡將Session Id 返回給瀏覽器,瀏覽器就將標識中的資料存在Cookie中。
下面我們來簡單寫個 demo 測試一下:
初始化一個spring boot 專案,並且程式碼如下:
demo
我們只需要在使用者登入的時候將使用者資訊存在HttpSession中
@RestController
public class UserController {
@PostMapping("login")
public String login(@RequestBody User user, HttpSession session) {
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 登入成功 寫入Session
session.setAttribute("sessionId", user);
return "login success";
}
return "username or password incorrect";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 登出 刪除Session
session.removeAttribute("sessionId");
return "logout success";
}
public String api(HttpSession session) {
// 使用者操作 判斷是否登入
User user = (User) session.getAttribute("sessionId");
if (user == null) {
return "please login";
}
return "return data";
}
}
下面我們向 login
地址 請求,並檢視響應。 可以看到,使用者登入時服務端會返回 Set-Cookie
欄位。這些工作 Servlet幫我們做好了
下面我們向 api
地址請求。可以看到, 後續訪問服務端自動就會攜帶Cookie:
服務端認證身份成功,返回資料。
Session + Header認證
當前開發的幾個專案都是採用這種模式。
即 將 Session 會話放進 請求頭中作為認證資訊。
下面簡單寫個 demo 並用 postman 測試
demo
pom.xml:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
HeaderAndParamHttpSessionStrategy:
將cookie認證改為Header認證, 請求關鍵字為 x-auth-token
/**
* Header或是請求引數中的帶有 token 的認證策略
* */
public class HeaderAndParamHttpSessionStrategy extends HeaderHttpSessionStrategy {
/**
* header認證關鍵字名稱
*/
private String headerName = "x-auth-token";
@Override
public String getRequestedSessionId(HttpServletRequest request) {
String token = request.getHeader(this.headerName);
return (token != null && !token.isEmpty()) ? token : request.getParameter(this.headerName);
}
}
MvcSecurityConfig:
使用spring 提供的 MapSessionRepository 來幫助我們管理Session
@Configuration
@EnableWebSecurity
@EnableSpringHttpSession
public class MvcSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 開放埠
.antMatchers("**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and().cors()
.and().csrf().disable();
http.headers().frameOptions().disable();
return http.build();
}
/**
* 使用header認證來替換預設的cookie認證
*/
@Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderAndParamHttpSessionStrategy();
}
/**
* 由於我們啟用了@EnableSpringHttpSession後,而非RedisHttpSession.
* 所以應該為SessionRepository提供一個實現。
* 而Spring中預設給了一個SessionRepository的實現MapSessionRepository.
*
* @return session策略
*/
@Bean
public SessionRepository sessionRepository() {
return new MapSessionRepository();
}
}
重新啟動專案:
嘗試1: 向 login
地址請求:
可以看到,服務端的響應頭上帶有了 x-auth-token, 這種將 session 放在請求頭部的認證就是 header認證。
結果: 響應頭返回 x-auth-token 欄位
嘗試2: 請求頭不帶 x-auth-token
, 向服務端請求 api
而這時候,假如客戶端傳送接下來的請求的時候,請求頭不帶上服務端返回的 x-auth-token
,那麼是無法得到認證的。
結果: 認證失敗
嘗試3: 請求頭帶 x-auth-token
, 向服務端請求 api
將登入成功之後,服務端返回
結果: 認證成功
透過以上的方式: 我們成功將 Cookie + Session 認證 ,替換為了 Header + Session 認證。
那麼換之後,有什麼好處呢?
Header + Session 相較 Cookie + Session 有幾點好處:
- 防止跨站指令碼攻擊(XSS):使用 Cookie 儲存會話 ID 的話,Cookie 是透過瀏覽器自動管理的,容易受到 XSS 攻擊的影響。而將會話 ID 儲存在頭部,可以避免這種攻擊。
- 避免 CSRF 攻擊:使用 Cookie 儲存會話 ID 的話,攻擊者可以利用 CSRF 攻擊來獲取 Cookie 中的會話 ID,從而偽造使用者請求。將會話 ID 儲存在頭部的話,可以避免這種攻擊。
- 不受第三方 Cookie 支援的限制:如果使用者的瀏覽器禁用了第三方 Cookie,那麼使用 Cookie + Session 的方式就無法使用。而將會話 ID 儲存在頭部,不需要使用 Cookie,不受這個限制。
缺點:
- 會話 ID 儲存在頭部,可能被重放攻擊利用
- 執行效能代價較高:由於 HTTP 頭比 Cookie 更大,因此將會話 ID 儲存在頭部通常會佔用更多的網路資源,增加傳輸延遲。
因此,應該根據具體的應用場景、協議、需求和安全要求來選擇合適的身份認證方式。
Token 認證
除了Session之外,目前比較流行的做法就是使用JWT(JSON Web Token)。
JWT具有以下倆種特性:
- 可以將一段資料加密成一段字串,也可以從這字串解密回資料
- 可以對這個字串進行校驗,比如有沒有過期,有沒有被篡改
看到這,這不和 Session + Header 認證一樣嘛!就是把 SessionId 換成了JWT字串而已,有必要麼??
Session 和 JWT有一個重要的區別,就是 Session 是有狀態的,JWT是無狀態的。
即,Session 在服務端儲存了使用者資訊,而JWT在服務端沒有儲存任何資訊。
當前端攜帶Session Id到服務端時,服務端要檢查其對應的 HttpSession 中有沒有儲存使用者資訊,儲存了就代表登入了。
當使用JWT時,服務端只需要對這個字串進行校驗,校驗透過就代表登入了。
下面繼續從一個Demo體驗:
demo
pom.xml:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
寫一個工具類
public interface CommonService {
/**
* 簽名秘鑰
*/
String SECRET = "shareMusic";
// 根據使用者id生成token
static String createJwtToken(Long id) {
long ttlMillis = -1; // 表示不新增過期時間
return createJwtToken(id.toString(), ttlMillis);
}
/**
* 生成Token
*
* @param id 編號
* @param ttlMillis 簽發時間 (有效時間,過期會報錯)
* @return token String
*/
static String createJwtToken(String id, long ttlMillis) {
// 簽名演算法 ,將對token進行簽名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成簽發時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 透過秘鑰簽名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.signWith(signatureAlgorithm, signingKey);
// 如果指定了過期時間,則新增
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
// 驗證並解析JWT
static Claims parseJWT(String jwt) { // 如果是空字串直接返回null
if (jwt == null ||jwt.isEmpty()) {
return null;
}
// 這個Claims物件包含了許多屬性,比如簽發時間、過期時間以及存放的資料等
Claims claims = null;
// 解析失敗了會丟擲異常,所以我們要捕捉一下。token過期、token非法都會導致解析失敗
try {
claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
} catch (JwtException e) {
System.err.println("解析失敗!");
}
return claims;
}
}
同時改寫一下我們的 Controller:
登入成功返回
@PostMapping("login")
public String login(@RequestBody User user, HttpServletResponse response) {
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 判斷成功 返回頭加入token
String token = CommonService.createJwtToken(user.getUsername());
response.setHeader("Authorization", token);
return "login success";
}
return "username or password incorrect";
}
@GetMapping("/api")
public String api(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
// 解析失敗就提示使用者登入
if (CommonService.parseJWT(jwt) == null) {
return "please login";
}
return "return data";
}
重啟服務開始測試。
嘗試1: 向 login 地址請求.
可以看到,我們成功地在服務端響應頭中返回了 JWT,它是以 Authorization
作為名字的欄位。
嘗試2: 不帶JWT, 向 api 地址請求.
認證失敗。
嘗試3: 帶JWT, 向 api 地址請求.
結果: 認證成功, 返回資料。
上面我們成功使用JWT完成了登入和請求api。下面簡單看一下它的原理:
JWT原理:
JWT 通常由三部分組成:Header、Payload 和 Signature。
- Header:包含 Token 型別(即 JWT)和所使用的簽名演算法資訊(如 HS256)。
- Payload:儲存了一些描述資訊,如 Token 的頒發者、過期時間、訪問許可權等,也可以包含一些使用者的自定義資料。
- Signature:由伺服器端生成,用於驗證 Token 的正確性和完整性。
我們將剛才獲取到的JWT去線上網站解密一下:
可以看到,獲取到了我們傳輸的資訊: 使用者名稱
線上網站將 Header 和 Payload 中的 Base64 編碼資訊透過簡單的演算法將其還原成原始的明文資料
可以看到簽名是無法解密的,這是因為 JWT 的簽名主要是用於保證 JWT 的完整性和防止 JWT 被篡改 或偽造。
signature 可以選擇對稱加密演算法或者非對稱加密演算法,常用的就是 HS256、RS256。
具體JWt解析過程可以看這篇文章: https://www.freecodecamp.org/chinese/news/how-to-sign-and-val...
JWT 登出
你可能會留意到, 上面的JWT方法沒有登出的功能。那麼如何登出??
事實上,JWT 是無狀態的認證方式,因此它本身並不提供登出的機制。讓我們從後臺登出token,這對於jwt來說並不是那麼簡單,並不能像刪除 session 那樣來刪除token。
JWT的目的與session不同,不可能強制刪除或失效已經生成的token。
我們可以採用下面兩種方式:
Token 過期⏰。
透過過期時間機制。可以在生成 JWT Token 時設定一個過期時間,一旦 Token 過期後則視為無效。透過這種方式,可以保證 Token 在一定時間內有效,同時也避免了 Token 濫用和被盜用的風險。
我還是想登出
假如我有一個嚴格的登出功能,無法等待Token自動過期怎麼辦??️
那麼儲存一個所謂的“名單”,判斷Token是有效的。一般可以採用 Redis,
校驗時,檢查提供的 token 在 redis 中是否有效,如何無效的話就讓使用者去登入。
從這個方面也體現出了 JWt 更適合分散式結構。
Session 和 JWT
兩者的不同:
儲存位置:Session 資訊是儲存在服務端的,而 JWT 將認證資訊儲存在客戶端的 Token 中。
是否需要狀態:Session 基於狀態來維護會話,如果會話狀態丟失或者被篡改,伺服器將會重新初始化會話。而 JWT 身份認證機制是無狀態的,每個請求均包含足夠的資訊,伺服器不需要維持任何狀態。這一點使得 JWT 身份認證機制特別適合於分散式系統。
安全性:Session 是基於某種演算法生成的 Session ID 來維護使用者狀態的,如果 Session ID 被竊取或者偽造,會話會受到攻擊,憑證會失效。而 JWT 透過簽名來防止偽造和篡改,只有在經過驗證後才能使用。
擴充套件性:Session 方案一般適用於單一的服務或者單個應用,而 JWT 身份認證機制適用於跨域、分散式服務呼叫等多場景。
其實Session認證更適合我們平時的場景,可以看這篇文章,講得很好https://www.796t.com/content/1546004284.html
JWT更適合一次性操作的認證:,頒發一個有效期極短的JWT,即使暴露了危險也很小,由於每次操作都會生成新的JWT,因此也沒必要儲存JWT,真正實現無狀態。
例如: 服務B你好, 服務A告訴我,我可以操作<JWT內容>, 這是我的憑證(即JWT)
參考文章:
https://www.cnblogs.com/RudeCrab/p/14251154.html#%E6%94%B6%E5...
https://segmentfault.com/a/1190000041216780
https://cloud.tencent.com/developer/news/837117