最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot 整合 JWT + Redis 實現(xiàn)登錄鑒權

 更新時間:2026年02月06日 10:02:00   作者:造輪子的豬  
本文主要介紹了SpringBoot 整合 JWT + Redis 實現(xiàn)登錄鑒權,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

一、方案說明

本方案采用 JWT + Redis 組合實現(xiàn)登錄鑒權,解決了純JWT無法主動失效、無法續(xù)期的痛點:

  1. JWT 生成令牌,承載用戶核心信息,客戶端請求攜帶令牌實現(xiàn)無狀態(tài)認證
  2. Redis 存儲有效令牌,做雙重校驗(JWT簽名有效性+Redis令牌存在性),支持令牌主動失效(如登出)
  3. 實現(xiàn)令牌自動續(xù)期:令牌剩余有效期不足1/3時,自動刷新Redis過期時間
  4. 登錄接口 做防 暴 力 破解:連續(xù)5次登錄失敗,賬戶鎖定15分鐘,保障賬戶安全
  5. 基于SpringMVC的HandlerInterceptor實現(xiàn)全局請求攔截,統(tǒng)一校驗令牌

二、核心依賴引入

<!-- SpringSecurity 加密/核心工具 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>5.7.3</version>
</dependency>

<!-- JJWT 核心依賴 - JWT令牌生成/解析 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

<!-- SpringBoot Redis 啟動器 - 存儲有效令牌/續(xù)期控制 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

三、全局請求攔截器 - JwtInterceptor

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Resource
    private JwtServiceImpl jwtService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 獲取請求頭中的令牌
        String authToken = request.getHeader("Authorization");
        
        // 2. 令牌為空,直接返回未授權
        if (org.springframework.util.StringUtils.isBlank(authToken)) {
            return writeErrorResponse(response, "Token不能為空");
        }

        // 3. 校驗令牌格式是否包含Bearer前綴
        if (!authToken.startsWith("Bearer ")) {
            return writeErrorResponse(response, "Token格式錯誤,必須以Bearer 開頭");
        }

        // 4. 截取純token字符串
        String token = authToken.substring("Bearer".length() + 1).trim();

        try {
            // 5. 第一步校驗:Redis中是否存在該令牌,不存在=過期/已注銷/非法令牌
            if (!jwtService.redisHasToken(token)) {
                return writeErrorResponse(response, "Token無效或已過期");
            }

            // 6. 第二步校驗:解析JWT令牌,獲取載荷信息
            io.jsonwebtoken.Claims claims = jwtService.extractAllClaims(token);
            String userId = claims.getSubject();

            // 7. 第三步校驗:令牌簽名+用戶信息有效性校驗
            boolean isTokenValid = jwtService.validAuthToken(token, userId);
            if (!isTokenValid) {
                return writeErrorResponse(response, "Token無效或已過期");
            }

            // 8. 令牌有效,判斷是否需要自動續(xù)期(剩余時間不足1/3則續(xù)期)
            if (jwtService.isAuthTokenExpiringSoon(token)) {
                jwtService.expireToken(token);
            }

            // 9. 將用戶ID存入ThreadLocal,供后續(xù)業(yè)務邏輯獲取,無需重復解析
            UserContext.set(Integer.parseInt(userId));
        } catch (Exception e) {
            // 捕獲所有令牌異常:解析失敗、簽名篡改、數據異常等
            return writeErrorResponse(response, "Token無效或已過期");
        }
        // 全部校驗通過,放行請求
        return true;
    }

    /**
     * 抽離公共的錯誤響應方法,統(tǒng)一返回JSON格式錯誤信息
     */
    private boolean writeErrorResponse(HttpServletResponse response, String msg) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        String errorJson = "{\"code\": 401, \"message\": \"" + msg + "\"}";
        response.getWriter().print(errorJson);
        return false;
    }
}

四、JWT核心業(yè)務實現(xiàn)類 - JwtServiceImpl

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Base64;

@Service
public class JwtServiceImpl {

    @Resource
    private RedisService redisService;

    // ======================== 常量配置區(qū) ========================
    /** redis中令牌的前綴 */
    private static final String TOKEN_AUTH_PREFIX = "token:auth:";
    /** JWT簽名密鑰【生產環(huán)境請改為32位以上隨機字符串,Base64編碼后的值】 */
    private static final String SECRET_KEY = Base64.getEncoder().encodeToString("springboot-jwt-redis-auth-2026-key".getBytes());
    /** JWT令牌默認有效期 單位:分鐘 */
    private static final Integer DEFAULT_AUTH_EXPIRE_MINUTE = 30;
    /** JWT令牌有效期配置 單位:分鐘 */
    private static final Integer JWT_TOKEN_EXPIRE_MINUTE = 30;

