使用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不存在或用户未登录");}