当前位置: 首页 > news >正文

【技术派后端篇】技术派中 Session/Cookie 与 JWT 身份验证技术的应用及实现解析

在现代Web应用开发中,身份验证是保障系统安全的重要环节。技术派在身份验证领域采用了多种技术方案,其中Session/Cookie和JWT(JSON Web Token)是两种常用的实现方式。本文将详细介绍这两种身份验证技术在技术派中的应用及具体实现。

1 Session/Cookie身份验证

1.1 基本原理

技术派的用户登录信息主要通过Session/Cookie机制实现。其核心原理是利用Cookie中的JESSIONID作为用户身份标识,若JESSIONID相同,则视为同一用户。服务器会在内存中存储Session数据,并设置过期时间,通常每次用户访问时都会刷新该过期时间。当浏览器不支持Cookie时,可通过URL重写的方式,将sessionId写入URL地址中,参数名为jsessionid
在这里插入图片描述

1.2 在SpringBoot项目中的实现

项目仓库(GitHub):https://github.com/itwanger/paicoding
项目仓库(码云):https://gitee.com/itwanger/paicoding

1.2.1 登录入口,保存Session

在SpringBoot项目中,通过以下代码实现登录接口并保存Session:

@RestController
public class SessionController {@RequestMapping(path = "/login")public String login(String uname, HttpSession httpSession) {httpSession.setAttribute("name", uname);return "欢迎登录:" + uname;}
}

上述代码中,在login方法中通过HttpSession对象将用户名存储到Session中,后续在会话期间,其他请求可获取该Session信息。

1.2.2 Session读取测试

提供两种常见的Session获取方式:

  • 直接从HttpSession中获取:
@RestController
public class SessionController {@RequestMapping("time")public String showTime(HttpSession session) {return session.getAttribute("name") + " ,当前时间为:" + LocalDateTime.now();}
}
  • 通过HttpServletRequest来获取:
@RestController
public class SessionController {@RequestMapping("name")public String showName(HttpServletRequest request) {return "当前登录用户:" + request.getSession().getAttribute("name");}
}

1.2.3 退出登录

退出登录的实现代码如下:

@RestController
public class SessionController {@RequestMapping("logout")public String logOut(HttpServletResponse response) throws IOException {// 这里假设通过某个上下文获取当前请求的Session信息// 实际应用中根据具体的上下文获取方式进行调整Optional.ofNullable(ReqInfoContext.getReqInfo()).ifPresent(s -> {// 执行登出逻辑,如清除Session相关数据sessionService.logout(s.getSession());});// 重定向到首页response.sendRedirect("/");return "已成功登出";}
}

1.2.4 Session实现原理

SpringBoot的Session机制工作原理如下:

