使用jwt+redis实现单点登录

首先理一下登录流程

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

jwt工具类

java 复制代码
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的过期时间

java 复制代码
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;

    @Override
    public 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拦截器,并选择放行哪些接口

java 复制代码
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;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")       // 拦截所有请求
                .excludePathPatterns(
                        "/api/short-link/admin/v1/user/login"           // 登录接口不拦截
                );
    }
}

登录方法

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

接下来统一生成新token,存入redis

java 复制代码
@Override
    public 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);
        }
//        自定义载荷,如何还需要添加别的信息,可以继续添加,如用户ID
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", requestParam.getUsername());

        // 生成新 token
        String token = JwtUtil.generateToken(claims);
        // 存入 Redis,实现单点登录
        stringRedisTemplate.opsForValue().set(redisKey, token, 30, TimeUnit.MINUTES);

        return new UserLoginRespDTO(token);
    }

退出登录

在redis中删除用户即可

java 复制代码
 @Override
    public void logout(String username) {
        if (checkLogin(username)) {
            stringRedisTemplate.delete(USER_LOGIN_KEY + username);
            return;
        }
        throw new ClientException("用户Token不存在或用户未登录");
    }
相关推荐
JH30738 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
Coder_Boy_9 小时前
技术让开发更轻松的底层矛盾
java·大数据·数据库·人工智能·深度学习
invicinble10 小时前
对tomcat的提供的功能与底层拓扑结构与实现机制的理解
java·tomcat
较真的菜鸟10 小时前
使用ASM和agent监控属性变化
java
黎雁·泠崖10 小时前
【魔法森林冒险】5/14 Allen类(三):任务进度与状态管理
java·开发语言
qq_124987075311 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_11 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
Mr_sun.11 小时前
Day06——权限认证-项目集成
java
瑶山11 小时前
Spring Cloud微服务搭建四、集成RocketMQ消息队列
java·spring cloud·微服务·rocketmq·dashboard
abluckyboy11 小时前
Java 实现求 n 的 n^n 次方的最后一位数字
java·python·算法