使用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不存在或用户未登录");
    }
相关推荐
韩立学长4 小时前
【开题答辩实录分享】以《自助游网站的设计与实现》为例进行选题答辩实录分享
java·mysql·spring
ss2734 小时前
线程池:任务队列、工作线程与生命周期管理
java·后端
不像程序员的程序媛4 小时前
Spring的cacheEvict
java·后端·spring
SAP小崔说事儿4 小时前
在数据库中将字符串拆分成表单(SQL和HANA版本)
java·数据库·sql·sap·hana·字符串拆分·无锡sap
凌云若寒4 小时前
半导体代加工企业标签模板痛点的全景式解决方案
java
shoubepatien4 小时前
JAVA -- 11
java·后端·intellij-idea
利剑 -~5 小时前
jdk源码解析
java·开发语言
Predestination王瀞潞5 小时前
JDK安装及环境变量配置
java·linux·开发语言
谷哥的小弟5 小时前
Spring Framework源码解析——PropertiesLoaderUtils
java·后端·spring·框架·源码
JIngJaneIL5 小时前
基于java+ vue助农电商系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端