  • 借助Cookie中的JESSIONID来识别用户身份,将Session存储在内存中,并设置过期时间,每次访问会刷新过期时间。
  • 当浏览器关闭后重新打开,会重新生成JESSIONID的Cookie值,导致服务器无法识别之前的用户。

1.3 技术派的身份认证流程(以管理员后台为例)

整个流程如下图所示:
在这里插入图片描述

1.3.1 登录与登出

在技术派的管理员后台登录实现中,相关代码位于AdminLoginController类:

@RestController
@RequestMapping(path = {"/api/admin/login", "/admin/login"})
public class AdminLoginController {private final UserService userService;private final SessionService sessionService;public AdminLoginController(UserService userService, SessionService sessionService) {this.userService = userService;this.sessionService = sessionService;}@PostMapping(path = {"", "/"})public ResVo<BaseUserInfoDTO> login(HttpServletRequest request, HttpServletResponse response) {String user = request.getParameter("username");String pwd = request.getParameter("password");BaseUserInfoDTO info = userService.passwordLogin(user, pwd);String session = sessionService.login(info.getUserId());if (session != null &&!session.isEmpty()) {// 将Session信息写入Cookieresponse.addCookie(new Cookie(SessionService.SESSION_KEY, session));return ResVo.ok(info);} else {return ResVo.fail(StatusEnum.LOGIN_FAILED_MIXED, "登录失败,请重试");}}@RequestMapping("logout")public ResVo<Boolean> logOut(HttpServletResponse response) throws IOException {Optional.ofNullable(ReqInfoContext.getReqInfo()).ifPresent(s -> sessionService.logout(s.getSession()));// 重定向到首页response.sendRedirect("/");return ResVo.ok(true);}
}

上述代码中,登录时先验证用户密码,生成唯一的Session值并保存到Redis缓存,然后将Session写入Cookie返回给前端;登出时清除相关Session数据并重定向。

1.3.2 用户身份识别

用户身份识别的核心逻辑位于ReqRecordFilter中。为了更好地实现功能解耦,建议将用户身份识别功能分离到独立的Filter中,原ReqRecordFilter仅负责请求日志记录。
在这里插入图片描述

/*** 初始化用户信息** @param reqInfo*/public void initLoginUser(ReqInfoContext.ReqInfo reqInfo) {HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();if (request.getCookies() == null) {return;}Optional.ofNullable(SessionUtil.findCookieByName(request, LoginService.SESSION_KEY)).ifPresent(cookie -> initLoginUser(cookie.getValue(), reqInfo));}public void initLoginUser(String session, ReqInfoContext.ReqInfo reqInfo) {BaseUserInfoDTO user = userService.getAndUpdateUserIpInfoBySessionId(session, null);reqInfo.setSession(session);if (user != null) {reqInfo.setUserId(user.getUserId());reqInfo.setUser(user);reqInfo.setMsgNum(notifyService.queryUserNotifyMsgCount(user.getUserId()));}}
  • 获取当前请求对象:借助 RequestContextHolder 来获取当前的 HttpServletRequest 对象,这个对象可用于访问请求相关的信息,像请求头、请求参数、Cookie 等。
  • 检查 Cookie 是否存在:查看请求中的 Cookie 是否为空,若为空则直接返回,不进行后续操作。
  • 查找指定 Cookie 并初始化用户信息:运用 SessionUtil.findCookieByName 方法查找名为 LoginService.SESSION_KEYCookie,若找到该 Cookie,就调用 initLoginUser 方法,传入 Cookie 的值和 ReqInfo 对象来初始化登录用户信息。
  • initLoginUser(String session, ReqInfoContext.ReqInfo reqInfo) 方法会依据传入的 session 值从数据库获取用户信息,并且更新用户的 IP 信息,之后将用户信息、会话 ID 和消息数量设置到 ReqInfo 对象里。

综上所述,initLoginUser(ReqInfoContext.ReqInfo reqInfo) 方法的核心功能是从请求的 Cookie 里获取会话信息,然后初始化登录用户的相关信息并存储到 ReqInfo 对象中。

1.4 优缺点

  • 优点:实现相对简单,符合传统Web开发的会话管理习惯,适用于对会话管理要求较高的场景。
  • 缺点
    • Session存储在内存中,受内存大小限制,可能导致内存溢出(OOM)问题。
    • 浏览器关闭再打开后,无法识别之前的用户会话。
    • Cookie若被窃取,用户身份存在安全风险。

2 JWT身份验证

2.1 基本原理

JWTJSON Web Token)是一种用于在网络应用间安全传输信息的开放标准(RFC 7519)。它由三部分组成:Header(头部)、Payload(负载)和Signature(签名)。

  • Header

    • 包含令牌类型(如JWT)和签名算法(如HMAC SHA256或RSA)。

    • 示例

      {"alg": "HS256","typ": "JWT"
      }
      
    • 经Base64编码后生成JWT第一部分。

  • Payload

    • 存储用户声明(Claims),包括三类:

      • Registered Claims:预定义字段(如iss签发者、exp过期时间)。
      • Public Claims:自定义公开字段。
      • Private Claims:业务相关私有字段。
    • 示例

      {"iss": "一灰灰blog","exp": 1692256049,"uname": "一灰","wechat": "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg"
      }
      
