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

前端與Spring Boot后端無(wú)感Token 刷新的完整實(shí)例代碼

 更新時(shí)間:2025年06月26日 11:08:30   作者:Micro麥可樂  
本文詳細(xì)介紹無(wú)感Token刷新的核心原理,以及前端Axios攔截器與后端 Spring Boot + MyBatis-Plus + Redis 的完整示例代碼,通過(guò)雙 Token、Redis 校驗(yàn)與攔截重試,你可以在保證安全性的同時(shí),給用戶帶來(lái) 無(wú)感登錄過(guò)期刷新的體驗(yàn),感興趣的朋友跟隨小編一起看看吧

1. 前言

在我們前后端分離的應(yīng)用中,常用的身份認(rèn)證方案是基于 JWTJSON Web Token)。在保證安全性的同時(shí),短生命周期的 Access Token 又會(huì)帶來(lái)頻繁登錄的體驗(yàn)痛點(diǎn)。為了解決這個(gè)問題,我們引入 Refresh Token 并結(jié)合無(wú)感刷新機(jī)制,讓客戶端在 Access Token 過(guò)期時(shí)自動(dòng)刷新,而無(wú)需用戶手動(dòng)重新登錄,從而最大化提升用戶體驗(yàn)。

小伙伴們可以通過(guò)本文,快速掌握無(wú)感 Token 刷新的原理以及實(shí)現(xiàn)方式

2. 為什么要無(wú)感刷新

在基于Token的用戶認(rèn)證系統(tǒng)中,通常會(huì)設(shè)計(jì)兩種Token

Access Token:用于訪問資源,有效期短(通常15-30分鐘)
Refresh Token:用于獲取新Access Token,有效期長(zhǎng)(通常7天)

傳統(tǒng)Token機(jī)制存在兩大痛點(diǎn):

頻繁強(qiáng)制退出Access Token過(guò)期時(shí)用戶需重新登錄
安全隱患:延長(zhǎng)Access Token有效期會(huì)增加安全風(fēng)險(xiǎn)

無(wú)感刷新解決了這些問題:

用戶體驗(yàn)優(yōu)先
Access Token 常設(shè)很短(如 5–15 分鐘),若不自動(dòng)刷新,登錄態(tài)會(huì)頻繁過(guò)期,用戶被迫“重新登錄”,體驗(yàn)極差

安全與性能平衡
短生命周期的 Access Token 能減少被截獲濫用的風(fēng)險(xiǎn)
結(jié)合 Refresh Token(相對(duì)較長(zhǎng)有效期),可以在安全與便捷間找到最佳點(diǎn)

前后端解耦
通過(guò)前端攔截器統(tǒng)一處理過(guò)期場(chǎng)景,無(wú)須在各業(yè)務(wù)請(qǐng)求中散落重復(fù)邏輯
后端專注提供刷新接口與失效策略,無(wú)需關(guān)心前端實(shí)現(xiàn)細(xì)節(jié)

3 無(wú)感刷新原理

3.1 無(wú)感刷新流程

3.2 關(guān)鍵技術(shù)點(diǎn)

雙 Token 機(jī)制

Access Token:短時(shí)有效,攜帶用戶身份和權(quán)限
Refresh Token:長(zhǎng)期有效,專用于換取新的 Access Token

攔截與重試

1、前端在每次 API 請(qǐng)求中攜帶 Access Token
2、若響應(yīng)為 401 Unauthorized(或后端自定義過(guò)期碼),前端攔截器自動(dòng)調(diào)用刷新token接口,用 Refresh Token 獲取新一對(duì) Token;
3、獲取成功后,前端重新發(fā)起失敗的原始請(qǐng)求,用戶無(wú)感知。

后端安全策略
Refresh Token 寫入 Redis,并在刷新時(shí)做一次性或者滑動(dòng)過(guò)期(可選)校驗(yàn);
Refresh Token 刷新后失效,防止被盜用。

4、前端實(shí)現(xiàn)

下面以 Axios 為例演示攔截器邏輯。我們將 Tokens 保存在 localStorage 或者更安全的 [HttpOnly Cookie] 中(此處示例用 localStorage 方便演示)

