使用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不存在或用户未登录");
    }
相关推荐
枫叶落雨22215 分钟前
ShardingSphere 介绍
java
花花鱼20 分钟前
Spring Security 与 Spring MVC
java·spring·mvc
言慢行善1 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星1 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟2 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z2 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可2 小时前
Java 中的实现类是什么
java·开发语言
He少年2 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新2 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏4942 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构