【技术派后端篇】技术派中 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_KEY
的Cookie
,若找到该 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 基本原理
JWT(JSON 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)的用户身份验证和请求处理流程,具体如下:
- 登录请求
前端用户输入用户名和密码,发起登录请求,将用户名和密码作为参数发送给后端。这是流程的起始点,目的是让后端验证用户身份 。 - 生成并返回JWT
后端接收到登录请求后,对用户名和密码进行验证。如果验证通过,后端生成一个JWT ,并将其返回给前端。JWT包含了用户的身份信息等内容,用于后续请求的身份验证 。 - 前端发起请求并携带JWT
前端接收到JWT后,将其保存到本地(如localStorage
、sessionStorage
等 )。之后前端向后端发起其他业务请求时,会从本地获取JWT,并将其携带在请求头中(常见是放在Authorization
头中,格式为Bearer <JWT>
),发送给后端 。 - 后端校验JWT并响应
- 校验失败:后端接收到请求后,从请求头中获取JWT,并进行校验(包括签名验证、有效期检查等 )。如果校验失败,说明用户未登录或JWT无效,后端返回未登录提示,或重定向到登录页面,让用户重新登录 。
- 校验通过:如果JWT校验通过,表明用户身份合法,后端正常处理请求,并返回请求结果给前端 。
2.2.2 实现代码
技术派中JTW的核心逻辑代码位于com.github.paicoding.forum.service.user.service.help.UserSessionHelper
类。
UserSessionHelper 是一个用于处理用户会话管理的工具类,借助 JWT(JSON Web Token)来存储用户的会话信息,同时结合 Redis 实现会话的主动失效功能。下面详细阐述其代码逻辑:
- 类和依赖注入
@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 的合法性。
- 构造函数
public UserSessionHelper(JwtProperties jwtProperties) {this.jwtProperties = jwtProperties;algorithm = Algorithm.HMAC256(jwtProperties.getSecret());verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build(); }
- 构造函数接收
JwtProperties
对象,初始化签名算法和验证器。
- 生成会话方法
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 中,并设置过期时间。
- 移除会话方法
removeSession
public void removeSession(String session) {RedisClient.del(session); }
- 从 Redis 中删除指定的会话信息,实现会话的主动失效。
-
根据会话获取用户信息方法
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
。
综上所述,UserSessionHelpe
r 类结合 JWT 和 Redis 实现了用户会话的生成、管理和验证功能,既利用了 JWT 的无状态特性,又通过 Redis 解决了 JWT 无法主动失效的问题。
2.3 优缺点
- 优点:
- 自包含性:令牌中包含用户信息,无需在服务端存储会话状态,便于在分布式系统中使用。
- 跨域友好:可通过多种方式传输,如URL、HTTP Header或Cookie,适用于不同域之间的身份验证。
- 安全性较高:通过签名机制保证令牌的完整性和真实性。
- 缺点:
- 令牌体积较大:包含用户信息和签名等内容,可能增加传输数据量。
- 无法主动失效:一旦生成,在有效期内无法主动使其失效,除非采用额外机制(如黑名单)。
3 总结
Session/Cookie和JWT身份验证各有优劣。Session/Cookie适用于传统单体应用,对会话管理要求较高且信任浏览器端Cookie的场景;JWT则更适合分布式系统、前后端分离架构以及对无状态性要求较高的场景。技术派在实际项目开发中,应根据具体需求、系统架构和安全要求,合理选择或结合使用这两种身份验证方式,以构建安全可靠的用户身份验证体系。
4 参考链接
- 技术派Session/Cookie身份验证
- 技术派JWT身份验证