软件开发创新课程设计---针对图书商城的登录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拦截器的开发,更重要的是建立了"从业务痛点出发选择技术方案"的思维方式。后续我会持续完善该项目:补充刷新令牌、黑名单校验、密钥规范化管理等功能,同时完成全量回归测试,确保改造后的登录模块既安全可靠,又能兼容原项目的所有业务流程。

这次实践也让我明白,优秀的软件系统不是一蹴而就的,而是在持续发现问题、解决问题的过程中迭代优化的——小到一个拦截器的放行规则,大到整个认证体系的选型,每一个细节的打磨,都是软件开发创新的体现。

posted @ 2026-03-05 23:14  Xkzxyyqq  阅读(66)  评论(0)    收藏  举报