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

推荐订阅源

小众软件
小众软件
WordPress大学
WordPress大学
IT之家
IT之家
G
Google Developers Blog
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
Martin Fowler
Martin Fowler
V
V2EX
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog
V
Visual Studio Blog
有赞技术团队
有赞技术团队
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网

博客园 - 锐洋智能

A股怎么算的量比? CIDR 从最大网段 `0.0.0.0/0` → 最小单个 IP `/32`完整梳理 CIDR 无类别域间路由写法 Java 实现A股前复权/后复权计算原理 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
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);
    }