    • 经Base64编码后生成JWT第二部分。

  • Signature

    • 对编码后的Header和Payload,使用密钥和指定算法(如HMAC SHA256)生成签名,防止数据篡改。
    • 生成公式示例: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

2.2 在技术派中的使用姿势

2.2.1 JWT鉴权流程

在这里插入图片描述
上图展示了基于JWT(JSON Web Token)的用户身份验证和请求处理流程,具体如下:

  1. 登录请求
    前端用户输入用户名和密码,发起登录请求,将用户名和密码作为参数发送给后端。这是流程的起始点,目的是让后端验证用户身份 。
  2. 生成并返回JWT
    后端接收到登录请求后,对用户名和密码进行验证。如果验证通过,后端生成一个JWT ,并将其返回给前端。JWT包含了用户的身份信息等内容,用于后续请求的身份验证 。
  3. 前端发起请求并携带JWT
    前端接收到JWT后,将其保存到本地(如localStoragesessionStorage 等 )。之后前端向后端发起其他业务请求时,会从本地获取JWT,并将其携带在请求头中(常见是放在Authorization头中,格式为Bearer <JWT> ),发送给后端 。
  4. 后端校验JWT并响应
    • 校验失败:后端接收到请求后,从请求头中获取JWT,并进行校验(包括签名验证、有效期检查等 )。如果校验失败,说明用户未登录或JWT无效,后端返回未登录提示,或重定向到登录页面,让用户重新登录 。
    • 校验通过:如果JWT校验通过,表明用户身份合法,后端正常处理请求,并返回请求结果给前端 。

2.2.2 实现代码

技术派中JTW的核心逻辑代码位于com.github.paicoding.forum.service.user.service.help.UserSessionHelper类。

UserSessionHelper 是一个用于处理用户会话管理的工具类,借助 JWT(JSON Web Token)来存储用户的会话信息,同时结合 Redis 实现会话的主动失效功能。下面详细阐述其代码逻辑:

  1. 类和依赖注入
    @Slf4j
    @Component
    public class UserSessionHelper {@Component@Data@ConfigurationProperties("paicoding.jwt")public static class JwtProperties {private String issuer;private String secret;private Long expire;}private final JwtProperties jwtProperties;private Algorithm algorithm;private JWTVerifier verifier;
    
  • @Slf4j:使用 Lombok 注解,为类添加日志记录功能。

  • @Component:将 UserSessionHelper 类注册为 Spring 组件,使其能被 Spring 容器管理。

  • JwtProperties:静态内部类,借助 @ConfigurationProperties 注解从配置文件里读取 paicoding.jwt 前缀的配置信息,包含签发人 issuer、密钥 secret 和有效期 expire。
    在这里插入图片描述

  • algorithm:JWT 签名算法,使用 HMAC256 算法。

  • verifier:JWT 验证器,用于验证 JWT 的合法性。

  1. 构造函数
    public UserSessionHelper(JwtProperties jwtProperties) {this.jwtProperties = jwtProperties;algorithm = Algorithm.HMAC256(jwtProperties.getSecret());verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build();
    }
    
  • 构造函数接收 JwtProperties 对象,初始化签名算法和验证器。
  1. 生成会话方法 genSession
    public String genSession(Long userId) {// 1.生成jwt格式的会话,内部持有有效期,用户信息String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId));String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire())).withPayload(session).sign(algorithm);// 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息// 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000);return token;
    }
    
  • 生成包含用户信息的 JSON 字符串 session
  • 利用 JWT 创建一个包含签发人、过期时间和有效载荷的 token。
  • 将生成的 token 和对应的用户 ID 存储到 Redis 中,并设置过期时间。
  1. 移除会话方法 removeSession
    public void removeSession(String session) {RedisClient.del(session);
    }
    
  • 从 Redis 中删除指定的会话信息,实现会话的主动失效。
  1. 根据会话获取用户信息方法 getUserIdBySession