// auth.js
import axios from 'axios';
// Base Axios 實(shí)例
const api = axios.create({
  baseURL: '/api',
});
// Token 存取
function getAccessToken() { return localStorage.getItem('access_token'); }
function getRefreshToken() { return localStorage.getItem('refresh_token'); }
function setTokens({ accessToken, refreshToken }) {
  localStorage.setItem('access_token', accessToken);
  localStorage.setItem('refresh_token', refreshToken);
}
// 請(qǐng)求攔截:自動(dòng)附帶 Access Token
api.interceptors.request.use(config => {
  const token = getAccessToken();
  if (token) config.headers['Authorization'] = `Bearer ${token}`;
  return config;
});
// 響應(yīng)攔截:遇到 401 刷新并重試
let isRefreshing = false;
let subscribers = [];
function onRefreshed(newToken) {
  subscribers.forEach(cb => cb(newToken));
  subscribers = [];
}
function addSubscriber(cb) {
  subscribers.push(cb);
}
api.interceptors.response.use(
  res => res,
  error => {
    const { config, response } = error;
    if (response && response.status === 401 && !config._retry) {
      if (isRefreshing) {
        // 正在刷新,加入隊(duì)列
        return new Promise(resolve => {
          addSubscriber(token => {
            config.headers['Authorization'] = `Bearer ${token}`;
            resolve(api(config));
          });
        });
      }
      config._retry = true;
      isRefreshing = true;
      // 調(diào)用刷新接口
      return api.post('/auth/refresh', { refreshToken: getRefreshToken() })
        .then(res => {
          const { accessToken, refreshToken } = res.data;
          setTokens({ accessToken, refreshToken });
          isRefreshing = false;
          onRefreshed(accessToken);
          // 重試原請(qǐng)求
          config.headers['Authorization'] = `Bearer ${accessToken}`;
          return api(config);
        })
        .catch(err => {
          // 刷新失敗,跳轉(zhuǎn)登錄
          isRefreshing = false;
          window.location.href = '/login';
          return Promise.reject(err);
        });
    }
    return Promise.reject(error);
  }
);
export default api;

要點(diǎn)說(shuō)明

isRefreshingsubscribers 用于解決多個(gè)并發(fā) 401 時(shí)只發(fā)送一次刷新請(qǐng)求;
_retry 標(biāo)記避免無(wú)限循環(huán);
刷新失敗后,需清除本地登錄態(tài)并跳轉(zhuǎn)到登錄頁(yè)。

5. 后端實(shí)現(xiàn)

5.1 基礎(chǔ)依賴(pom.xml)

<dependencies>
  <!-- Spring Boot Starter Web -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- MyBatis-Plus -->
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
  </dependency>
  <!-- MySQL 驅(qū)動(dòng) -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <!-- Redis -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <!-- 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>
</dependencies>

5.2 數(shù)據(jù)庫(kù)與實(shí)體(存儲(chǔ)用戶可選)

這里就簡(jiǎn)單模擬用戶,僅有用戶名和密碼為例

-- 用戶表(簡(jiǎn)化)
CREATE TABLE user_account (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL
);

5.3 Redis 存儲(chǔ) Refresh Token

我們用 ·Redis· 的 String,Key 為 refresh:{userId},Value 存 JSON { token, expireTime }

5.4 JWT 工具類

// JwtUtil.java
@Component
public class JwtUtil {
    @Value("${jwt.secret}") private String secret;
    @Value("${jwt.access.expire}") private long accessExpire;   // ms
    @Value("${jwt.refresh.expire}") private long refreshExpire; // ms
    // 生成 Access Token(短期)
    public String generateAccessToken(Long userId) {
        return Jwts.builder()
            .setSubject(userId.toString())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + accessExpire))
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
            .compact();
    }
    // 生成 Refresh Token(長(zhǎng)期)
    public String generateRefreshToken(Long userId) {
        return Jwts.builder()
            .setSubject(userId.toString())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + refreshExpire))
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
            .compact();
    }
    // 解析 Token
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(secret.getBytes())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}

5.5 刷新服務(wù)

