在微信小程序开发中,身份认证是保障用户数据安全的核心环节。JWT(JSON Web Token)作为一种轻量级的身份验证机制,凭借其无状态、易扩展的特性,成为前后端分离架构的首选方案。本文将围绕 JWT 的生成、传递、验证和刷新等全生命周期管理,结合 UniApp+Java 技术栈,详解如何在微信小程序中实现安全可靠的身份认证系统。
一、JWT 基础:为什么选择 JWT?
JWT 的核心优势
- 无状态:服务器无需存储会话信息,减轻服务器负担
- 跨域支持:适合分布式系统和前后端分离架构
- 自包含:令牌本身包含用户关键信息,减少数据库查询
- 防篡改:使用密钥签名,确保传输过程中不被篡改
JWT 的结构组成
一个完整的 JWT 由三部分组成,用.分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkiLCJuYW1lIjoiSm9obiBEb2UifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header(头部):指定算法和令牌类型
- Payload(载荷):存储用户 ID、过期时间等声明信息
- Signature(签名):用密钥对前两部分加密生成的签名
二、后端 JWT 工具类:生成与验证实现
基于前文微信授权登录的 Java 后端,我们先实现一个通用的 JWT 工具类,用于令牌的生成、验证和解析:
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类:负责令牌的生成、验证和解析
*/
public class JwtUtils {
// 令牌过期时间:7天(单位:毫秒)
private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000;
// 密钥(实际项目中应放在配置文件,且定期更换)
private static final String SECRET_KEY = "your-256-bit-secret-key-should-be-very-long-and-complex";
/**
* 生成JWT令牌
* @param userId 用户唯一标识
* @return 生成的令牌
*/
public static String generateToken(String userId) {
// 自定义载荷信息
Map<String, Object> claims = new HashMap<>();
claims.put("role", "user"); // 示例:存储用户角色
return Jwts.builder()
.setClaims(claims) // 设置自定义载荷
.setSubject(userId) // 设置主题(一般为用户ID)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 设置签名算法和密钥
.compact();
}
/**
* 从令牌中获取用户ID
* @param token JWT令牌
* @return 用户ID
*/
public static String getUserIdFromToken(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 验证令牌是否有效
* @param token JWT令牌
* @param userId 预期的用户ID
* @return 令牌是否有效
*/
public static boolean validateToken(String token, String userId) {
final String extractedUserId = getUserIdFromToken(token);
return (extractedUserId.equals(userId) && !isTokenExpired(token));
}
/**
* 检查令牌是否过期
* @param token JWT令牌
* @return 是否过期
*/
public static boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 从令牌中获取过期时间
* @param token JWT令牌
* @return 过期时间
*/
private static Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 从令牌中提取指定的载荷信息
* @param token JWT令牌
* @param claimsResolver 解析函数
* @return 提取的信息
*/
private static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 从令牌中提取所有载荷信息
* @param token JWT令牌
* @return 载荷信息
*/
private static Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
}
三、Spring Boot 集成:拦截器实现令牌验证
通过 Spring Boot 的拦截器机制,实现对需要身份认证的接口进行自动验证:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JWT拦截器:验证请求中的令牌
*/
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头中获取令牌
String authHeader = request.getHeader("Authorization");
// 2. 检查令牌是否存在
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("未授权访问:请提供有效的令牌");
return false;
}
// 3. 提取令牌(去除"Bearer "前缀)
String token = authHeader.substring(7);
try {
// 4. 验证令牌
String userId = JwtUtils.getUserIdFromToken(token);
// 5. 检查令牌是否过期
if (JwtUtils.isTokenExpired(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌已过期,请重新登录");
return false;
}
// 6. 将用户ID存入请求属性,供后续处理使用
request.setAttribute("userId", userId);
return true;
} catch (SignatureException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌验证失败:签名无效");
return false;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌验证失败:" + e.getMessage());
return false;
}
}
}
配置拦截器使其生效:
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 {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册JWT拦截器
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/api/user/**") // 需要验证的接口
.excludePathPatterns("/api/login") // 登录接口不验证
.excludePathPatterns("/api/register"); // 注册接口不验证
}
}
四、UniApp 前端:JWT 存储与请求拦截
在 UniApp 中实现 JWT 的存储、自动添加到请求头以及过期处理:
1. 封装存储工具(utils/storage.js)
/**
* 本地存储工具类:处理JWT令牌的存储与读取
*/
export default {
// 存储令牌
setToken(token) {
uni.setStorageSync('token', token)
},
// 获取令牌
getToken() {
return uni.getStorageSync('token')
},
// 清除令牌
removeToken() {
uni.removeStorageSync('token')
},
// 检查是否有令牌
hasToken() {
return !!this.getToken()
}
}
2. 配置请求拦截器(utils/request.js)
import storage from './storage'
// 创建请求实例
const request = (options) => {
return new Promise((resolve, reject) => {
// 1. 配置默认参数
const baseUrl = 'https://api.yourdomain.com'
const token = storage.getToken()
// 2. 添加请求头(包含令牌)
const header = {
...options.header,
'Content-Type': options.contentType || 'application/json'
}
// 如果有令牌,添加到请求头
if (token) {
header.Authorization = `Bearer ${token}`
}
// 3. 发起请求
uni.request({
url: baseUrl + options.url,
method: options.method || 'GET',
data: options.data || {},
header,
success: (res) => {
// 4. 处理响应
if (res.statusCode === 200) {
// 业务逻辑成功
if (res.data.code === 0) {
resolve(res.data)
} else {
reject(res.data)
}
} else if (res.statusCode === 401) {
// 401未授权:令牌无效或过期
storage.removeToken()
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
// 跳转到登录页
setTimeout(() => {
uni.navigateTo({ url: '/pages/login/index' })
}, 1500)
reject(new Error('登录已过期'))
} else {
// 其他错误
uni.showToast({
title: `请求失败:${res.statusCode}`,
icon: 'none'
})
reject(res)
}
},
fail: (err) => {
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
})
reject(err)
}
})
})
}
// 导出常用请求方法
export const api = {
get: (url, data, options = {}) => request({ url, data, method: 'GET', ...options }),
post: (url, data, options = {}) => request({ url, data, method: 'POST', ...options }),
put: (url, data, options = {}) => request({ url, data, method: 'PUT', ...options }),
delete: (url, data, options = {}) => request({ url, data, method: 'DELETE', ...options })
}
五、令牌刷新机制:无缝续期实现
为避免用户在使用过程中因令牌过期而被迫重新登录,实现令牌自动刷新机制:
1. 后端刷新接口
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TokenController {
/**
* 刷新令牌接口
* @param authHeader 包含旧令牌的请求头
* @return 新令牌
*/
@PostMapping("/api/token/refresh")
public Result refreshToken(@RequestHeader("Authorization") String authHeader) {
try {
// 验证旧令牌格式
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return Result.error("无效的令牌格式");
}
String oldToken = authHeader.substring(7);
// 检查旧令牌是否即将过期(还剩1小时内)
Date expiration = JwtUtils.extractExpiration(oldToken);
long timeLeft = expiration.getTime() - System.currentTimeMillis();
if (timeLeft > 60 * 60 * 1000) {
return Result.error("令牌无需刷新");
}
// 从旧令牌中获取用户ID
String userId = JwtUtils.getUserIdFromToken(oldToken);
// 生成新令牌
String newToken = JwtUtils.generateToken(userId);
return Result.success(newToken, "令牌刷新成功");
} catch (Exception e) {
return Result.error("令牌刷新失败:" + e.getMessage());
}
}
}
2. 前端自动刷新实现
修改请求拦截器,添加令牌自动刷新逻辑:
// 在request.js中添加以下逻辑
let isRefreshing = false; // 是否正在刷新令牌
let requests = []; // 等待刷新的请求队列
// 刷新令牌函数
const refreshToken = async () => {
try {
const oldToken = storage.getToken();
const res = await uni.request({
url: 'https://api.yourdomain.com/api/token/refresh',
method: 'POST',
header: {
'Authorization': `Bearer ${oldToken}`
}
});
if (res.data.code === 0) {
// 刷新成功,更新令牌
storage.setToken(res.data.data);
return res.data.data;
} else {
// 刷新失败,需要重新登录
storage.removeToken();
throw new Error('令牌刷新失败');
}
} catch (err) {
storage.removeToken();
uni.navigateTo({ url: '/pages/login/index' });
throw err;
}
};
// 修改请求函数中的成功处理部分
success: async (res) => {
if (res.statusCode === 401) {
// 401错误处理
const currentRequest = options;
if (!isRefreshing) {
isRefreshing = true;
try {
// 刷新令牌
const newToken = await refreshToken();
// 重新发起队列中的请求
requests.forEach(cb => cb(newToken));
requests = [];
// 重新发起当前请求
const retryRes = await request({
...currentRequest,
header: {
...currentRequest.header,
'Authorization': `Bearer ${newToken}`
}
});
resolve(retryRes);
} catch (err) {
reject(err);
} finally {
isRefreshing = false;
}
} else {
// 正在刷新令牌,将请求加入队列
return new Promise(resolve => {
requests.push((token) => {
currentRequest.header = {
...currentRequest.header,
'Authorization': `Bearer ${token}`
};
resolve(request(currentRequest));
});
});
}
}
// ...其他状态处理
}
六、安全最佳实践与常见问题
安全加固措施
- 令牌存储:小程序中使用uni.setStorageSync存储,避免暴露在 URL 中生产环境提议使用 HTTPS 协议,防止中间人攻击
- 密钥管理:密钥长度至少 256 位,定期更换不同环境(开发 / 测试 / 生产)使用不同密钥密钥存储在安全的配置中心,而非代码中
- 令牌策略:合理设置过期时间(提议 1-7 天)实现令牌黑名单机制,处理注销和安全事件敏感操作(如支付、修改密码)提议重新验证身份
常见问题解决方案
- 令牌被盗用:缩短令牌有效期实现设备绑定,检查请求来源提供快速注销功能
- 刷新令牌失效:实现刷新令牌的有效期(长于访问令牌)限制刷新次数,防止滥用
- 性能优化:避免在 Payload 中存储过多信息对令牌验证过程进行缓存思考使用 Redis 存储黑名单,加速验证
通过本文的实现,可以掌握 JWT 在 UniApp 小程序中的完整应用方案,包括令牌的生成、验证、存储和自动刷新。这种身份认证机制既能保障用户数据安全,又能提供流畅的用户体验,是现代小程序开发的必备技能。
在实际项目中,可根据业务需求调整令牌有效期、载荷内容和安全策略,构建更贴合自身业务的身份认证系统。
#JWT 实战 #UniApp 开发 #微信小程序 #身份认证 #前后端分离
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...
