跳到主要内容
版本:v3

Token认证拦截器

策略类型

拦截器

在使用扩展 hooks 前,确保你已熟悉了 alova 的基本使用。

Token 认证拦截器,对基于 token 的登录、登出、token 附带、token 刷新进行统一管理,并支持无感刷新 token。

特性

  • 统一维护 Token 身份认证的所有代码,包括登录、登出、token 附带、token 刷新等;
  • 支持在客户端和服务端验证 token 过期,并无感刷新 token;
  • 依赖 token 的请求自动等待 token 刷新完成再请求;
  • 使用元数据设置请求身份;
  • 自动放行不依赖 token 的访客请求;

绑定 Token 认证拦截器

Token 身份认证是通过全局的拦截器完成的,分别提供了createClientTokenAuthenticationcreateServerTokenAuthentication 用于基于客户端和服务端的身份认证。

  • 基于客户端的身份认证:表示从客户端判断 token 是否过期,例如在登录时获取到的 token 过期时间;
  • 基于服务端的身份认证:表示从服务端返回的状态判断 token 是否过期,例如status为 401 时表示过期;

绑定基于客户端的身份认证的拦截器

import { createClientTokenAuthentication } from 'alova/client';
import { createAlova } from 'alova';

const { onAuthRequired, onResponseRefreshToken } = createClientTokenAuthentication({
// ...
});

const alovaInstance = createAlova({
// ...
beforeRequest: onAuthRequired(method => {
// ...原请求前拦截器
}),
responded: onResponseRefreshToken((response, method) => {
//...原响应成功拦截器
return response.json();
})
});

onResponseRefreshToken中也可以绑定响应错误和完成的拦截器,也和原来的用法相同。

createAlova({
// ...
responded: onResponseRefreshToken({
onSuccess: (response, method) => {
//...原响应成功拦截器
},
onError: (error, method) => {
//...原响应错误拦截器
},
onComplete: method => {
//...原响应完成拦截器
}
})
});

如果不需要设置拦截器,也可以不传入拦截器函数。

createAlova({
//...
beforeRequest: onAuthRequired(),
responded: onResponseRefreshToken()
});

绑定基于服务端的身份认证拦截器

与基于客户端的用法相同

import { createServerTokenAuthentication } from 'alova/client';
import { createAlova } from 'alova';

const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
// ...
});

const alovaInstance = createAlova({
// ...
beforeRequest: onAuthRequired(method => {
// ...原请求前拦截器
}),
responded: onResponseRefreshToken((response, method) => {
//...原响应成功拦截器
return response.json();
})
});
注意

当你使用alova/fetch适配器时,你可能会遇到TypeError: Failed to execute 'json' on 'Response': body stream already read这个问题,这是因为Responsebody stream只能访问一次,你可以response.clone().json()来解决它。

在客户端无感刷新 Token

设置refreshToken并指定 token 是否过期,以及调用刷新 token 的函数就可以了。当 token 刷新完成前,所有依赖 token 的请求都将会等待 token 刷新完成。

createClientTokenAuthentication({
refreshToken: {
// 在请求前触发,将接收到method参数,并返回boolean表示token是否过期
isExpired: method => {
return tokenExpireTime < Date.now();
},

// 当token过期时触发,在此函数中触发刷新token
handler: async method => {
try {
const { token, refresh_token } = await refreshToken();
localStorage.setItem('token', token);
localStorage.setItem('refresh_token', refresh_token);
} catch (error) {
// token刷新失败,跳转回登录页
location.href = '/login';
// 并抛出错误
throw error;
}
}
}
});
注意
  1. 为了让refreshToken请求顺利通过,需要通过元数据标识authRolerefreshToken
  2. 如果 token 刷新失败必须抛出错误,阻止失败接口重试和等待接口继续请求。

了解更多元数据的信息,请前往method 元数据