    // ======================== 私有工具方法 ========================
    /**
     * 獲取JWT簽名密鑰對象,基于HS256算法
     */
    private Key getSigningKey() {
        byte[] keyBytes = Base64.getDecoder().decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 獲取Redis中令牌的剩余過期時間,單位:秒
     */
    private long getTokenRedisExpire(String token) {
        return redisService.getExpire(TOKEN_AUTH_PREFIX + token);
    }

    /**
     * 獲取令牌續(xù)期閾值:剩余有效期不足總時長的1/3時,觸發(fā)自動續(xù)期
     */
    private long getAuthRefreshSeconds() {
        return getAuthExpireSeconds() / 3;
    }

    // ======================== 公有對外方法 ========================
    /**
     * 計算令牌有效期,轉成秒數返回
     */
    public int getAuthExpireSeconds() {
        Integer expireMinutes = Optional.ofNullable(JWT_TOKEN_EXPIRE_MINUTE).orElse(DEFAULT_AUTH_EXPIRE_MINUTE);
        return expireMinutes * 60;
    }

    /**
     * 構建JWT令牌,自定義載荷信息
     * @param identifier 主題,存儲用戶ID等唯一標識
     * @param claimsMap  自定義載荷,可存儲用戶角色、權限等信息
     * @param secondsValidity 令牌有效期,單位:秒
     * @return 生成的JWT令牌字符串
     */
    public String generateTokenWithClaims(String identifier, Map<String, Object> claimsMap, int secondsValidity) {
        return Jwts.builder()
                .setClaims(claimsMap)
                .setSubject(identifier)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + secondsValidity * 1000))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 解析token,獲取自定義載荷中的purpose字段
     */
    public String extractPurpose(String token) {
        try {
            return extractAllClaims(token).get("purpose", String.class);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 解析token,獲取全部載荷信息
     * @throws ExpiredJwtException token過期
     * @throws SignatureException 簽名錯誤/令牌篡改
     * @throws MalformedJwtException 令牌格式錯誤
     */
    public Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 生成登錄鑒權專用令牌
     * @param userId 用戶ID
     * @return JWT令牌
     */
    public String generateAuthToken(Integer userId) {
        // 設置令牌用途和業(yè)務類型,便于后續(xù)擴展多場景令牌
        Map<String, Object> claimsMap = generateClaims("auth", "accessControl");
        // 生成JWT令牌
        String token = generateTokenWithClaims(String.valueOf(userId), claimsMap, getAuthExpireSeconds());
        // 令牌存入Redis,Redis過期時間與JWT一致,雙重保障
        redisService.set(TOKEN_AUTH_PREFIX + token, String.valueOf(userId), getAuthExpireSeconds());
        return token;
    }

    /**
     * 校驗令牌有效性:Redis存在性+用戶ID一致性校驗
     */
    public boolean validAuthToken(String token, String userId) {
        String storedUserId = getAuthData(token);
        return StringUtils.isNotBlank(storedUserId) && userId.equals(storedUserId);
    }

    /**
     * 判斷令牌是否即將過期,是否需要續(xù)期
     */
    public boolean isAuthTokenExpiringSoon(String token) {
        long remainExpireSeconds = getTokenRedisExpire(token);
        long refreshThreshold = getAuthRefreshSeconds();
        return remainExpireSeconds > 0 && remainExpireSeconds < refreshThreshold;
    }

    /**
     * 令牌續(xù)期:刷新Redis中令牌的過期時間,實現(xiàn)無感續(xù)期
     */
    public void expireToken(String token){
        redisService.expireKey(TOKEN_AUTH_PREFIX + token, getAuthExpireSeconds());
    }

    /**
     * 獲取Redis中存儲的令牌綁定的用戶ID
     */
    public String getAuthData(String token){
        return redisService.get(TOKEN_AUTH_PREFIX + token);
    }

    /**
     * 構建JWT自定義載荷信息
     */
    public Map<String, Object> generateClaims(String purpose, String busType) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("purpose", purpose);
        claims.put("busType", busType);
        return claims;
    }

    /**
     * 判斷Redis中是否存在該令牌
     */
    public boolean redisHasToken(String token){
        return redisService.hasKey(TOKEN_AUTH_PREFIX + token);
    }
}

五、登錄業(yè)務實現(xiàn)

import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class LoginServiceImpl {

    @Resource
    private UserService userService;
    @Resource
    private JwtServiceImpl jwtService;

    /** 登錄失敗鎖定閾值:連續(xù)失敗5次 */
    private static final Integer FAILED_LOCK_COUNT = 5;
    /** 賬戶鎖定時長:15分鐘,單位毫秒 */
    private static final Long LOCK_TIME = 15 * 60 * 1000L;

    /**
     * 用戶登錄核心方法
     * @param userDto 登錄入參(用戶名+密碼)
     * @return 登錄成功返回JWT令牌,失敗拋出業(yè)務異常
     */
    public String login(UserDto userDto){
        // 1. 根據用戶名查詢用戶,用戶不存在直接返回失敗
        User user = userService.getByUsername(userDto.getUsername());
        if (user == null) {
            throw new BusinessException("用戶名或密碼錯誤");
        }

        boolean needResetStatus = false;
        Integer failedAttempts = user.getFailedAttempts();
        Short userStatus = user.getStatus();

        // 2. 判斷賬戶是否被鎖定(連續(xù)5次失?。?
        if (FAILED_LOCK_COUNT.equals(failedAttempts)) {
            long lastFailTime = user.getUpdatedDate().getTime();
            // 判斷是否超過鎖定時間
            if (isTimeExceeded(lastFailTime, LOCK_TIME)) {
                // 鎖定時間已過,重置失敗次數和狀態(tài)
                needResetStatus = true;
            } else {
                // 賬戶仍在鎖定中
                throw new BusinessException("連續(xù)5次登錄失敗,賬戶已鎖定15分鐘,請稍后再試");
            }
        }

        // 3. 判斷用戶狀態(tài)是否正常
        if (!UserEnum.NORMAL.getStatus().equals(userStatus) && !needResetStatus) {
            throw new BusinessException("賬戶狀態(tài)異常,無法登錄");
        }

        // 4. 校驗密碼是否正確
        boolean passwordValid = userService.verifyPassword(userDto.getPassword(), user.getPassword());
        if (passwordValid) {
            // 密碼正確:重置失敗次數+解鎖賬戶
            if (needResetStatus) {
                userService.updateUserStatus(user.getId());
            }
            // 生成并返回令牌
            return jwtService.generateAuthToken(user.getId());
        } else {
            // 密碼錯誤:失敗次數+1,達到閾值則鎖定
            userService.incrFailedAttempts(userDto.getUsername());
            throw new BusinessException("用戶名或密碼錯誤");
        }
    }

    /**
     * 判斷是否超過指定時長
     * @param startTime 開始時間戳
     * @param timeLimit 時長限制(毫秒)
     */
    private boolean isTimeExceeded(long startTime, long timeLimit) {
        return System.currentTimeMillis() - startTime > timeLimit;
    }
}

六、攔截器和ThreadLocal

6.1 攔截器注冊配置類

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                // 攔截所有請求
                .addPathPatterns("/**")
                // 放行登錄接口、靜態(tài)資源等無需鑒權的接口
                .excludePathPatterns("/user/login", "/error");
    }
}

