慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

小众软件
小众软件
博客园 - 叶小钗
有赞技术团队
有赞技术团队
大猫的无限游戏
大猫的无限游戏
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
Blog — PlanetScale
Blog — PlanetScale
爱范儿
爱范儿
T
Tailwind CSS Blog
Jina AI
Jina AI
量子位
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
J
Java Code Geeks
V
Visual Studio Blog
月光博客
月光博客

博客园 - 锐洋智能

Eclipse IDE for Enterprise Find/Replace 窗口可以"停驻" 安装 SDelete 方式 SDelete 的核心作用,不是 “删文件”,而是 “把你已经删掉的文件,彻底从磁盘上抹干净”,同时帮你把虚拟机里的 “空闲空间” 变成连续的、可被回收的状态。 windows 10 启动就运行了一个批处理文件 在什么地方修改?启动项中? 让 Spring Framework7.0.7 支持 velocity Java 9+ 开启了模块化安全限制,不允许 Ignite 直接访问底层内存地址,导致 Ignite 启动失败 接口鉴权:Session/Cookie 与 JWT 的核心区别 下是针对 RedisSessionManager 的 Tomcat context.xml 配置示例,覆盖基础单机 Redis、带密码 / 指定库、Redis 哨兵集群、自定义序列化 / 持久化策略 等常见场景 Redis-8.6.3-Windows-x64-cygwin 与 Redis-8.6.3-Windows-x64-msys2 有什么不一样? commons-fileupload2 M4 升级 M5 报错解决方案 阿里云的网络安全策略 ip 地址详细说明 Paralithic、 QLExpress、AviatorScript、exp4j 性能对比一下 java 序列化影响(重要!) paralithic 与 Aviator 表达式那一个更快 券商接受委托的完整时间线 jQuery 4.0 移除了许多已废弃的方法和特性 xheditor插件无限递归错误解决方法 jedis-7.1.0.jar 升级至 jedis-7.2.0.jar 就提示:The type JedisPoolConfig is deprecated The type JedisPooled is deprecated Spring 从 5.x 到 6.x 和 7.x 区别 js 原生 剪切板 复制功能 安装 Microsoft Visual C++ 运行时 Java与Python进程通信优化方案 Autocomplete | jQuery UI 同一面页定义不同的 .ui-autocomplete Quartz的misfire处理策略设置不当导致Job在应用启动时立即执行。 mysql-connector-j-9.5.0.jar BUG 推荐几款好用的CSS在线压缩工具 CSS 变量 定义及引用 TLS virtual host [_default_], certificate type [RSA] configured from keystore [d:/reyo/jks/localhost.jks] using alias [tomcat] with trust store [null] okhttp5.x 需要在 请求级别 控制是否使用重试拦截器,而不是在客户端级别 Apache Commons FileUpload 1.x 与 2.x 的主要区别
Spring 5.x + 老項目的 JWT 攔截器 + 自動續期
锐洋智能 · 2026-05-27 · via 博客园 - 锐洋智能

最終完整代碼:JWT 攔截器 + 自動續期

1. 先給你 續期工具方法(加到你的 JwtUtil 裡)

java

運行

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {

    private static final String SECRET = "12345678901234567890123456789012";
    private static final Key KEY = Keys.hmacShaKeyFor(SECRET.getBytes());
    private static final long EXPIRATION = 7200000; // 2小時
    private static final long REFRESH_BEFORE = 10 * 60 * 1000; // 剩餘<10分鐘就續期

    // 創建Token
    public static String createToken(String userId, String username) {
        Map<String, Object> map = new HashMap<>();
        map.put("userId", userId);
        map.put("username", username);
        return Jwts.builder()
                .setClaims(map)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(KEY)
                .compact();
    }

    // 解析
    public static Claims getClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    // ====================== 【自動續期核心方法】 ======================
    // 判斷是否需要刷新
    public static boolean needRefresh(String token) {
        try {
            Claims claims = getClaims(token);
            long expire = claims.getExpiration().getTime();
            long now = System.currentTimeMillis();
            return (expire - now) < REFRESH_BEFORE;
        } catch (Exception e) {
            return false;
        }
    }

    // 刷新Token(用舊信息生成新Token)
    public static String refreshToken(String token) {
        Claims claims = getClaims(token);
        String userId = (String) claims.get("userId");
        String username = claims.getSubject();
        return createToken(userId, username);
    }

    // 獲取用戶ID
    public static String getUserId(String token) {
        return (String) getClaims(token).get("userId");
    }
}

