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

使用jwt+redis实现单点登录

首先理一下登录流程
前端登录—>账号密码验证—>成功返回token—>后续请求携带token---->用户异地登录---->本地用户token不能用,不能再访问需要携带token的网页

jwt工具类

package com.nageoffer.shortlink.admin.util;import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.util.Date;
import java.util.Map;public class JwtUtil {// 默认过期时间 1 小时private static final long EXPIRE_TIME = 60 * 60 * 1000L;// 签名密钥private static final String SECRET = "short-link-secret-key";/*** 生成 token** @param claims 自定义的载荷* @return JWT token*/public static String generateToken(Map<String, Object> claims) {Date now = new Date();Date expireDate = new Date(now.getTime() + EXPIRE_TIME);return JWT.create().withIssuedAt(now)         // 签发时间.withExpiresAt(expireDate) // 过期时间.withPayload(claims)       // 自定义载荷.sign(Algorithm.HMAC256(SECRET)); // 签名算法}/*** 验证 token 是否有效** @param token 待验证的 JWT* @return 是否有效*/public static boolean verifyToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(SECRET);JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token);return true;} catch (JWTVerificationException e) {return false;}}/*** 获取 token 中的某个 claim** @param token JWT token* @param key   claim 的 key* @return claim 对应的值*/public static String getClaim(String token, String key) {try {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim(key).asString();} catch (JWTDecodeException e) {return null;}}/*** 获取 token 的过期时间** @param token JWT token* @return 过期时间*/public static Date getExpireAt(String token) {try {DecodedJWT jwt = JWT.decode(token);return jwt.getExpiresAt();} catch (JWTDecodeException e) {return null;}}public static String getCurrentUser() {String username = null;try {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader(UserConstant.TOKEN);if (ObjectUtil.isNotEmpty(token)) {username = JWT.decode(token).getClaim("username").asString();}} catch (Exception e) {throw new ClientException("获取当前用户信息出错");}return username;}
}

JWT拦截器

每次更新token的过期时间

package com.nageoffer.shortlink.admin.config;import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.concurrent.TimeUnit;import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.USER_LOGIN_KEY;/*** jwt拦截器*/
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {private final StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader(UserConstant.TOKEN);if (token == null || !JwtUtil.verifyToken(token)) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);throw new ClientException("token无效或已过期");}// 从 token 获取用户名String username = JwtUtil.getClaim(token,"username");// 可选:检查 Redis 是否存在 token,实现单点登录String redisToken = stringRedisTemplate.opsForValue().get(USER_LOGIN_KEY + username);if (redisToken == null || !redisToken.equals(token)) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);throw new ClientException("您已经在其他地方登录,请重新登录");}// 可选:刷新 Redis token 过期时间String redisKey = USER_LOGIN_KEY + username;stringRedisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);// 将用户名放入请求上下文,供 Controller 使用request.setAttribute("username", username);return true;}}

注册JWT拦截器,并选择放行哪些接口

package com.nageoffer.shortlink.admin.config;import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {private final JwtInterceptor jwtInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInterceptor).addPathPatterns("/**")       // 拦截所有请求.excludePathPatterns("/api/short-link/admin/v1/user/login"           // 登录接口不拦截);}
}

登录方法

首先判断账号密码,正确以后,判断redis是否有这个用户,如果有,说明已经登录过了,把原来的token删除了。
接下来统一生成新token,存入redis

@Overridepublic UserLoginRespDTO login(UserLoginReqDTO requestParam) {LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class).eq(UserDO::getUsername, requestParam.getUsername()).eq(UserDO::getPassword, requestParam.getPassword()).eq(UserDO::getDelFlag, 0);UserDO userDO = baseMapper.selectOne(queryWrapper);if (userDO == null) {throw new ClientException("用户不存在");}String redisKey = USER_LOGIN_KEY + requestParam.getUsername();// 检查 Redis 是否已存在 token,实现单点登录String existingToken = stringRedisTemplate.opsForValue().get(redisKey);if (existingToken != null) {stringRedisTemplate.delete(redisKey);}
//        自定义载荷,如何还需要添加别的信息,可以继续添加,如用户IDMap<String, Object> claims = new HashMap<>();claims.put("username", requestParam.getUsername());// 生成新 tokenString token = JwtUtil.generateToken(claims);// 存入 Redis,实现单点登录stringRedisTemplate.opsForValue().set(redisKey, token, 30, TimeUnit.MINUTES);return new UserLoginRespDTO(token);}

退出登录

在redis中删除用户即可

 @Overridepublic void logout(String username) {if (checkLogin(username)) {stringRedisTemplate.delete(USER_LOGIN_KEY + username);return;}throw new ClientException("用户Token不存在或用户未登录");}
http://www.xdnf.cn/news/18393.html

相关文章:

  • LeetCode 回文链表
  • 力扣1005:k次取反后最大化的数组和
  • Elasticsearch官方文档学习-未完待续
  • 三层交换机
  • Bartender 5 多功能菜单栏管理(Mac电脑)
  • 【学习嵌入式day-29-网络】
  • 深入解析C++非类型模板参数
  • 网络打印机自动化部署脚本
  • 软考 系统架构设计师系列知识点之杂项集萃(130)
  • 记录前端菜鸟的日常——小程序内嵌H5页面自定义分享按钮
  • 深入解析HashMap的存储机制:扰动函数、哈希计算与索引定位
  • 信息收集4----(收集网站指纹信息)
  • 20250821 圆方树总结
  • 一、部署LNMP
  • 实现自己的AI视频监控系统-第一章-视频拉流与解码3
  • mac的m3芯使用git
  • 18维度解密·架构魔方:一览无遗的平衡艺术
  • LT8712SX,Type-C/DP1.4 /eDP转 DP1.4/HD-DVI2.0 带音频
  • AXI GPIO S——ZYNQ学习笔记10
  • Java项目:基于SpringBoot和VUE的在线拍卖系统(源码+数据库+文档)
  • K 均值聚类(K-Means)演示,通过生成笑脸和爱心两种形状的模拟数据,展示了无监督学习中聚类算法的效果。以下是详细讲解:
  • 【typenum】 19 类型相同检查(type_operators.rs片段)
  • JavaWeb前端03(Ajax概念及在前端开发时应用)
  • SD 节点学习
  • ZStack Zaku替代VMware Tanzu:六项对比、构建虚拟机+容器一体化架构
  • HTTP 403 错误:后端权限校验机制深度解析
  • Matplotlib数据可视化实战:Matplotlib高级使用技巧与性能优化
  • 用OpencvSharp编写视频录制工具
  • Matplotlib数据可视化实战:Matplotlib数据可视化入门与实践
  • 【Android】悬浮窗清理