亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

如何使用jwt+redis實(shí)現(xiàn)單點(diǎn)登錄

 更新時(shí)間:2025年08月22日 14:54:50   作者:俊紅de讀研生活  
文章介紹基于Redis的單點(diǎn)登錄實(shí)現(xiàn),通過JWT攔截器管理token生成、驗(yàn)證、更新及注銷,利用cookie與Redis同步確保異地登錄安全,防止token被偽造使用,感興趣的朋友跟隨小編一起看看吧

首先理一下登錄流程
前端登錄—>賬號(hào)密碼驗(yàn)證—>成功返回token—>后續(xù)請(qǐng)求攜帶token---->用戶異地登錄---->本地用戶token不能用,不能再訪問需要攜帶token的網(wǎng)頁

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 {
    // 默認(rèn)過期時(shí)間 1 小時(shí)
    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)         // 簽發(fā)時(shí)間
                .withExpiresAt(expireDate) // 過期時(shí)間
                .withPayload(claims)       // 自定義載荷
                .sign(Algorithm.HMAC256(SECRET)); // 簽名算法
    }
    /**
     * 驗(yàn)證 token 是否有效
     *
     * @param token 待驗(yàn)證的 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 中的某個(gè) claim
     *
     * @param token JWT token
     * @param key   claim 的 key
     * @return claim 對(duì)應(yīng)的值
     */
    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 的過期時(shí)間
     *
     * @param token JWT token
     * @return 過期時(shí)間
     */
    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("獲取當(dāng)前用戶信息出錯(cuò)");
        }
        return username;
    }
}

JWT攔截器

每次更新token的過期時(shí)間

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,實(shí)現(xiàn)單點(diǎn)登錄
        String redisToken = stringRedisTemplate.opsForValue().get(USER_LOGIN_KEY + username);
        if (redisToken == null || !redisToken.equals(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            throw new ClientException("您已經(jīng)在其他地方登錄,請(qǐng)重新登錄");
        }
        // 可選:刷新 Redis token 過期時(shí)間
        String redisKey = USER_LOGIN_KEY + username;
        stringRedisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
        // 將用戶名放入請(qǐng)求上下文,供 Controller 使用
        request.setAttribute("username", username);
            return true;
    }
}

注冊(cè)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("/**")       // 攔截所有請(qǐng)求
                .excludePathPatterns(
                        "/api/short-link/admin/v1/user/login"           // 登錄接口不攔截
                );
    }
}

登錄方法

首先判斷賬號(hào)密碼,正確以后,判斷redis是否有這個(gè)用戶,如果有,說明已經(jīng)登錄過了,把原來的token刪除了。
接下來統(tǒng)一生成新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,實(shí)現(xiàn)單點(diǎn)登錄
        String existingToken = stringRedisTemplate.opsForValue().get(redisKey);
        if (existingToken != null) {
            stringRedisTemplate.delete(redisKey);
        }
//        自定義載荷,如何還需要添加別的信息,可以繼續(xù)添加,如用戶ID
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", requestParam.getUsername());
        // 生成新 token
        String token = JwtUtil.generateToken(claims);
        // 存入 Redis,實(shí)現(xiàn)單點(diǎn)登錄
        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不存在或用戶未登錄");
    }

補(bǔ)充:基于Redis的單點(diǎn)登錄實(shí)現(xiàn)方案

一.單點(diǎn)登錄流程分析

1.客戶端統(tǒng)一攔截過濾器

流程分析:

1.在對(duì)需要進(jìn)行授權(quán)登錄的url連接,訪問統(tǒng)一被上放的過濾器流程所攔截
2.進(jìn)入過濾器,首先判斷當(dāng)前域名下是否有cookie,如果存在,則判斷該cookie的值作為key在redis中是否存在,如果存在則進(jìn)入進(jìn)行,不存在則進(jìn)入服務(wù)端的登錄授權(quán)界面.
3.如果當(dāng)前域名下的cookie值不存在,則判斷當(dāng)前鏈接是否帶有一個(gè)參數(shù)值為ticket的值,如果存在,則設(shè)置該值為cookie,然后重定向到本地址.