6.2 ThreadLocal用戶上下文 - UserContext

public class UserContext {
    private static final ThreadLocal<Integer> USER_ID_CONTEXT = new ThreadLocal<>();

    /** 設置當前線程的用戶ID */
    public static void set(Integer userId) {
        USER_ID_CONTEXT.set(userId);
    }

    /** 獲取當前線程的用戶ID */
    public static Integer get() {
        return USER_ID_CONTEXT.get();
    }

    /** 清除當前線程的用戶ID,防止內存泄漏 */
    public static void remove() {
        USER_ID_CONTEXT.remove();
    }
}

七、核心亮點

  1. 高安全性:JWT簽名防篡改 + Redis雙重校驗 + 賬戶防暴力破解鎖定,三重保障
  2. 高可用性:令牌自動無感續(xù)期,用戶無需重復登錄,體驗友好
  3. 高健壯性:完善的異常處理,避免token格式錯誤、篡改、過期導致的程序崩潰
  4. 高可維護性:代碼結構清晰,常量統(tǒng)一管理,注釋完整,邏輯精簡

生產環(huán)境必改配置

  1. SECRET_KEY:必須改為32位以上的隨機字符串,Base64編碼后使用,防止密鑰被破解
  2. 令牌有效期:可根據業(yè)務調整,建議后臺管理系統(tǒng)30分鐘,移動端2小時
  3. Redis部署:生產環(huán)境建議使用Redis集群,防止單點故障
  4. 密碼加密:必須使用BCryptPasswordEncoder加密存儲,禁止明文存儲

核心設計思想

