Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > jwt redis单点登录

如何使用jwt+redis实现单点登录

作者:俊红de读研生活

文章介绍基于Redis的单点登录实现,通过JWT拦截器管理token生成、验证、更新及注销,利用cookie与Redis同步确保异地登录安全,防止token被伪造使用,感兴趣的朋友跟随小编一起看看吧

首先理一下登录流程
前端登录—>账号密码验证—>成功返回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;
    @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拦截器,并选择放行哪些接口

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

@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中删除用户即可

 @Override
    public void logout(String username) {
        if (checkLogin(username)) {
            stringRedisTemplate.delete(USER_LOGIN_KEY + username);
            return;
        }
        throw new ClientException("用户Token不存在或用户未登录");
    }

补充:基于Redis的单点登录实现方案

一.单点登录流程分析

1.客户端统一拦截过滤器

流程分析:

1.在对需要进行授权登录的url连接,访问统一被上放的过滤器流程所拦截
2.进入过滤器,首先判断当前域名下是否有cookie,如果存在,则判断该cookie的值作为key在redis中是否存在,如果存在则进入进行,不存在则进入服务端的登录授权界面.
3.如果当前域名下的cookie值不存在,则判断当前链接是否带有一个参数值为ticket的值,如果存在,则设置该值为cookie,然后重定向到本地址.

2.服务端流程

分析:

1.登录接口:用户名密码验证成功后,设置cookie,设置redis缓存,登录成功
2.登录页面:先判断是否有cookie以及redis缓存,如果满足,则重定向返回backurl以及附加参数ticket值为当前的cookie,如果不满足,则进入登录界面
3.退出登录:删除redis缓存,删除cookie
4.为了防止别人拿着带着ticket的链接去其他浏览器进行伪造登录,则可以对其ticket存入redis中,客户端对其进行一次性消费.

二.客户端与服务端cookie同步

1.js脚本更新

可通过html加入js脚本访问服务端,服务端再对其cookie重置时长

2.增长服务端cookie时长

可通过增长服务端cookie时长,如果客户端退出了,redis缓存将清理,再次访问服务端登录接口,服务端查询不到redis缓存,则对其cookie进行删除.但是这种不能算是完全意义上的同步.

三.关键代码

1.客户端过滤器filter

 String cookieName = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
        if(cookieName!=null&& !cookieName.equals("")){
            //验证cookie的有效性
            User user = tokenManagerInter.getUserInfo(cookieName);
            if(user!=null){
                //验证成功,继续执行
                //TODO 重置cookie时间,缓存时间
                filterChain.doFilter(servletRequest,servletResponse);
            }else {
                //验证失败,删除无效cookie
                CookieUtil.deleteCookie(Constans.COOKIE_SSO,response, "/");
                //返回登录界面
                String qstr = makeQueryString(request); // 将所有请求参数重新拼接成queryString
                String backUrl=request.getRequestURL() + qstr; // 回调url
                String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
                response.sendRedirect(location);
            }
        }else {
            String vtParam = pasreVtParam(request); // 从请求中
            if (vtParam == null) {
                // 请求中中没有vtParam,引导浏览器重定向到服务端执行登录校验
                //返回登录界面
                String qstr = makeQueryString(request); // 将所有请求参数重新拼接成queryString
                String backUrl=request.getRequestURL() + qstr; // 回调url
                String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
                response.sendRedirect(location);
            } else if (vtParam.length() == 0) {
                // 有vtParam,但内容为空,表示到服务端loginCheck后,得到的结果是未登录
                response.sendError(403);
            } else {
                // 让浏览器向本链接发起一次重定向,此过程去除vtParam,将vt写入cookie
                redirectToSelf(vtParam);
            }
        }

2.服务端登录界面

先判断当前是否有cookie,如果没有,则进入登录界面,如果存在,则判断是否有redis缓存存在,如果不存在,则进入登录界面,如果存在,则重定向到backurl中去,同时带着ticket返回客户端

 @Override
    public String login(String backUrl, ModelMap map) {
        String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
        //判断有无cookie
        if(cookie==null){
            //无cookie,进入登录界面
            map.put("backUrl", backUrl);
            return "login";
        }else {
            //当前存在cookie
            if(tokenManagerInter.checkToken(cookie)){
                //缓存存在
                if (backUrl != null) {
                    try {
                        response.sendRedirect(StringUtils.appendUrlParameter(backUrl, Constans.PARAGRAM_VT, cookie));
                    } catch (IOException e) {
                        e.printStackTrace();
                        logger.error("登录重定向失败:"+e);
                    }
                    return null;
                } else {
                    AccountUser user = tokenManagerInter.getUserInfo(cookie);
                    if(user!=null){
                        map.put("user", user);
                    }
                    map.put("vt", cookie);
                    return "loginSuccess";
                }
            }else {
                //缓存不存在,删除cookie,返回登录界面
                CookieUtil.deleteCookie(Constans.COOKIE_SSO,response,"/");
                map.put("backUrl", backUrl);
                return "login";
            }
        }
    }

3.服务端登录接口

验证完用户名密码后设置cookie,增加redis缓存,重定向到backurl去.

 				//设置cookie
                String uuid = UUID.randomUUID().toString().replaceAll("-","");
                Cookie cookie = new Cookie(Constans.COOKIE_SSO, uuid);
                cookie.setMaxAge(Constans.COOKIE_EXPIRE_TIME);//设置cookie时间
                cookie.setPath("/");
                response.addCookie(cookie);
                //存入redis中
                System.out.println("创建uuid:"+uuid);
                accountUser.setUuid(uuid);
                result.setModel(accountUser);
                boolean creatToken = tokenManagerInter.createToken(accountUser);
                System.out.println("创建token:"+creatToken);
                if(!creatToken){
                    return resultUtil.setErrResult(ReturnCodeBase.ERR5002);
                }

4.退出登录接口

退出登录时候,检查cookie是否存在,然后删除redis缓存,删除cookie,重定向到登录界面

 		String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
        if (cookie != null && !cookie.equals("")) {
            tokenManagerInter.deleteToken(cookie);
            CookieUtil.deleteCookie(Constans.COOKIE_SSO, response, "/");
        }
        if (backUrl != null && !backUrl.equals("")) {
            try {
               return "redirect:"+Constans.URL.PREFIX+"login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
            } catch (UnsupportedEncodingException e) {
                logger.error("退出登录重定向失败:" + e);
            }
        }
        return "redirect:/login";

到此这篇关于如何使用jwt+redis实现单点登录的文章就介绍到这了,更多相关jwt redis单点登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文