2.服務(wù)端流程

分析:

1.登錄接口:用戶名密碼驗(yàn)證成功后,設(shè)置cookie,設(shè)置redis緩存,登錄成功
2.登錄頁面:先判斷是否有cookie以及redis緩存,如果滿足,則重定向返回backurl以及附加參數(shù)ticket值為當(dāng)前的cookie,如果不滿足,則進(jìn)入登錄界面
3.退出登錄:刪除redis緩存,刪除cookie
4.為了防止別人拿著帶著ticket的鏈接去其他瀏覽器進(jìn)行偽造登錄,則可以對(duì)其ticket存入redis中,客戶端對(duì)其進(jìn)行一次性消費(fèi).

二.客戶端與服務(wù)端cookie同步

1.js腳本更新

可通過html加入js腳本訪問服務(wù)端,服務(wù)端再對(duì)其cookie重置時(shí)長(zhǎng)

2.增長(zhǎng)服務(wù)端cookie時(shí)長(zhǎng)

可通過增長(zhǎng)服務(wù)端cookie時(shí)長(zhǎng),如果客戶端退出了,redis緩存將清理,再次訪問服務(wù)端登錄接口,服務(wù)端查詢不到redis緩存,則對(duì)其cookie進(jìn)行刪除.但是這種不能算是完全意義上的同步.

三.關(guān)鍵代碼

1.客戶端過濾器filter

 String cookieName = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
        if(cookieName!=null&& !cookieName.equals("")){
            //驗(yàn)證cookie的有效性
            User user = tokenManagerInter.getUserInfo(cookieName);
            if(user!=null){
                //驗(yàn)證成功,繼續(xù)執(zhí)行
                //TODO 重置cookie時(shí)間,緩存時(shí)間
                filterChain.doFilter(servletRequest,servletResponse);
            }else {
                //驗(yàn)證失敗,刪除無效cookie
                CookieUtil.deleteCookie(Constans.COOKIE_SSO,response, "/");
                //返回登錄界面
                String qstr = makeQueryString(request); // 將所有請(qǐng)求參數(shù)重新拼接成queryString
                String backUrl=request.getRequestURL() + qstr; // 回調(diào)url
                String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
                response.sendRedirect(location);
            }
        }else {
            String vtParam = pasreVtParam(request); // 從請(qǐng)求中
            if (vtParam == null) {
                // 請(qǐng)求中中沒有vtParam,引導(dǎo)瀏覽器重定向到服務(wù)端執(zhí)行登錄校驗(yàn)
                //返回登錄界面
                String qstr = makeQueryString(request); // 將所有請(qǐng)求參數(shù)重新拼接成queryString
                String backUrl=request.getRequestURL() + qstr; // 回調(diào)url
                String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
                response.sendRedirect(location);
            } else if (vtParam.length() == 0) {
                // 有vtParam,但內(nèi)容為空,表示到服務(wù)端loginCheck后,得到的結(jié)果是未登錄
                response.sendError(403);
            } else {
                // 讓瀏覽器向本鏈接發(fā)起一次重定向,此過程去除vtParam,將vt寫入cookie
                redirectToSelf(vtParam);
            }
        }

2.服務(wù)端登錄界面

