1. 核心引言
传统单一 JWT 模型下,令牌有效期与安全性存在矛盾:长有效期方便用户,但一旦泄露风险高;短有效期安全,但会频繁要求用户登录,影响体验。为解决这一问题,引入双 Token 机制,将职责拆分:Access Token 无状态、短生命周期,仅用于接口鉴权,泄露风险小;Refresh Token 有状态、长生命周期,由服务端集中管理,用于安全刷新 Access Token,并支持即时撤销。引入双token机制,可以快速检测用户重复登录,盗号等问题。
2. alova
Alova 是一个现代前端请求库,它把请求(Method)和状态(State)分开,让接口调用和组件状态天然绑定。你可以为每个请求单独配置 URL、参数、headers 和缓存策略,同时自动把响应更新到界面。它支持多种请求适配器(fetch、axios 等)、实例化配置 baseURL、超时和全局缓存,还能用 Hook 轻松管理状态。Alova 还考虑了安全性,支持 credentials、请求拦截器和 Token 注入,方便做双 Token 验证或自动刷新。简单来说,它不仅帮你发请求,更让状态管理、缓存和认证逻辑变得清晰、好用,开发起来更高效。 但是作者是个菜鸟,在引入Alova时,没有使用Hook方式进行封装,反而使用了类似Axios的方式来进行封装,所以下面的教程会让Alova看起来更像Axios,后续将会重新封装为Hook。
话不多说,开始吧。
3. alova封装
Alova封装采用createAlova的方式进行封装,采用拦截器来实现配置项的添加,下面是最基础的Alova的封装示例:
const instance = createAlova({
requestAdapter: adapterFetch(),
baseURL: BASE_URL,
beforeRequest(method) {
method.config.credentials = 'include'
}
})
上面的代码创建了一个最简单Alova实例,请求发起如下所示:
GET = <T, R>(url: string, requestData?: IRequest<T>): Promise<IResponse<R>> => {
return alovaInstance.Get<IResponse<R>>(url, {
params: requestData?.params ?? {},
debounce: requestData?.debounce ?? 0
}).send()
}
上面是一个泛型封装的GET请求示例,使用刚刚的Alova发起GET请求。其余方式也类似封装,当然,如果嫌弃作者TS水平太菜也可以自行封装。
4.双token校验实现
对于前端来说,双token需要做的是登录正常获取token,然后当接口返回401时,请求后端接口续签获得新的access_token,其余事情都交给后端来完成,实际上是相对简单的,前端需要关注的就是如何重放当前请求的问题。
正常获取token
首先就是正常获取token,就按照以前的代码流程来进行交互,登录,前端存储token到缓存。类似代码如下:
const res = await login<typeof rest, any>({ data: rest })
const { access_token, user } = res.data || {}
setUser({ ...user, access_token })
缓存后正常请求后端。
续签access_token
当access_token过期时,后端会返回401。这时前端就需要请求相应的接口续签token,这时的难点实际上是如何发起新的请求和重放当前请求。 先来看发起新的请求,采用最简单也是较为合理的方式来进行。直接创建一个新的实例来进行请求。这里如果不使用新的实例会发生什么事,我们来盘一盘: 1.首先用同一个实例,会导致状态判定混乱,续签和正常请求token验证失败都是401,对写代码的我们来说是一个大问题。 2.这是最关键的一点,由于请求是在拦截器中断的,当前实例如何发起另一个请求?想解决他,你得去翻阅大量文档,还有不一定有结果(当然作者没有翻过,不敢妄下定论)。 总所上述,新开一个实例带来的成本要低一些,还好区分状态和维护。 要想完成这个工作第一步肯定是新建一个实例,只不过这个实例不必封装,直接只发起一次请求就可以:
const refreshToken = async (): Promise<Response> => {
const tokenInstance = createAlova({
requestAdapter: adapterFetch(),
baseURL: BASE_URL,
beforeRequest(method) {
method.config.credentials = 'include'
}
})
return tokenInstance.Post('/yourapi/refresh')
}
值得注意的是:
method.config.credentials = 'include'
这一行代码是由于作者目前是本地环境,采用了cookie的方式来存储refresh_token,所以后端跨域放行后需要带上这个,如果你直接存储redis或者其它方式存储可以不加。
好,现在新的实例已经创建,下面需要完成的就很简单,拦截主请求的401并通过这个实例续签,然后刷新refresh即可。只需要在响应拦截器编写如下代码:
const data = await response.json()
if (data.code === 401) {
return await refreshTokenHandler(methodInstance)
}
这里有一个重要的地方,刷新续签refresh_token后需要重新发起当前请求,由于Alova目前较新,网上解决方式很少,如果你也找不到解决方案,可以参考作者的方法:
方法一
也就是作者的方法,我们来读一下响应拦截器的Success方法的源码类型:
onSuccess?: RespondedHandler<AG>;
export type RespondedHandler<AG extends AlovaGenerics> = (response: AG['Response'], methodInstance: Method<AG>) => any;
追溯下来不难看到它有第二个参数叫做methodInstance,也就是当前方法的实例,再来读一下它的类型:
/**
* Request method type
*/
export declare class Method<AG extends AlovaGenerics = any> extends Promise<AG['Responded']> {
baseURL: string;
url: string;
type: MethodType;
config: MethodRequestConfig & AlovaMethodConfig<AG, AG['Responded'], AG['Transformed']>;
data?: RequestBody;
send(forceRequest?: boolean): Promise<AG['Responded']>;
}
类型很大,无用的信息作者已舍去,感兴趣可以自行阅读。不难看出,我们想要的信息都有,包括baseURL,url,send,config等重要信息,还记得吗?上面我们新增了一个Alova的实例,你愿意的话,可以利用这个实例来发起请求,不过相对麻烦,并不推荐。注意send本身就是发起请求的方法,我们此时只需要调用这个函数就可以很简单的重新发起这个请求了。示例如下所示:
const refreshTokenHandler = async (methodInstance: Method) => {
const refreshResponse = await refreshToken()
const refreshData = await refreshResponse.json()
if (refreshData.code === 200) {
const newToken = refreshData.data.access_token
useUserStore.setState(state => ({
user: {
...state.user,
access_token: newToken
}
}))
methodInstance.config.headers['Authorization'] = `Bearer ${newToken}`
return methodInstance.send()
}
// 续签接口也报错401,说明完全过期,直接返回错误
if (refreshData.code === 401) {
// 强制登出
useUserStore.setState(state => ({
user: {
...state.user,
access_token: ''
}
}))
}
throw new Error(refreshData.message || '刷新token失败')
}
需要注意就是带上新的续签的refresh_token即可。
方法二
方法二和上面大差不差,就是调用方式不一样,上面我们已经拿到了当前的示例,那么就可以调用我们的Alova实例,将方法实例传递进去即可,只有发起请求有变化,其余都一样:
alovaInstance.send(methodInstance)
方法二作者是浅尝辄止,当时测试正确就没进一步,或许会有记忆不清的地方,各位大佬多担待。
5 总结
到这里实际上双token验证就已经实现,只要后端是支持双token的,那么这样可以成功使用access_token请求并在其过期时请求接口获得新的refresh_token,从而完成双token验证。前端侧并不需要承担过多复杂逻辑,只需在登录阶段正确缓存 token,在接口返回 401 时通过独立实例请求续签接口,并在成功后利用 methodInstance.send() 重放原始请求即可。通过将续签逻辑与业务请求解耦,既避免了状态混乱,也让代码结构更加清晰可控。这种实现方式较为稳定、易理解,也适合在真实项目中长期维护和演进。