    public Long getUserIdBySession(String session) {// jwt的校验方式,如果token非法或者过期,则直接验签失败try {DecodedJWT decodedJWT = verifier.verify(session);String pay = new String(Base64Utils.decodeFromString(decodedJWT.getPayload()));// jwt验证通过,获取对应的userIdString userId = String.valueOf(JsonUtil.toObj(pay, HashMap.class).get("u"));// 从redis中获取userId,解决用户登出,后台失效jwt token的问题String user = RedisClient.getStr(session);if (user == null || !Objects.equals(userId, user)) {return null;}return Long.valueOf(user);} catch (Exception e) {log.info("jwt token校验失败! token: {}, msg: {}", session, e.getMessage());return null;}
    }
    
  • 使用 verifier 验证 JWT 的合法性,若验证失败则捕获异常并记录日志。
  • 解码 JWT 的有效载荷,从中提取用户 ID。
  • 从 Redis 中获取对应的用户 ID,对比两者是否一致,若不一致则返回 null

综上所述,UserSessionHelper 类结合 JWT 和 Redis 实现了用户会话的生成、管理和验证功能,既利用了 JWT 的无状态特性,又通过 Redis 解决了 JWT 无法主动失效的问题。

2.3 优缺点

  • 优点
    • 自包含性:令牌中包含用户信息,无需在服务端存储会话状态,便于在分布式系统中使用。
    • 跨域友好:可通过多种方式传输,如URL、HTTP Header或Cookie,适用于不同域之间的身份验证。
    • 安全性较高:通过签名机制保证令牌的完整性和真实性。
  • 缺点
    • 令牌体积较大:包含用户信息和签名等内容,可能增加传输数据量。
    • 无法主动失效:一旦生成,在有效期内无法主动使其失效,除非采用额外机制(如黑名单)。

3 总结

Session/Cookie和JWT身份验证各有优劣。Session/Cookie适用于传统单体应用,对会话管理要求较高且信任浏览器端Cookie的场景;JWT则更适合分布式系统、前后端分离架构以及对无状态性要求较高的场景。技术派在实际项目开发中,应根据具体需求、系统架构和安全要求,合理选择或结合使用这两种身份验证方式,以构建安全可靠的用户身份验证体系。

4 参考链接

  1. 技术派Session/Cookie身份验证
  2. 技术派JWT身份验证

相关文章:

  • Trae+DeepSeek学习Python开发MVC框架程序笔记(一):1个程序实现MVC
  • R/G-B/G色温坐标系下对横纵坐标取对数的优势
  • Volcano 实战快速入门 (一)
  • Long类型封装Json传输时精度丢失问题
  • 每日Html 4.24
  • 关于Qt对Html/CSS的支持
  • Java中正则表达式使用方法
  • docker 配置代理
  • js 的call 和apply方法用处
  • Python - 爬虫-网页解析数据-库lxml(支持XPath)
  • 真.从“零”搞 VSCode+STM32CubeMx+C <1>构建
  • 明远智睿2351开发板:重塑嵌入式开发性价比新标杆
  • NC149KMP算法详解
  • 如何彻底卸载Android Studio?
  • 深度解析 LangChain、ReAct、ReROO 架构及其在 AI Agent 中的应用
  • node.js 实战——(Http 知识点学习)
  • EasyRTC音视频实时通话在线教育解决方案:打造沉浸式互动教学新体验
  • Oracle 11g RAC手动打补丁详细步骤
  • 240424 leetcode exercises II
  • 算法训练营第二天| 209.长度最小的子数组、59.螺旋矩阵II、区间和
  • 云南富源回应“岔河水库死鱼”事件: 初步研判与水体缺氧有关
  • 瞭望:高校大门要向公众打开,不能让“一关了之”成为常态
  • 助力中国足球未来,香港赛马会鼎力支持U15国少选拔队赴英训练
  • 临清农商行回应监管处罚:系2023年问题,已经进行了整改
  • 俄“联盟MS-26”载人飞船安全返回地球
  • 北京理工大学解除宫某聘用关系,该教授此前被指骚扰猥亵学生