写在开头的话
本文章为学校课程【软件开辟与创新】的作业文章,存在不成熟不美满的地方,接待各人一起讨论
阅读本文须要对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(署名)三部门构成,三者利用英文句号"."分隔,长成如许:
- 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令牌
- * @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拦截器的开辟,更告急的是创建了"从业务痛点出发选择技能方案"的头脑方式。后续我会连续美满该项目:增补革新令牌、黑名单校验、密钥规范化管理等功能,同时完玉成量回归测试,确保改造后的登录模块既安全可靠,又能兼容原项目标全部业务流程。
这次实践也让我明白,良好的软件体系不是一挥而就的,而是在连续发现标题、办理标题标过程中迭代优化的——小到一个拦截器的放行规则,大到整个认证体系的选型,每一个细节的打磨,都是软件开辟创新的表现。
|