先判斷當(dāng)前是否有cookie,如果沒有,則進(jìn)入登錄界面,如果存在,則判斷是否有redis緩存存在,如果不存在,則進(jìn)入登錄界面,如果存在,則重定向到backurl中去,同時(shí)帶著ticket返回客戶端

 @Override
    public String login(String backUrl, ModelMap map) {
        String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
        //判斷有無cookie
        if(cookie==null){
            //無cookie,進(jìn)入登錄界面
            map.put("backUrl", backUrl);
            return "login";
        }else {
            //當(dāng)前存在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.服務(wù)端登錄接口

驗(yàn)證完用戶名密碼后設(shè)置cookie,增加redis緩存,重定向到backurl去.

 				//設(shè)置cookie
                String uuid = UUID.randomUUID().toString().replaceAll("-","");
                Cookie cookie = new Cookie(Constans.COOKIE_SSO, uuid);
                cookie.setMaxAge(Constans.COOKIE_EXPIRE_TIME);//設(shè)置cookie時(shí)間
                cookie.setPath("/");
                response.addCookie(cookie);
                //存入redis中
                System.out.println("創(chuàng)建uuid:"+uuid);
                accountUser.setUuid(uuid);
                result.setModel(accountUser);
                boolean creatToken = tokenManagerInter.createToken(accountUser);
                System.out.println("創(chuàng)建token:"+creatToken);
                if(!creatToken){
                    return resultUtil.setErrResult(ReturnCodeBase.ERR5002);
                }

4.退出登錄接口

退出登錄時(shí)候,檢查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";

到此這篇關(guān)于如何使用jwt+redis實(shí)現(xiàn)單點(diǎn)登錄的文章就介紹到這了,更多相關(guān)jwt redis單點(diǎn)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄

    Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄

    本文主要介紹了Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Redis源碼與設(shè)計(jì)剖析之網(wǎng)絡(luò)連接庫(kù)

    Redis源碼與設(shè)計(jì)剖析之網(wǎng)絡(luò)連接庫(kù)

    這篇文章主要為大家介紹了Redis源碼與設(shè)計(jì)剖析之網(wǎng)絡(luò)連接庫(kù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Redis哨兵模式實(shí)現(xiàn)一主二從三哨兵

    Redis哨兵模式實(shí)現(xiàn)一主二從三哨兵

    本文主要介紹了Redis哨兵模式實(shí)現(xiàn)一主二從三哨兵,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Redis?存儲(chǔ)對(duì)象信息用?Hash?和String的區(qū)別

    Redis?存儲(chǔ)對(duì)象信息用?Hash?和String的區(qū)別

    這篇文章主要介紹了Redis存儲(chǔ)對(duì)象信息用Hash和String的區(qū)別,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • 使用Redis實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制

    使用Redis實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制

    JWT是一種輕量級(jí)的身份驗(yàn)證和授權(quán)機(jī)制,它是一種 JSON 格式的數(shù)據(jù)串,通常用于客戶端和服務(wù)端之間的單點(diǎn)登錄(Single Sign-On, SSO)場(chǎng)景,本文給大家介紹了如何使用Redis來實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制,需要的朋友可以參考下
    2024-08-08
  • redis主從連接不成功錯(cuò)誤問題及解決

    redis主從連接不成功錯(cuò)誤問題及解決

    這篇文章主要介紹了redis主從連接不成功錯(cuò)誤問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教<BR>
    2024-01-01
  • redis學(xué)習(xí)之RDB、AOF與復(fù)制時(shí)對(duì)過期鍵的處理教程

    redis學(xué)習(xí)之RDB、AOF與復(fù)制時(shí)對(duì)過期鍵的處理教程

    這篇文章主要給大家介紹了關(guān)于redis學(xué)習(xí)之RDB、AOF與復(fù)制時(shí)對(duì)過期鍵處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • Redis?key的過期時(shí)間和永久有效的實(shí)現(xiàn)

    Redis?key的過期時(shí)間和永久有效的實(shí)現(xiàn)

    在Redis中,鍵可以設(shè)置過期時(shí)間或被永久保存,`EXPIRE`和`PEXPIRE`命令分別用于設(shè)置鍵的過期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-09-09
  • redis中事務(wù)機(jī)制及樂觀鎖的實(shí)現(xiàn)

    redis中事務(wù)機(jī)制及樂觀鎖的實(shí)現(xiàn)

    這篇文章主要介紹了redis中事務(wù)機(jī)制及樂觀鎖的相關(guān)內(nèi)容,通過事務(wù)的執(zhí)行分析Redis樂觀鎖,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-10-10
  • 如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)

    如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)

    測(cè)試過程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測(cè)試過程中經(jīng)常有人會(huì)清空redis,回歸測(cè)試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下
    2024-03-03

最新評(píng)論