寫在開頭的話
本文章為學校課程【軟件開發與創新】的作業文章,存在不成熟不完善的地方,歡迎大家一起討論
閲讀本文需要對Java Web開發、Spring Boot框架有基本的瞭解
項目選擇與分析
學校這學期開了一門軟件開發創新課程,第一節課要求就是針對已有的軟件缺陷進行完善或者二次開發,剛好找到了一個同學做的圖書商城項目,讓我們部署一下試試.
看着還挺不錯的,不過我在研究他的登陸系統設計的時候發現了一個問題:這個項目的會話保持技術是基於session實現的,關於session技術請看這篇文章Session詳解,學習Session,這篇文章就夠了(包含底層分析和使用)
技術分析與選擇
傳統Session登錄機制,是基於服務器端存儲用户會話信息、客户端攜帶Cookie攜帶SessionID實現身份校驗,在單體小型項目中尚能運行,但放到圖書商城這類具備多端訪問、可擴展需求的項目中,缺點被無限放大,具體體現在這幾點:
-
服務器存儲壓力大,橫向擴展受阻
Session信息默認存儲在服務器內存中,圖書商城面向海量用户,每一位登錄用户都會生成獨立Session,高併發場景下會大幅佔用服務器資源,甚至引發內存溢出.而且Session與服務器強綁定,單台服務器宕機,對應用户會話全部失效;若做集羣分佈式部署,還需額外搭建Redis、Memcached實現Session共享,大幅增加系統複雜度和運維成本,違背輕量化設計理念. -
跨域與多端適配性極差
當下圖書商城早已不侷限於Web端、小程序、APP、H5移動端等多端並行訪問已成常態.Session依賴Cookie傳遞會話標識,而Cookie受同源策略限制,跨域請求時無法正常傳遞,想要實現多端互通,需要繁瑣的跨域配置,兼容性差且極易出現登錄失效、身份校驗失敗的問題. -
無狀態拓展困難,安全性可控性弱
Session機制屬於有狀態會話,服務器需持續維護用户登錄狀態,靈活性不足.同時,SessionID存儲在Cookie中,易遭遇CSRF跨站請求偽造攻擊,即便設置Cookie加密、過期時間,依舊存在安全隱患,難以滿足商城系統對用户賬號安全的高要求.
針對這些痛點,我們決定摒棄傳統的Session技術,選用JWT(JSON Web Token)令牌驗證技術,實現無狀態、分佈式友好、高適配性的登錄驗證方案,適配圖書商城這類對併發、多端訪問有要求的項目的業務需求.
JWT令牌驗證技術介紹
JWT即JSON Web令牌,是一種輕量級、自包含、跨語言的身份驗證規範,核心優勢在於其具有無狀態、分佈式友好、多端兼容、安全性高等特點.
1.JWT基本原理
JWT令牌本質是一段經過加密簽名的JSON字符串,由Header(頭部)、Payload(負載)、Signature(簽名)三部分組成,三者使用英文句號"."分隔,長成這樣:
xxxxx.yyyyy.zzzzz
-
Header:聲明令牌類型和加密算法,通常採用HS256對稱加密算法.
-
Payload:存儲用户核心身份信息(如用户ID、用户名、角色權限)、令牌簽發時間、過期時間等非敏感數據,但是不建議存儲密碼等涉密信息.
-
Signature:通過Header指定的算法,將Header、Payload和密鑰加密生成,用於校驗令牌是否被篡改,保障安全性.
2.JWT對比Session
對比傳統的Session技術,JWT具有以下優勢:
-
無狀態存儲:服務器無需存儲JWT令牌,大幅降低內存壓力,集羣部署時無需做會話共享,任意服務器均可校驗令牌,橫向擴展難度低.
-
跨域多端適配:JWT通過請求頭(Authorization)傳遞,擺脱Cookie同源策略限制,Web、小程序、APP等多端通用.
-
安全性可控:令牌自帶簽名校驗,篡改即失效,可設置靈活過期時間,搭配刷新令牌機制,兼顧安全性與用户使用體驗.
-
輕量高效:令牌體積小,傳輸速度快,身份校驗流程簡潔,提升接口響應效率.
項目實戰:圖書商城JWT登陸驗證實現
本項目是基於SpringBoot3技術棧來實現的圖書商城後端,JWT登錄驗證模塊的核心流程分為用户登錄簽發令牌、請求攔截校驗令牌、令牌過期設置三步.
1.創建JWT令牌工具類
首先導入所有相關包
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
//上面三個包是JWT令牌相關
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
//這兩個是SpringBoot的IOC容器配置
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
//這些是加密相關的工具包
其次創建JWT工具類定義,請注意,一個簡單的JWT工具類至少應該包含:
- 密鑰算法
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
- 令牌過期時間
private static final long EXPIRATION_TIME = 3600 * 1000;// 1小時
- 生成JWT令牌的方法
/**
* 生成JWT令牌
* @param username 用户名
* @param userId 用户ID
* @return JWT令牌
*/
// 方法定義:接收用户名和用户ID,返回生成的Token字符串
public String generateToken(String username, String userId) {
// 1. 創建HashMap存儲自定義的JWT聲明(Claims)
Map<String, Object> claims = new HashMap<>();
// 2. 往聲明中添加用户名和用户ID
claims.put("username", username);
claims.put("userid", userId);
// 3. 使用Jwts構建器生成Token
return Jwts.builder()
.setSubject(username) // 設置Token的主題(標準聲明)
.addClaims(claims) // 添加自定義聲明
.signWith(SECRET_KEY, SignatureAlgorithm.HS512) // 使用HS512算法和密鑰簽名
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 設置過期時間
.compact(); // 壓縮生成最終的Token字符串
}
//我這裏封裝了用户名和id信息,請根據實際需要選擇封裝信息,但是請不要封裝密碼等敏感信息!!!請不要封裝密碼等敏感信息!!!請不要封裝密碼等敏感信息!!!
- 令牌解析方法
/**
* 解析JWT令牌並獲取用户信息
* @param token JWT令牌
* @return 用户信息Map
*/
public Map<String, Object> parseToken(String token) {
try {
// 1. 構建JWT解析器,設置簽名密鑰並解析token
var claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY) // 設置驗證簽名的密鑰
.build() // 構建解析器
.parseClaimsJws(token); // 解析token並驗證簽名
// 2. 從解析結果中提取用户信息,封裝到HashMap
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("username", claims.getBody().get("username")); // 提取用户名
userInfo.put("userid", claims.getBody().get("userid")); // 提取用户ID
userInfo.put("exp", claims.getBody().getExpiration()); // 提取過期時間
System.out.println(userInfo); // 打印用户信息
return userInfo; // 返回用户信息Map
} catch (Exception e) {
// 3. 捕獲所有異常,封裝成運行時異常拋出
throw new RuntimeException("JWT令牌解析失敗: " + e.getMessage());
}
}
- 令牌驗證方法
/**
* 驗JWT令牌是否有效
* @param token JWT令牌
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
// 1. 創建JWT解析器構建器
Jwts.parserBuilder()
// 2. 設置簽名密鑰(用於驗證token的簽名是否被篡改)
.setSigningKey(SECRET_KEY)
// 3. 構建解析器實例
.build()
// 4. 解析token並驗證:
// - 驗證簽名是否正確
// - 檢查token是否過期(默認校驗exp聲明)
// - 檢查token格式是否合法
.parseClaimsJws(token);
// 5. 解析成功=token合法,返回true
return true;
} catch (Exception e) {
// 6. 任何異常(簽名錯誤、過期、格式錯誤等)都返回false
return false;
}
}
完整代碼如下
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具類,用於生成和解析JWT令牌
*/
@Component
@Configuration
public class JwtUtils {
// 默認密鑰
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// 令牌過期時間
private static final long EXPIRATION_TIME = 3600 * 1000; // 1小時
/**
* 生成JWT令牌
* @param username 用户名
* @param userId 用户ID
* @return JWT令牌
*/
public String generateToken(String username, String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
claims.put("userid", userId);
return Jwts.builder()
.setSubject(username)
.addClaims(claims)
.signWith(SECRET_KEY, SignatureAlgorithm.HS512)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.compact();
}
/**
* 解析JWT令牌並獲取用户信息
* @param token JWT令牌
* @return 用户信息Map
*/
public Map<String, Object> parseToken(String token) {
try {
var claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token);
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("username", claims.getBody().get("username"));
userInfo.put("userid", claims.getBody().get("userid"));
userInfo.put("exp", claims.getBody().getExpiration());
System.out.println(userInfo);
return userInfo;
} catch (Exception e) {
throw new RuntimeException("JWT令牌解析失敗: " + e.getMessage());
}
}
/**
* 驗JWT令牌是否有效
* @param token JWT令牌
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
在完成工具類的開發之後,我們還需要一個JWT認證攔截器,用於攔截請求,對圖書選購,購物車查看,下單流程,個人信息查看等敏感接口請求進行攔截,只有攜帶合法令牌的請求才能訪問.
攔截器代碼如下:
/**
* JWT認證攔截器
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String path = request.getRequestURI();
System.out.println("當前請求路徑:" + path);
// 檢查是否命中排除規則
boolean isExcluded = path.startsWith("/img/") || path.startsWith("/static/") ;
System.out.println("是否排除:" + isExcluded);
if (isExcluded) {
return true; // 排除的路徑直接放行
}
// 在 preHandle 方法中提前放行靜態資源
if (request.getRequestURI().startsWith("/img/")) {
return true;
}
//檢查是否為登錄請求,如果是則放行
if (request.getRequestURI().contains("/login")) {
System.out.println("登錄請求");
return true;
}
//檢查是否是註冊請求,如果是則放行
if (request.getRequestURI().contains("/register")) {
System.out.println("註冊請求");
return true;
}
//檢查是否是商品信息獲取請求,如果是則放行
if (request.getRequestURI().contains("/goodsInfo")) {
System.out.println("商品信息獲取請求");
return true;
}
// 獲取請求頭中的token
String token = request.getHeader("Authorization");
// 如果請求頭中沒有token,則響應401錯誤
if (token == null) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
String resultJson = objectMapper.writeValueAsString(Result.error(401,"請先登錄"));
response.getWriter().write(resultJson);
return false;
}
System.out.println(token);
// 驗證token是否有效
if (!jwtUtils.validateToken(token)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
String resultJson = objectMapper.writeValueAsString(Result.error(401,"token無效"));
response.getWriter().write(resultJson);
return false;
}
// 驗證通過,繼續執行請求
return true;
}
}
軟件測試
在經過一頓大刀闊斧的改動之後,我們就可以來測試一下新的登陸系統
測試用例如下
部分測試結果如下:
後續還需要做迴歸測試,驗證其他的功能有沒有受到影響,不過限於本文篇幅,就不具體的書寫了
思考與總結
本次針對圖書商城項目登錄模塊的改造,從傳統Session機制遷移至JWT令牌驗證體系,不僅解決了原項目在分佈式部署、多端訪問、服務器性能等方面的核心痛點,也讓我對Java Web開發中身份認證技術的選型與落地有了更深刻的理解,現將整個過程的思考與收穫總結如下:
一、技術改造的核心收穫
-
感受到了"無狀態"架構的實用性
改造前對"無狀態"的認知僅停留在理論層面,實操後才真正體會到:JWT將用户身份信息封裝在令牌中,服務器無需存儲會話數據,不僅降低了內存佔用,更讓項目具備了橫向擴展的基礎——只需多部署幾台應用服務器,無需額外搭建Session共享中間件,就能支撐更高的併發量,這對商城類面向海量用户的項目至關重要。 -
意識到技術選型應該平衡安全性與易用性
開發過程中深刻意識到JWT並非"萬能鑰匙":雖然擺脱了Cookie同源策略的限制,適配了Web、小程序等多端場景,但也存在令牌一旦簽發無法主動作廢的問題。為此我在設計中強化了兩點:一是嚴格控制Payload僅存儲用户名、用户ID等非敏感信息,杜絕密碼、手機號等數據泄露風險;二是將令牌過期時間設為1小時,既避免頻繁登錄影響用户體驗,也降低了令牌被盜用後的風險窗口。 -
攔截器設計的邊界思考
編寫JWT攔截器時,我踩過"過度攔截"的坑:最初未區分靜態資源(圖片、CSS)和業務接口,導致頁面樣式加載失敗。因此攔截器設計必須明確"攔截範圍"——僅對購物車、下單、個人信息等敏感業務接口做令牌校驗,對登錄、註冊、商品展示等公開接口及靜態資源直接放行,才能保證系統功能的完整性。
二、現存問題與優化方向
-
缺少刷新令牌機制
當前令牌過期後用户需重新登錄,體驗較差。後續可引入"雙令牌"機制:簽發Access Token(短期有效,1小時)和Refresh Token(長期有效,7天),當Access Token過期時,前端用未過期的Refresh Token請求新的Access Token,既保證安全性,又提升用户體驗。 -
令牌校驗邏輯存在優化空間
現有validateToken方法僅校驗簽名和過期時間,未對用户狀態做校驗(如用户賬號被封禁後,令牌仍可能有效)。後續可結合Redis維護"無效令牌黑名單",將封禁用户、主動登出的令牌存入黑名單,在校驗時增加"令牌是否在黑名單"的判斷,彌補JWT無法主動作廢的缺陷。
三、對軟件開發創新的理解
本次改造並非從零開發新功能,而是針對已有項目的缺陷進行"精準優化",這讓我認識到:軟件開發的創新未必是技術的顛覆,更多是結合業務場景的"合適選型"與"細節打磨"。原項目使用Session在單體測試環境中能正常運行,但未考慮商城項目的實際使用場景(多端、高併發),而我的改造核心就是讓技術方案匹配業務需求——這正是"軟件開發與創新"課程的核心要義:技術服務於業務,而非為了用技術而用技術。
四、總結
通過本次改造,我不僅掌握了JWT的核心用法(令牌生成、解析、校驗)、Spring Boot攔截器的開發,更重要的是建立了"從業務痛點出發選擇技術方案"的思維方式。後續我會持續完善該項目:補充刷新令牌、黑名單校驗、密鑰規範化管理等功能,同時完成全量回歸測試,確保改造後的登錄模塊既安全可靠,又能兼容原項目的所有業務流程。
這次實踐也讓我明白,優秀的軟件系統不是一蹴而就的,而是在持續發現問題、解決問題的過程中迭代優化的——小到一個攔截器的放行規則,大到整個認證體系的選型,每一個細節的打磨,都是軟件開發創新的體現。