// AuthService.java
@Service
public class AuthService {
    @Autowired private JwtUtil jwtUtil;
    @Autowired private StringRedisTemplate redis;
    public Tokens login(String username, String password) {
        // 1. 驗(yàn)證用戶名密碼(略,用 MyBatis-Plus 查詢)
        Long userId = /* ... */;
        // 2. 生成雙 Token
        String accessToken  = jwtUtil.generateAccessToken(userId);
        String refreshToken = jwtUtil.generateRefreshToken(userId);
        // 3. 保存到 Redis
        String key = "refresh:" + userId;
        redis.opsForValue().set(key, refreshToken, jwtUtil.getRefreshExpire(), TimeUnit.MILLISECONDS);
        return new Tokens(accessToken, refreshToken);
    }
    public Tokens refresh(String refreshToken) {
        // 1. 解析
        Claims claims = jwtUtil.parseToken(refreshToken);
        Long userId = Long.parseLong(claims.getSubject());
        // 2. Redis 校驗(yàn)
        String key = "refresh:" + userId;
        String cached = redis.opsForValue().get(key);
        if (cached == null || !cached.equals(refreshToken)) {
            throw new RuntimeException("Refresh Token 無(wú)效或已過(guò)期");
        }
        // 3. 生成新 Token
        String newAccess  = jwtUtil.generateAccessToken(userId);
        String newRefresh = jwtUtil.generateRefreshToken(userId);
        // 4. 覆蓋 Redis
        redis.opsForValue().set(key, newRefresh, jwtUtil.getRefreshExpire(), TimeUnit.MILLISECONDS);
        return new Tokens(newAccess, newRefresh);
    }
}

5.6 控制器Controller

// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired private AuthService authService;
    @PostMapping("/login")
    public Tokens login(@RequestBody LoginReq req) {
        return authService.login(req.getUsername(), req.getPassword());
    }
    @PostMapping("/refresh")
    public Tokens refresh(@RequestBody Map<String,String> body) {
        return authService.refresh(body.get("refreshToken"));
    }
}
// DTOs
@Data
class LoginReq { private String username, password; }
@Data
@AllArgsConstructor
class Tokens { 
    private String accessToken; 
    private String refreshToken; 
}

5.7 JWT 驗(yàn)證過(guò)濾器

由于驗(yàn)證并非本文的重點(diǎn),小伙伴們可以參考博主的 《Spring Security》專欄學(xué)習(xí),這里僅提供思路:
在每次請(qǐng)求攔截中,解析 Access Token 并將用戶信息放入 SecurityContext,若過(guò)期則交由前端刷新邏輯處理。

6. 結(jié)語(yǔ)

本文詳細(xì)介紹了 無(wú)感 Token 刷新 的核心原理,以及前端 Axios 攔截器與后端 Spring Boot + MyBatis-Plus + Redis 的完整示例代碼。通過(guò)雙 Token、Redis 校驗(yàn)與攔截重試,你可以在保證安全性的同時(shí),給用戶帶來(lái) 無(wú)感登錄過(guò)期刷新 的體驗(yàn)

后續(xù)可繼續(xù)優(yōu)化:

  • Refresh Token 滑動(dòng)過(guò)期:每次刷新延長(zhǎng)有效期;
  • Refresh Token 一次性使用:每個(gè)舊 Token 只能刷新一次;
  • 前端多 tab 協(xié)調(diào):同域下可共享刷新狀態(tài),避免重復(fù)刷新;
  • 安全加固:結(jié)合 IP、UA 風(fēng)控,防止 Token 被盜用。