export const refreshToken = () => {
const method = alovaInstance.Get('/refresh_token');
method.meta = {
authRole: 'refreshToken'
};
return method;
};

在服务端无感刷新 Token

与在客户端无感刷新 Token 相同,指定 token 是否过期,以及调用刷新 token 的函数就可以了。当 token 刷新完成前,所有依赖 token 的请求都将会等待 token 刷新完成。

在请求成功拦截器中处理

当使用alova/fetch时,只要服务端返回了响应数据,就会触发响应成功拦截器,此时我们需要在响应成功拦截器中处理 token 的刷新。

createServerTokenAuthentication({
refreshTokenOnSuccess: {
// 响应时触发,可获取到response和method,并返回boolean表示token是否过期
// 当服务端返回401时,表示token过期
isExpired: (response, method) => {
return response.status === 401;
},

// 当token过期时触发,在此函数中触发刷新token
handler: async (response, method) => {
try {
const { token, refresh_token } = await refreshToken();
localStorage.setItem('token', token);
localStorage.setItem('refresh_token', refresh_token);
} catch (error) {
// token刷新失败,跳转回登录页
location.href = '/login';
// 并抛出错误
throw error;
}
}
}
});

在请求错误拦截器中处理

当使用axios拦截器时,服务端返回了非200/300的状态码就会触发响应错误拦截器,此时我们需要在响应错误拦截器中处理 token 的刷新。

createServerTokenAuthentication({
refreshTokenOnError: {
// 响应时触发,可获取到error和method,并返回boolean表示token是否过期
// 当服务端返回401时,表示token过期
isExpired: (error, method) => {
return error.response.status === 401;
},

// 当token过期时触发,在此函数中触发刷新token
handler: async (error, method) => {
try {
const { token, refresh_token } = await refreshToken();
localStorage.setItem('token', token);
localStorage.setItem('refresh_token', refresh_token);
} catch (error) {
// token刷新失败,跳转回登录页
location.href = '/login';
// 并抛出错误
throw error;
}
}
}
});
注意
  1. 为了让refreshToken请求顺利通过,需要通过元数据标识authRolerefreshToken
  2. 如果 token 刷新失败必须抛出错误,阻止失败接口重试和等待接口继续请求。

了解更多元数据的信息,请前往method 元数据

export const refreshToken = () => {
const method = alovaInstance.Get('/refresh_token');
method.meta = {
authRole: 'refreshToken'
};
return method;
};

放行访客请求

有些接口不需要依赖 token 认证,我们称它们为“访客请求”,此时我们可以设置它们的元数据为authRole: null来绕过前端的拦截,让它们顺利发出请求和接收响应。

export const requestTokenNotRequired = () => {
const method = alovaInstance.Get('/token_not_required');
method.meta = {
authRole: null
};
return method;
};

登录拦截

在身份认证拦截器中,你还可以拦截登录请求,在拦截器中保存登录信息,达到统一维护身份认证代码的目的。

首先标识登录请求的元数据为authRole: 'login'

export const login = () => {
const method = alovaInstance.Get('/login');
method.meta = {
authRole: 'login'
};
return method;
};

再在登录拦截器中保存登录信息。

createClientTokenAuthentication({
login(response, method) {
localStorage.setItem('token', response.token);
localStorage.setItem('refresh_token', response.refresh_token);
}
});

createServerTokenAuthentication的登录拦截器用法相同。

附加 token

通常,我们会在beforeRequest附加 token 到请求信息中。在 Token 认证拦截器中提供了assignToken回调函数用于附加 token,它会过滤访客请求和登录请求,并在请求前触发,也可以达到统一维护身份认证代码的目的。

createClientTokenAuthentication({
assignToken: method => {
method.config.headers.Authorization = localStorage.getItem('token');
}
});

createServerTokenAuthentication的 assignToken 回调函数用法相同。

登出拦截

当你的登出也需要调用接口时,也可以拦截登出请求,清除登录信息。

