惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
D
DataBreaches.Net
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
S
Secure Thoughts
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
L
LINUX DO - 最新话题
V2EX - 技术
V2EX - 技术
腾讯CDC
GbyAI
GbyAI
G
Google Developers Blog
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
Schneier on Security
Schneier on Security
Microsoft Security Blog
Microsoft Security Blog
Jina AI
Jina AI
WordPress大学
WordPress大学
aimingoo的专栏
aimingoo的专栏
MyScale Blog
MyScale Blog
Help Net Security
Help Net Security
K
Kaspersky official blog
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
MongoDB | Blog
MongoDB | Blog
Scott Helme
Scott Helme
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
H
Heimdal Security Blog
H
Help Net Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
S
Security Affairs
TaoSecurity Blog
TaoSecurity Blog
The GitHub Blog
The GitHub Blog
Hacker News: Ask HN
Hacker News: Ask HN
Martin Fowler
Martin Fowler
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI

博客园 - 锐洋智能

windows 下 降级迁移 Redis 8.8.0(源) → Redis 8.6.3(目标) 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 里)

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)

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)

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 滑动过期


前端要做什么?(超级简单)

// 请求拦截器 → 带上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)

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)

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. 测试接口返回效果(你会看到)

{
  "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)

// 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. 单个请求单独设置(局部使用)

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

二、原生 JavaScript(fetch / XMLHttpRequest)

1. fetch 方式

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 方式

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();

三、微信小程序

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,前端只需要改一行:

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