软件开发创新课程设计---针对图书商城的登录session缺点改进---基于JWT令牌验证技术
写在开头的话
本文章为学校课程【软件开发与创新】的作业文章,存在不成熟不完善的地方,欢迎大家一起讨论
阅读本文需要对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拦截器的开发,更重要的是建立了"从业务痛点出发选择技术方案"的思维方式。后续我会持续完善该项目:补充刷新令牌、黑名单校验、密钥规范化管理等功能,同时完成全量回归测试,确保改造后的登录模块既安全可靠,又能兼容原项目的所有业务流程。
这次实践也让我明白,优秀的软件系统不是一蹴而就的,而是在持续发现问题、解决问题的过程中迭代优化的——小到一个拦截器的放行规则,大到整个认证体系的选型,每一个细节的打磨,都是软件开发创新的体现。

浙公网安备 33010602011771号