為什么用JWT+Redis,而不是純JWT/純Redis?

  • 純JWT:令牌一旦生成無法主動失效,過期時間固定,續(xù)期困難
  • 純Redis:需要存儲大量用戶信息,Redis壓力大,且無狀態(tài)認證優(yōu)勢喪失
  • JWT+Redis:揚長避短,JWT做無狀態(tài)令牌,Redis做有效令牌存儲+續(xù)期,完美解決痛點

到此這篇關于SpringBoot 整合 JWT + Redis 實現(xiàn)登錄鑒權的文章就介紹到這了,更多相關SpringBoot JWT Redis登錄鑒權內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Java基于ProcessBuilder+FFmpeg實現(xiàn)MP4轉WAV音頻轉碼方案

    Java基于ProcessBuilder+FFmpeg實現(xiàn)MP4轉WAV音頻轉碼方案

    在音視頻處理場景中,將MP4視頻文件中的音頻流提取為WAV格式是一個常見需求,本文介紹一種基于Java ProcessBuilder + FFmpeg的高效解決方案,通過調用命令行工具FFmpeg完成轉換,實現(xiàn)跨平臺、高質量的音頻提取,需要的朋友可以參考下
    2026-03-03
  • Java使用Hutool+自定義注解實現(xiàn)數據脫敏

    Java使用Hutool+自定義注解實現(xiàn)數據脫敏

    我們在使用手機銀行的時候經常能看到APP上會將銀行卡的卡號中間部分給隱藏掉使用 ***** 來代替,在某些網站上查看一些業(yè)務密碼時(例如簽到密碼等)也會使用 ***** 來隱藏掉真正的密碼,那么這種方式是如何實現(xiàn)的呢,本文將給大家介紹使用Hutool+自定義注解實現(xiàn)數據脫敏
    2023-09-09
  • mybatis-plus之自動映射字段(typeHandler)的注意點及說明

    mybatis-plus之自動映射字段(typeHandler)的注意點及說明

    這篇文章主要介紹了mybatis-plus之自動映射字段(typeHandler)的注意點及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • java多線程實現(xiàn)文件下載

    java多線程實現(xiàn)文件下載

    這篇文章主要為大家詳細介紹了java多線程實現(xiàn)文件下載,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • Spring4如何自定義@Value功能詳解

    Spring4如何自定義@Value功能詳解

    這篇文章主要給大家介紹了關于Spring4如何自定義@Value功能的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用spring4具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-09-09
  • java的Builder原理和實現(xiàn)詳解

    java的Builder原理和實現(xiàn)詳解

    大家好,本篇文章主要講的是java的Builder原理和實現(xiàn)詳解,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽
    2021-12-12
  • Java正則表達式如何匹配特定html標簽內的內容

    Java正則表達式如何匹配特定html標簽內的內容

    這篇文章主要給大家介紹了關于Java正則表達式如何匹配特定html標簽內的內容的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-09-09
  • Java樹形結構數據生成導出excel文件方法記錄

    Java樹形結構數據生成導出excel文件方法記錄

    最近好像得罪了poi,遇到的都是導出word、Excel、pdf的問題,下面這篇文章主要給大家介紹了關于Java樹形結構數據生成導出excel文件的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下
    2021-10-10
  • 學習不同 Java.net 語言中類似的函數結構

    學習不同 Java.net 語言中類似的函數結構

    這篇文章主要介紹了學習不同 Java.net 語言中類似的函數結構,函數式編程語言包含多個系列的常見函數。但開發(fā)人員有時很難在語言之間進行切換,因為熟悉的函數具有不熟悉的名稱。函數式語言傾向于基于函數范例來命名這些常見函數。,需要的朋友可以參考下
    2019-06-06
  • windows 32位eclipse遠程hadoop開發(fā)環(huán)境搭建

    windows 32位eclipse遠程hadoop開發(fā)環(huán)境搭建

    這篇文章主要介紹了windows 32位eclipse遠程hadoop開發(fā)環(huán)境搭建的相關資料,需要的朋友可以參考下
    2016-07-07

最新評論

汝城县| 延津县| 乌苏市| 伊宁市| 金门县| 镇江市| 郎溪县| 瑞金市| 乳山市| 张家口市| 惠安县| 虹口区| 永吉县| 报价| 定西市| 东安县| 寿阳县| 石屏县| 广州市| 松原市| 石阡县| 闸北区| 康定县| 山东| 濮阳县| 卢湾区| 海安县| 九龙坡区| 梅河口市| 蓬安县| 保德县| 天门市| 师宗县| 道真| 赞皇县| 新巴尔虎左旗| 莱芜市| 嘉黎县| 称多县| 仙居县| 孙吴县|