2. Spring MVC 攔截器(自動續期 + 自動返回新 Token)

java

運行

import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class JwtRefreshInterceptor implements HandlerInterceptor {

    // 前端傳過來的 header 名稱
    private static final String TOKEN_HEADER = "Authorization";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(TOKEN_HEADER);

        // 沒有token → 放行(登錄接口等)
        if (token == null || token.isEmpty()) {
            return true;
        }

        try {
            // 1. 先驗證token是否合法
            String userId = JwtUtil.getUserId(token);
            request.setAttribute("userId", userId);

            // 2. 判斷是否需要續期(剩餘<10分鐘)
            if (JwtUtil.needRefresh(token)) {
                String newToken = JwtUtil.refreshToken(token);
                System.out.println("自動續期新Token:" + newToken);

                // 把新Token放回響應頭,前端自動替換即可
                response.setHeader("Authorization", newToken);
                response.setHeader("Access-Control-Expose-Headers", "Authorization");
            }

            return true;

        } catch (ExpiredJwtException e) {
            response.setStatus(401);
            return false;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
}

3. 註冊攔截器(Spring 5.x)

java

運行

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtRefreshInterceptor jwtRefreshInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtRefreshInterceptor)
                .addPathPatterns("/api/**")        // 你的接口前綴
                .excludePathPatterns("/api/login");// 登錄接口不攔截
    }
}

✅ 效果(和 session 完全一樣)

  • Token 設置 2 小時過期
  • 只要你10 分鐘內有操作
  • 攔截器自動生成新 Token
  • 前端從響應頭拿到新 Token
  • 永遠不掉線

這就實現了 JWT 模仿 Session 滑動過期


前端要做什麼?(超級簡單)

js

// 請求攔截器 → 帶上token
axios.interceptors.request.use(config => {
  config.headers.Authorization = localStorage.getItem('token')
  return config
})

// 響應攔截器 → 如果有新token,自動更新
axios.interceptors.response.use(res => {
  let newToken = res.headers.authorization
  if (newToken) {
    localStorage.setItem('token', newToken)
  }
  return res
})

我給你總結這套東西的效果

  • 無狀態 JWT
  • 自動續期
  • 和 session 體驗完全一樣
  • 不用存服務器
  • 分佈式 / 多服務器 完美支持

1. 登錄接口(直接複製到你的 Controller)

java

運行

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class LoginController {

    // 登錄接口
    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody LoginParam param) {
        Map<String, Object> result = new HashMap<>();

        // 1. 你自己的數據庫校驗賬號密碼
        // 這裡我直接模擬校驗通過
        if ("admin".equals(param.getUsername()) && "123456".equals(param.getPassword())) {

            // 2. 生成 JWT Token
            String token = JwtUtil.createToken("1001", param.getUsername());

            // 3. 返回給前端
            result.put("code", 200);
            result.put("msg", "登錄成功");
            result.put("token", token);
            return result;
        }

        // 登錄失敗
        result.put("code", 401);
        result.put("msg", "賬號或密碼錯誤");
        return result;
    }
}

2. 登錄參數類(LoginParam.java)

java

運行

public class LoginParam {
    private String username;
    private String password;

    // getter & setter
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

3. 測試接口返回效果(你會看到)

json

{
  "code": 200,
  "msg": "登錄成功",
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMDAx..."
}

4. 整套流程現在完整了 ✅

  1. 前端調用 /api/login → 獲取 token
  2. 前端把 token 放在請求頭Authorization: token
  3. 攔截器自動鑑權
  4. 快過期時自動續期
  5. 前端自動更新本地 token

和 Session 體驗一模一樣,但完全是 JWT 無狀態!


5. 你現在擁有的完整功能

