Spring 5.x + 老项目的 JWT 拦截器 + 自动续期
最终完整代码: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. 整套流程现在完整了 ✅
- 前端调用 /api/login → 获取 token
- 前端把 token 放在请求头:
Authorization: token - 拦截器自动鉴权
- 快过期时自动续期
- 前端自动更新本地 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);
}
})
补充关键要点
-
存储位置登录成功后,把接口返回的
token存入:- 网页端:
localStorage/sessionStorage - 小程序:
wx.setStorageSync
- 网页端:
-
跨域头兼容(后端已处理)拦截器里加了
Access-Control-Expose-Headers: Authorization,作用是允许前端读取响应头里的新 token,不用额外改代码。 -
如果后续要加 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); }
浙公网安备 33010602011771号