到此這篇關(guān)于前端與Spring Boot后端無(wú)感Token 刷新的完整實(shí)例代碼的文章就介紹到這了,更多相關(guān)Spring Boot無(wú)感Token刷新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java中Map與List作用、定義、調(diào)用實(shí)戰(zhàn)詳解

    Java中Map與List作用、定義、調(diào)用實(shí)戰(zhàn)詳解

    在Java編程語(yǔ)言中,數(shù)據(jù)結(jié)構(gòu)之間轉(zhuǎn)換是常見的需求,尤其是集合框架中的List、Set和Map以及數(shù)組,這篇文章主要介紹了Java中Map與List作用、定義、調(diào)用的相關(guān)資料,需要的朋友可以參考下
    2025-12-12
  • SpringBoot實(shí)現(xiàn)任意文件在線預(yù)覽功能

    SpringBoot實(shí)現(xiàn)任意文件在線預(yù)覽功能

    在現(xiàn)代工作與生活環(huán)境中,文件共享與查看已經(jīng)成為日常不可或缺的一部分,不過(guò)傳統(tǒng)的文件處理方式要求用戶下載文件到本地后才能查看,不僅占用了設(shè)備的存儲(chǔ)空間,還可能導(dǎo)致文件傳播效率低下,所以本文給大家介紹了SpringBoot實(shí)現(xiàn)任意文件在線預(yù)覽功能,需要的朋友可以參考下
    2025-07-07
  • Java中使用增強(qiáng)for循環(huán)的實(shí)例方法

    Java中使用增強(qiáng)for循環(huán)的實(shí)例方法

    在本篇文章里小編給大家整理是的關(guān)于Java中如何使用增強(qiáng)for循環(huán)的實(shí)例內(nèi)容以及相關(guān)代碼,需要的朋友們可以學(xué)習(xí)下。
    2019-08-08
  • java中forward轉(zhuǎn)發(fā)的使用

    java中forward轉(zhuǎn)發(fā)的使用

    在Java中,forward轉(zhuǎn)發(fā)是一種非常常見且重要的操作,我們將深入探討forward的概念和用法,并給出一些代碼示例來(lái)幫助讀者更好地理解,感興趣的可以了解下
    2023-11-11
  • SpringBoot 動(dòng)態(tài)配置郵箱發(fā)件人過(guò)程解析

    SpringBoot 動(dòng)態(tài)配置郵箱發(fā)件人過(guò)程解析

    這篇文章主要介紹了SpringBoot 動(dòng)態(tài)配置郵箱發(fā)件人過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-08-08
  • 使用@Slf4j注解,log.info()無(wú)法使用問題

    使用@Slf4j注解,log.info()無(wú)法使用問題

    在使用Lombok的@Slf4j注解打印日志時(shí)遇到問題,通過(guò)降低Lombok版本(從1.18.x降至1.16.10)解決了問題
    2024-12-12
  • Spring?Aware接口示例代碼詳解

    Spring?Aware接口示例代碼詳解

    Spring的依賴注入的最大亮點(diǎn)是所有的Bean對(duì)Spring容器的存在是沒有意識(shí)的,我們可以將Spring容器換成其他的容器,Spring容器中的Bean的耦合度因此也是極低的,本文給大家介紹Spring?Aware接口示例代碼詳解,感興趣的朋友一起看看吧
    2022-02-02
  • java 算法之快速排序?qū)崿F(xiàn)代碼

    java 算法之快速排序?qū)崿F(xiàn)代碼

    這篇文章主要介紹了java 算法之快速排序?qū)崿F(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • idea如何忽略.iml文件和.idea目錄

    idea如何忽略.iml文件和.idea目錄

    這篇文章主要介紹了idea如何忽略.iml文件和.idea目錄問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • javaWeb使用servlet搭建服務(wù)器入門

    javaWeb使用servlet搭建服務(wù)器入門

    這篇文章主要為大家詳細(xì)介紹了javaWeb使用servlet搭建服務(wù)器入門,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-11-11

最新評(píng)論

新巴尔虎右旗| 鹤山市| 嘉峪关市| 达孜县| 安化县| 宜丰县| 泽普县| 乌拉特中旗| 徐闻县| 昌乐县| 五华县| 陆川县| 论坛| 中江县| 二连浩特市| 云霄县| 黄梅县| 兴义市| 杂多县| 昌宁县| 永安市| 东安县| 山东| 容城县| 五台县| 林州市| 清水河县| 大宁县| 通州市| 天水市| 定西市| 廊坊市| 兴海县| 新余市| 龙泉市| 边坝县| 蒲江县| 威海市| 五原县| 黄冈市| 台南市|