  • ✅ JWT 生成 / 解析
  • ✅ 自動續期(滑動過期,像 Session)
  • ✅ 登錄接口
  • ✅ 鑑權攔截器
  • ✅ 適配 jjwt 0.13.0
  • ✅ 適配 Spring 5.x
  • ✅ 不影響老項目 Session

原生 JS、Axios、小程序三種常用場景,直接複製代碼即可,同時說明和後端的對應規則。

前置說明

後端約定:請求頭 Authorization 存放完整 token,不需要 Bearer 前綴(和上面攔截器匹配)。


一、Axios(最常用,Vue/React/ 普通前端)

1. 全局統一設置(推薦,所有請求自動帶 token)

javascript

運行

// 1. 先從本地存儲取出登錄後拿到的token
const token = localStorage.getItem('token');

// 2. 全局請求攔截器,自動附加請求頭
axios.interceptors.request.use(function (config) {
    // 存在token就加到請求頭
    if (token) {
        config.headers.Authorization = token;
    }
    return config;
}, function (error) {
    return Promise.reject(error);
});

// 3. 響應攔截器:接收後端續期返回的新token,自動更新本地
axios.interceptors.response.use(function (response) {
    const newToken = response.headers.authorization;
    if (newToken) {
        localStorage.setItem('token', newToken);
    }
    return response;
}, function (error) {
    // 401 未授權,跳轉到登錄頁
    if (error.response && error.response.status === 401) {
        localStorage.removeItem('token');
        window.location.href = '/login';
    }
    return Promise.reject(error);
});

2. 單個請求單獨設置(局部使用)

javascript

運行

axios.post('/api/test', {}, {
    headers: {
        Authorization: localStorage.getItem('token')
    }
}).then(res => {
    console.log(res.data);
})

二、原生 JavaScript(fetch / XMLHttpRequest)

1. fetch 方式

javascript

運行

const token = localStorage.getItem('token');

fetch('/api/test', {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': token  // 放置token
    }
}).then(res => {
    // 接收續期新token
    const newToken = res.headers.get('authorization');
    if (newToken) {
        localStorage.setItem('token', newToken);
    }
    return res.json();
}).then(data => {
    console.log(data);
});

2. XMLHttpRequest 方式

javascript

運行

const xhr = new XMLHttpRequest();
const token = localStorage.getItem('token');
xhr.open('GET', '/api/test', true);
// 設置請求頭
xhr.setRequestHeader('Authorization', token);
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.onload = function() {
    // 更新新token
    const newToken = xhr.getResponseHeader('authorization');
    if (newToken) {
        localStorage.setItem('token', newToken);
    }
    console.log(xhr.responseText);
};
xhr.send();

三、微信小程序

javascript

運行

const token = wx.getStorageSync('token');

wx.request({
    url: 'https://xxx/api/test',
    header: {
        'Authorization': token, // 攜帶token
        'content-type': 'application/json'
    },
    success: (res) => {
        // 接收續期token
        const newToken = res.header.authorization;
        if (newToken) {
            wx.setStorageSync('token', newToken);
        }
        console.log(res.data);
    },
    fail: (err) => {
        console.log(err);
    }
})

補充關鍵要點

  1. 存儲位置

    登錄成功後,把接口返回的 token 存入:

    • 網頁端:localStorage / sessionStorage
    • 小程序:wx.setStorageSync
  2. 跨域頭兼容(後端已處理)

    攔截器里加了 Access-Control-Expose-Headers: Authorization,作用是允許前端讀取響應頭裡的新 token,不用額外改代碼。

  3. 如果後續要加 Bearer 前綴(拓展)

    若後續改成標準格式 Authorization: Bearer 你的token,前端只需要改一行:

    javascript

    運行

    config.headers.Authorization = 'Bearer ' + token;
    

    同時後端攔截器也要對應截取:

    java

    運行

    String token = request.getHeader("Authorization");
    if(token != null && token.startsWith("Bearer ")){
        token = token.substring(7);
    }