什么是无感刷新token?
无感刷新token是指,在token过期之前,系统自动使用RefreshToken获取新的AccessToken,从而实现Token的无感知刷新,不用跳转到登录页面重新登录,用户可以无缝继续使用。
在实现无感知刷新token的过过程中,需要考虑以下几个问题:
- 如何判断Token是否过期?通常Token过期,请求接口会返回401的状态码。
- 如何在Token过期时自动使用RefreshToken获取新的AccessToken?
- 获取到新的token后如何继续执行上一次的请求?
本文是基于Flutter的Dio进行实现的。在实现过程中使用了两个Dio,一个负责正常的网络请求,一个负责刷新token。
运行环境
Flutter 3.24.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 2663184aa7 (9 weeks ago) • 2024-09-11 16:27:48 -0500
Engine • revision 36335019a8
Tools • Dart 3.5.3 • DevTools 2.37.3
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
dio: ^5.7.0
实现步骤
步骤一:获取AccessToken和RefreshToken
用户登录成功之后,服务器接口会返回用户登录信息,并将AccessToken和RefreshToken一起返回在用户信息之中,客户端将用户信息保存在一个全局变量中,并缓存在本地。AccessToken用于访问受保护的接口,RefreshToken用于获取新的AccessToken。我们采用JWT来实现认证。
class Global {
static late User? loginUser;
}
步骤二:设置请求拦截,在请求中携带AccessToken
在这里呢,我们就需要对Dio进行二次封装,然后设置请求拦截器,在请求拦截器中给每个需要认证的接口请求头中携带AccessToken。AccessToken可以在Global.loginUser中拿到(记得将登陆用户存到本地缓存中,每次启动App的时候自动从本地缓存读取上次登录的用户)。
ApiClient
定义一个ApiClient,里面有两个Dio,分别是_httpClient和_tokenDio。
_httpClient负责正常的API接口请求。_tokenDio负责无感刷新AccessToken。refreshTokenSuccessToken刷新成功的回调,在此处可进行Token刷新后的其他业务逻辑处理。
代码如下所示:
class ApiClient {
static final BaseOptions _defaultOptions = BaseOptions(
baseUrl: 'https://yourserveraddress',
headers: {'tenant-id': 1});
late final Dio _httpClient;
late final Dio _tokenDio;
late final Function(Map)? refreshTokenSuccess;
/// Creates an [ApiClient] with default options.
ApiClient({this.refreshTokenSuccess}) {
_httpClient = Dio(_defaultOptions);
_tokenDio = Dio(_defaultOptions);
_tokenDio.interceptors.add(TokenInterceptors());
_tokenDio.interceptors.add(LogInterceptor());
_httpClient.interceptors.add(LogInterceptor());
_httpClient.interceptors.add(AuthInterceptor(tokenDio: _tokenDio, refreshTokenSuccess: refreshTokenSuccess!));
}
@override
String toString() {
return "ApiClient(_httpClient.options.headers['Authorization']: ${_httpClient.options.headers['Authorization']})";
}
}
TokenInterceptors
TokenInterceptors定义一个Token拦截器,在onRequest中将Token设置给每个请求的接口,为下面的AuthInterceptor做准备。
class TokenInterceptors extends Interceptor {
TokenInterceptors();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
User? user = Global.loginUser;
// set request header
if (user != null) {
options.headers['Authorization'] = 'Bearer ${user.accessToken}';
options.headers['shop-id'] = user.shopId;
options.headers['staff-type'] = user.type;
}
handler.next(options);
}
}
AuthInterceptor
AuthInterceptor正常访问接口的拦截器,在onRequest中依将给每个需要认证的接口请求头中携带AccessToken。
在onResponse中拦截返回的接口数据。
- 如果发现接口返回Token过期的状态码
_tokenExpiredCode(通常为401,具体看服务器返回),这时通过调用refreshToken方法,使用ApiClient中的_tokenDio来刷新服务器Token。 - Token刷新成功之后,重新设置
Global.loginUser后,再调用_retry(response.requestOptions)将上一个Token过期的请求重新发送。 - 如果Token没有过期直接调用
handler.next(response)返回正确的数据。
class AuthInterceptor extends QueuedInterceptorsWrapper {
final Dio tokenDio;
final int _tokenExpiredCode = 401;
final Function(Map)? refreshTokenSuccess;
AuthInterceptor({required this.tokenDio, this.refreshTokenSuccess});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
User? user = Global.loginUser;
// set request header
if (user != null) {
options.headers['Authorization'] = 'Bearer ${user.accessToken}';
options.headers['shop-id'] = user.shopId;
options.headers['staff-type'] = user.type;
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
if (response.data['code'] == _tokenExpiredCode) {
debugPrint('${response.requestOptions.path} not have auth need refresh token');
bool isAuth = await refreshToken();
debugPrint('refresh token success!');
if (isAuth) {
Response retryResponse = await _retry(response.requestOptions);
debugPrint('retry path: ${response.requestOptions.path}');
debugPrint('retry response: ${retryResponse.data.toString()}');
handler.resolve(retryResponse);
}
} else {
handler.next(response);
}
}
Future<bool> refreshToken() async {
User? user = Global.loginUser;
if (user == null) return false;
debugPrint("refresh token start");
Map<String, dynamic> params = {"refreshToken": user?.refreshToken};
Response response = await tokenDio.get('/refresh-token-v2', queryParameters: params);
if (response.data['code'] == 0) {
// update local token
if (refreshTokenSuccess != null) {
refreshTokenSuccess!(response.data);
}
return true;
} else {
return false;
}
}
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
return tokenDio.request<dynamic>(requestOptions.path);
}
}