文章目錄
- 前置知識
- Authorization頭部和 Cookie
- CRSF攻擊
- JWT概念
- JWT認證流程
- 使用
- Springboot整合JWT
- JwtUtil
- JWT案例
- 控制器
- JWT攔截器
- 注冊攔截器
- 結果
- session VS Jwt
前置知識
Authorization頭部和 Cookie
Authorization
頭部和 Cookie
是 HTTP 協議中兩種不同的身份認證 / 信息傳遞機制.
-
Authorization
頭部
是 HTTP 協議中專門用于傳遞認證信息的請求頭,通常格式為:Authorization: <認證方案> <憑證>
常見的認證方案有
Bearer
(用于 JWT、OAuth2.0 令牌)、Basic
(基礎認證)等。
核心用途:向服務器證明客戶端的身份(如 “我是已登錄的用戶 XXX”),僅在需要身份驗證的請求中使用。 -
Cookie
是服務器通過Set-Cookie
響應頭下發給客戶端的小型數據片段,客戶端(如瀏覽器)會在后續請求中自動附加到請求頭中發送給服務器。
核心用途:不僅用于身份認證(如 Session ID),還可存儲其他狀態信息(如用戶偏好、購物車數據等),是客戶端與服務器之間維護 “狀態” 的主要方式。
- 自動性:
Cookie
由客戶端自動攜帶,Authorization
需手動設置,這是兩者最根本的區別,也導致了 CSRF 風險的差異。 - 安全性:
Cookie
有內置安全屬性(HttpOnly
等),Authorization
依賴令牌本身和傳輸層安全(HTTPS)。 - 靈活性:
Authorization
更適合跨域、長令牌場景,Cookie
適合傳統狀態維護和輕量數據存儲。
CRSF攻擊
- 用戶登錄目標網站 A:用戶在瀏覽器中登錄網站 A(如銀行網站),網站 A 驗證通過后,會在用戶的瀏覽器中生成并存儲身份憑證(通常是 Cookie),用于后續請求的身份識別。
- 攻擊者誘導用戶訪問惡意網站 B:用戶在未退出網站 A 的情況下,被誘導點擊了攻擊者精心構造的惡意鏈接(如郵件、聊天消息中的鏈接),訪問了攻擊者控制的網站 B。
- 惡意網站 B 發送偽造請求:網站 B 的頁面中包含一段代碼(如 JavaScript),會自動向網站 A 的服務器發送一個請求(如轉賬請求)。由于用戶的瀏覽器中仍保存著網站 A 的登錄 Cookie,這個請求會自動攜帶該 Cookie,讓網站 A 誤以為是用戶本人發起的操作。
- 網站 A 執行未授權操作:網站 A 驗證請求中的 Cookie 有效,且未對請求的來源進行嚴格校驗,從而執行了攻擊者偽造的操作(如轉賬給攻擊者的賬戶)。
CSRF 攻擊的關鍵條件
- 用戶必須已登錄目標網站(即瀏覽器中存在有效的身份憑證,如 Cookie)。
- 攻擊者必須誘導用戶在登錄狀態下訪問惡意網站。
- 目標網站未對請求的合法性進行嚴格校驗(如未驗證請求來源、未使用 CSRF Token 等防御機制)。****
JWT概念
-
概念:
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基于JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用于認證,也可被加密。
-
構成:
第一部分我們稱它為頭部(header),
第二部分我們稱其為載荷(payload, 類似于飛機上承載的物品),
第三部分是簽證(signature).
-
header
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
-
playload
載荷就是存放有效信息的地方。這些有效信息包含三個部分
- 標準中注冊的聲明
- 公共的聲明
- 私有的聲明
標準中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大于簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 : 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味著該部分信息可以歸類為明文信息。
-
signature
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64后的)
- payload (base64后的)
- secret
這個部分需要base64加密后的header和base64加密后的payload使用
.
連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret
組合加密,然后就構成了jwt的第三部分。如對稱加密:
# 偽代碼 signature = HMAC-SHA256(key=secret,message=header_encoded + "." + payload_encoded )
-
注意事項
將這三部分用
.
連接成一個完整的字符串,構成了最終的jwt注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,**在任何場景都不應該流露出去。**一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
JWT認證流程
- 用戶登錄:
用戶提交用戶名和密碼 → 服務器驗證成功 → 服務器生成 JWT(包含用戶信息和簽名) → 返回 JWT 給客戶端。 - 客戶端存儲:
客戶端(如瀏覽器)將 JWT 存儲在localStorage
或Cookie
等中。 - 后續請求:
客戶端每次請求時,在請求頭中攜帶 JWT(如Authorization: Bearer <token>
) → 服務器驗證簽名有效性 → 解析用戶信息 → 處理請求并返回結果。
使用
Springboot整合JWT
-
JWT的引入:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>
-
具體說明:
-
token是通過荷載Claims或playholder兩者之一構造的,兩者不能同時提供,否則compact()將報異常。
-
token構建是荷載一般必須兩個屬性:sub和exp
- sub:即token中保存的必要信息。
- exp:即token的過期時間,當token過了過期時間時,解析token時會拋出
ExpiredJwtException
異常。
-
將token轉換成Claims的方法是:Jwts類中的如下方法完成。
public static JwtParser parser() {return new DefaultJwtParser();}
具體實現如下:
public Claims getClaimsByToken(String token) throws ExpiredJwtException{if(null == token)return null;return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
-
-
解析token。
-
解析過程:
- 第一步:將token轉換成Claims對象
- 第二步:通過Claims對象的getSubject()方法獲取token中保存的信息。
-
將token轉換成Claims的方法是:Jwts類中的如下方法完成。
public static JwtParser parser() {return new DefaultJwtParser();}
-
具體實現如下:
public Claims getClaimsByToken(String token) throws ExpiredJwtException{if(null == token)return null;return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
-
將token轉換成Claims過程中將拋出如下異常
UnsupportedJwtException
:當token不是通過Claims對象構建的token時。ExpiredJwtException
:當token已過期時。MalformedJwtException
:當token不是有效的Claims對象構建的token時。SignatureException
:當token的Signature驗證失敗時。IllegalArgumentException
:當token為null或token是空字符串或token中只有空字符時。
-
通過Claims的方法獲取token中保存的信息。
-
具體實現如下:
/*2.通過token獲取構建時的信息*/public String getUserNameFromToken(String token) throws Exception{Claims claims = getClaimsByToken(token);return claims.getSubject();}
-
-
JwtUtil
/*** JwtToken生成的工具類* JWT token的格式:header.payload.signature* header的格式(算法、token的類型):* {"alg": "HS512","typ": "JWT"}* payload的格式(用戶名、創建時間、生成時間):* {"sub":"wang","created":1489079981393,"exp":1489684781}* signature的生成算法:* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)* Created on 2018/4/26.*/
@Component
@Getter
public class JwtTokenUtil {private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);private static final String CLAIM_KEY_USERNAME = "user_name";private static final String CLAIM_KEY_CREATED = "created";@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;@Value("${jwt.tokenHead}")private String tokenHead;/*** 根據負責生成JWT的token*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 從token中獲取JWT中的負載*/private Claims getClaimsFromToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {LOGGER.info("JWT格式驗證失敗:{}", token);}return claims;}/*** 生成token的過期時間*/private Date generateExpirationDate() {return new Date(System.currentTimeMillis() + expiration * 1000);}/*** 解密:從token中獲取登錄用戶名(項目使用)*/public String getUserNameFromToken(String token) {String username;try {Claims claims = getClaimsFromToken(token);username = claims.get(CLAIM_KEY_USERNAME,String.class);} catch (Exception e) {username = null;}return username;}/*** 加密: 根據用戶名生成token(項目使用)*/public String generateUserNameStr(String username) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME,username);claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 判斷token是否已經失效*/private boolean isTokenExpired(String token) {Date expiredDate = getExpiredDateFromToken(token);return expiredDate.before(new Date());}/*** 從token中獲取過期時間*/private Date getExpiredDateFromToken(String token) {Claims claims = getClaimsFromToken(token);return claims.getExpiration();}/*** 當原來的token沒過期時是可以刷新的** @param oldToken 帶tokenHead的token*/public String refreshHeadToken(String oldToken) {if(StrUtil.isEmpty(oldToken)){return null;}String token = oldToken.substring(tokenHead.length());if(StrUtil.isEmpty(token)){return null;}//token校驗不通過Claims claims = getClaimsFromToken(token);if(claims==null){return null;}//如果token已經過期,不支持刷新if(isTokenExpired(token)){return null;}//如果token在30分鐘之內剛刷新過,返回原tokenif(tokenRefreshJustBefore(token,30*60)){return token;}else{claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}}/*** 判斷token在指定時間內是否剛剛刷新過* @param token 原token* @param time 指定時間(秒)*/private boolean tokenRefreshJustBefore(String token, int time) {Claims claims = getClaimsFromToken(token);Date created = claims.get(CLAIM_KEY_CREATED, Date.class);Date refreshDate = new Date();//刷新時間在創建時間的指定時間內if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){return true;}return false;}// 驗證令牌有效性//沒有使用 Spring Security,就不需要依賴其提供的 UserDetails 類,也無需遵循它的用戶認證流程。// 此時驗證 JWT 令牌并獲取用戶信息的思路會更簡潔,核心是從 JWT 中解析出用戶標識(如用戶名、ID 等),// 再通過自己的業務邏輯驗證用戶合法性。public Boolean validateToken(String token) {// 解析令牌(自動驗證簽名和過期時間)Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;}}
JWT案例
這里將在springboot中展示jwt,通過攔截器的方式。(只展示后端代碼,可以使用postman測試,如果token是本地存儲localStorage,會存到postman的相應地方,和瀏覽器一樣)
大體流程如下:
- 第一次登錄,傳入用戶名和密碼
- 數據庫檢驗用戶名和密碼是否正確,如果正確,根據用戶信息(這里使用用戶名)生成token。封裝token和其他信息返回給前端。
- 前端拿到token,并且存到本地客戶端(如localstorage)。以后每次發送請求都會在請求頭中帶上
Authorization: Bearer <token>
- 再次向服務端發送請求時就會經過Jwt的攔截器,拿到請求頭中的token,解析token是否合法,如果合法即可證明用戶身份并放行。
控制器
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;//簡單登錄接口@PostMapping("/login")public Result<LoginVO> login(@RequestBody LoginDTO loginDTO) {boolean auth = userService.authenticate(loginDTO);if (!auth){return Result.error(40101, "用戶名或密碼錯誤");}// 生成tokenString token = jwtTokenUtil.generateUserNameStr(loginDTO.getUsername());String authToken = "Bearer " + token;return Result.success(new LoginVO(loginDTO.getUsername(), authToken,jwtTokenUtil.getExpiration()*1000 + System.currentTimeMillis()));}//測試接口@GetMapping("/info")public Result<String> info() {return Result.success("info");}
}
JWT攔截器
public class JwtInceptor implements HandlerInterceptor {private final JwtTokenUtil jwtTokenUtil;public JwtInceptor(JwtTokenUtil jwtTokenUtil) {this.jwtTokenUtil = jwtTokenUtil;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//獲取請求頭中令牌String token = request.getHeader("Authorization");if(token != null && token.startsWith("Bearer ")){token = token.substring(7);//驗證令牌if(jwtTokenUtil.validateToken(token)){//驗證通過,放行,提取用戶信息病傳遞到后續處理String username = jwtTokenUtil.getUserNameFromToken(token);request.setAttribute("username",username);return true;}}//驗證失敗,返回401response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT");return false;}
}
注冊攔截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInceptor()).addPathPatterns("/**").excludePathPatterns("/user/login");}@Beanpublic JwtInceptor jwtInceptor() {return new JwtInceptor(jwtTokenUtil);}
}
結果
登錄:http://localhost:8080/user/login
{"code": 200,"msg": "操作成功","data": {"username": "saber","token": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX25hbWUiOiJzYWJlciIsImNyZWF0ZWQiOjE3NTEyNzE4NjY5NTEsImV4cCI6MTc1MTM1ODI2Nn0.TF2_XwhON3H-Jn_fdG5xacATxu_4lvJqY19C1JUCKkQjWYeW2vRdh5lwlgoCjw-nqTEPbJyRrC7Hg3TBOyuCjw","expireTime": 1751358267094}
}
測試:http://localhost:8080/user/info
{"code": 200,"msg": "操作成功","data": "info"
}
session VS Jwt
-
服務器存儲壓力大,擴展性差,有性能開銷:認證信息(用戶ID,權限等)存儲在服務器端(通常是內存中),客戶端通過session ID與服務器端關聯
- 當用戶量激增時,服務器需要存儲大量的session,會占用大量的內存/存儲資源
- 在分布式系統中(如多臺服務器負載均衡),需要保證session在多臺服務器間同步,復雜度高,維護成本大
- 根據 Session ID 在內存中查找對應的 Session 對象,有性能開銷,如果存儲在redis或數據庫,性能開銷較高。
- 如果存儲在內存中,重啟系統后用戶需要重新登錄,影響用戶體驗。
-
依賴cookie,存在安全風險:通常依賴 Cookie 傳遞 Session ID,而 Cookie 存在以下風險:
-
CSRF(跨站請求偽造):攻擊者可能利用用戶的 Cookie 在其他網站發起惡意請求(如轉賬),因為 Session ID 會隨 Cookie 自動發送。
-
Cookie 劫持:如果 Session ID 通過 HTTP 明文傳輸,可能被竊聽;即使使用 HTTPS,也可能通過 XSS 攻擊獲取 Cookie 中的 Session ID,進而偽造身份。
-
JWT 對比:
可通過 HTTP Header(如Authorization: Bearer <token>
)傳遞,不依賴 Cookie,從根源上避免了 CSRF 風險。即使 Token 被存儲在 LocalStorage,雖仍可能面臨 XSS 攻擊,但可通過設置 Token 有效期較短、配合 HTTPS 等方式降低風險,且 JWT 的簽名機制能防止 Token 被篡改。
-
-
難以實現跨域認證
- 傳統 Session:
由于 Cookie 的同源策略限制,跨域請求(如前后端分離架構中,前端域名與后端 API 域名不同)時,Session ID 可能無法正常傳遞(需額外配置CORS
和 Cookie 跨域屬性),實現復雜且兼容性差。 - JWT 對比:
基于 Header 傳遞的 JWT 不受同源策略限制,可輕松支持跨域認證(如不同域名的前端應用調用同一后端 API),只需在請求頭中攜帶 Token 即可,更適合前后端分離、微服務等分布式架構。
- 傳統 Session:
-
Token 有效期管理靈活度低
-
Session
Session 的有效期通常在服務器端設置(如 30 分鐘),若需提前失效 (如用戶登出、密碼修改、賬號異常),需在服務器端主動刪除對應的 Session 數據。服務端可控性強。
-
JWT
無法修改已生成的token,其exp無法更新,但可以通過token的刷新機制,但短期 Token(如 15 分鐘)配合刷新 Token(Refresh Token)的機制,可兼顧安全性和用戶體驗,且無需服務器存儲全量認證數據。
-
場景 | JWT | 傳統 Session |
---|---|---|
分布式 / 微服務架構 | ? 無狀態,無需共享 Session | ? 需要 Session 共享機制 |
跨域認證 | ? 支持跨域(Header 傳遞) | ? 依賴 Cookie,需額外配置 |
移動端應用 | ? 輕量,易于客戶端管理 | ? 依賴 Cookie,移動端支持差 |
高并發場景 | ? 驗證快(本地計算),無需 IO | ? 頻繁查詢存儲,性能瓶頸 |
安全性要求極高 | ? Token 一旦泄露風險大 | ? 服務端可控(如強制登出) |