博客 / 詳情

返回

軟件開發創新課程設計---針對圖書商城的登錄session缺點改進---基於JWT令牌驗證技術

寫在開頭的話

本文章為學校課程【軟件開發與創新】的作業文章,存在不成熟不完善的地方,歡迎大家一起討論
閲讀本文需要對Java Web開發、Spring Boot框架有基本的瞭解

項目選擇與分析

學校這學期開了一門軟件開發創新課程,第一節課要求就是針對已有的軟件缺陷進行完善或者二次開發,剛好找到了一個同學做的圖書商城項目,讓我們部署一下試試.

image

image

image

看着還挺不錯的,不過我在研究他的登陸系統設計的時候發現了一個問題:這個項目的會話保持技術是基於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;
    }
}

軟件測試

在經過一頓大刀闊斧的改動之後,我們就可以來測試一下新的登陸系統

測試用例如下

image

部分測試結果如下:

image

image

後續還需要做迴歸測試,驗證其他的功能有沒有受到影響,不過限於本文篇幅,就不具體的書寫了

思考與總結

本次針對圖書商城項目登錄模塊的改造,從傳統Session機制遷移至JWT令牌驗證體系,不僅解決了原項目在分佈式部署、多端訪問、服務器性能等方面的核心痛點,也讓我對Java Web開發中身份認證技術的選型與落地有了更深刻的理解,現將整個過程的思考與收穫總結如下:

一、技術改造的核心收穫

  1. 感受到了"無狀態"架構的實用性
    改造前對"無狀態"的認知僅停留在理論層面,實操後才真正體會到:JWT將用户身份信息封裝在令牌中,服務器無需存儲會話數據,不僅降低了內存佔用,更讓項目具備了橫向擴展的基礎——只需多部署幾台應用服務器,無需額外搭建Session共享中間件,就能支撐更高的併發量,這對商城類面向海量用户的項目至關重要。

  2. 意識到技術選型應該平衡安全性與易用性
    開發過程中深刻意識到JWT並非"萬能鑰匙":雖然擺脱了Cookie同源策略的限制,適配了Web、小程序等多端場景,但也存在令牌一旦簽發無法主動作廢的問題。為此我在設計中強化了兩點:一是嚴格控制Payload僅存儲用户名、用户ID等非敏感信息,杜絕密碼、手機號等數據泄露風險;二是將令牌過期時間設為1小時,既避免頻繁登錄影響用户體驗,也降低了令牌被盜用後的風險窗口。

  3. 攔截器設計的邊界思考
    編寫JWT攔截器時,我踩過"過度攔截"的坑:最初未區分靜態資源(圖片、CSS)和業務接口,導致頁面樣式加載失敗。因此攔截器設計必須明確"攔截範圍"——僅對購物車、下單、個人信息等敏感業務接口做令牌校驗,對登錄、註冊、商品展示等公開接口及靜態資源直接放行,才能保證系統功能的完整性。

二、現存問題與優化方向

  1. 缺少刷新令牌機制
    當前令牌過期後用户需重新登錄,體驗較差。後續可引入"雙令牌"機制:簽發Access Token(短期有效,1小時)和Refresh Token(長期有效,7天),當Access Token過期時,前端用未過期的Refresh Token請求新的Access Token,既保證安全性,又提升用户體驗。

  2. 令牌校驗邏輯存在優化空間
    現有validateToken方法僅校驗簽名和過期時間,未對用户狀態做校驗(如用户賬號被封禁後,令牌仍可能有效)。後續可結合Redis維護"無效令牌黑名單",將封禁用户、主動登出的令牌存入黑名單,在校驗時增加"令牌是否在黑名單"的判斷,彌補JWT無法主動作廢的缺陷。

三、對軟件開發創新的理解

本次改造並非從零開發新功能,而是針對已有項目的缺陷進行"精準優化",這讓我認識到:軟件開發的創新未必是技術的顛覆,更多是結合業務場景的"合適選型"與"細節打磨"。原項目使用Session在單體測試環境中能正常運行,但未考慮商城項目的實際使用場景(多端、高併發),而我的改造核心就是讓技術方案匹配業務需求——這正是"軟件開發與創新"課程的核心要義:技術服務於業務,而非為了用技術而用技術

四、總結

通過本次改造,我不僅掌握了JWT的核心用法(令牌生成、解析、校驗)、Spring Boot攔截器的開發,更重要的是建立了"從業務痛點出發選擇技術方案"的思維方式。後續我會持續完善該項目:補充刷新令牌、黑名單校驗、密鑰規範化管理等功能,同時完成全量回歸測試,確保改造後的登錄模塊既安全可靠,又能兼容原項目的所有業務流程。

這次實踐也讓我明白,優秀的軟件系統不是一蹴而就的,而是在持續發現問題、解決問題的過程中迭代優化的——小到一個攔截器的放行規則,大到整個認證體系的選型,每一個細節的打磨,都是軟件開發創新的體現。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.