首先标识登出请求的元数据为authRole: 'logout'

export const logout = () => {
const method = alovaInstance.Get('/logout');
method.meta = {
authRole: 'logout'
};
return method;
};

再在登出拦截器中清除登录信息。

createClientTokenAuthentication({
logout(response, method) {
localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
}
});

createServerTokenAuthentication的登录拦截器用法相同。

自定义标识身份

在上面的元数据身份标识中,实际上都默认的身份标识,如果需要自定义身份标识,你可以按下面这样设置。

token 刷新身份标识

createClientTokenAuthentication({
refreshToken: {
metaMatches: {
refreshToken: true
}
// ...
}
});
createServerTokenAuthentication({
refreshTokenOnSuccess: {
metaMatches: {
refreshToken: true
}
// ...
},
refreshTokenOnError: {
metaMatches: {
refreshToken: true
}
// ...
}
});

然后,元数据中具有refreshToken: true的请求,就会被认定为refreshToken身份。

export const refreshToken = () => {
const method = alovaInstance.Get('/refresh_token');
method.meta = {
refreshToken: true
};
return method;
};

访客身份标识

createClientTokenAuthentication({
visitorMeta: {
isVisitor: true
}
});

然后,元数据中具有isVisitor: true的请求,就会被认定为访客身份。

export const requestTokenNotRequired = () => {
const method = alovaInstance.Get('/token_not_required');
method.meta = {
isVisitor: true
};
return method;
};

登录身份标识

createClientTokenAuthentication({
login: {
metaMatches: {
login: true
},
handler(response, method) {
// 登录拦截器
}
}
});

然后,元数据中具有login: true的请求,就会被认定为login身份。

export const login = () => {
const method = alovaInstance.Get('/login');
method.meta = {
login: true
};
return method;
};

登出身份标识

createClientTokenAuthentication({
logout: {
metaMatches: {
logout: true
},
handler(response, method) {
// 登出拦截器
}
}
});

然后,元数据中具有logout: true的请求,就会被认定为logout身份。

export const logout = () => {
const method = alovaInstance.Get('/logout');
method.meta = {
logout: true
};
return method;
};

createServerTokenAuthentication的登录拦截器用法相同。

Typescript

默认情况下,createClientServerTokenAuthenticationcreateServerTokenAuthentication适配了alova/fetch请求适配器,你只需要指定statesHook的类型,如下:

const { onAuthRequired, onResponseRefreshToken } = createClientTokenAuthentication<
typeof VueHook
>({
//...
});

const alovaInstance = createAlova({
// ...
statesHook: VueHook,
beforeRequest: onAuthRequired(method => {
// method的类型为 Method<any, any, any, any, RequestInit, Response, Headers>
}),
responded: onResponseRefreshToken((response, method) => {
// response的类型为Response
return response.json();
})
});

如果你使用的不是alova/fetch请求适配器,你还需要指定请求适配器的类型,这也很简单。

以下为 axios 请求适配器为例,在createClientTokenAuthentication中指定请求适配器类型。

import { axiosRequestAdapter } from '@alova/adapter-axios';

const { onAuthRequired, onResponseRefreshToken } = createClientTokenAuthentication<
typeof VueHook,
typeof axiosRequestAdapter
>({
//...
});
const alovaInstance = createAlova({
//...
statesHook: VueHook,
beforeRequest: onAuthRequired(method => {
// method的类型为 Method<any, any, any, any, AlovaAxiosRequestConfig, AxiosResponse, AxiosResponseHeaders>
}),
responded: onResponseRefreshToken((response, method) => {
// response的类型为AxiosResponse
return response.data;
})
});

基于服务端的 Token 认证拦截器的用法相同。

import { axiosRequestAdapter } from '@alova/adapter-axios';

createServerTokenAuthentication<typeof VueHook, typeof axiosRequestAdapter>({
//...
});