我正在尝试使用 Dio 拦截器实现访问令牌刷新。我查看了我能找到的示例,但似乎都不起作用。
这是我的尝试:
class AuthInterceptor extends QueuedInterceptor {
final Dio dio;
final AuthService authService;
AuthInterceptor(this.dio, this.authService);
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
var accessToken = await TokenRepository.getAccessToken();
if (accessToken != null) {
logDebug('Access-Token: $accessToken');
options.headers['Authorization'] = 'Bearer $accessToken';
}
return handler.next(options);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.type == DioErrorType.response && err.response?.statusCode == 401) {
var accessToken = await TokenRepository.getAccessToken();
if (accessToken != null) {
final refreshToken = await TokenRepository.getRefreshToken();
final refreshResult = await authService
.refresh(RefreshRequest(accessToken, refreshToken!));
await TokenRepository.setAccessToken(refreshResult.accessToken);
handler.resolve(await _retry(err.requestOptions));
} else {
di<NavigationHelper>().navigateClearHistory(LoginPage());
handler.reject(err);
}
} else {
handler.next(err);
}
}
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return dio.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
Run Code Online (Sandbox Code Playgroud)
确实有效,我确实获得了一个新的访问令牌。但是我结束 onError 覆盖的方式有问题。我认为处理程序没有正确完成?我认为 handler.resolve 是错误的,但是我应该如何实现呢?如何正确完成此处理程序并使用新的访问令牌执行重试调用?
gen*_*ser 20
虽然关于如何使用 Dio 拦截器在过期时获取新令牌的信息不多,但我会尽力帮助您。
HttpClient 拦截器旨在修改、跟踪和验证来自服务器的 HTTP 请求和响应。
从方案中可以看出,拦截器是客户端的一部分,是我们应用程序中最边缘的功能。
由于拦截器是向服务器发送 HTTP 请求的最后一部分,因此它是处理请求重试并在过期时获取新令牌的好地方。
让我们谈谈您的代码,并尝试破坏它的每个部分。
根据要求:
虽然这部分应该可以正常工作,但await在每个 HTTP 请求上获取访问令牌的效率很低。它会大大减慢您的应用程序以及您检索 HTTP 响应的持续时间。
我建议您创建一个loadAccessToken()函数,负责将令牌加载到缓存存储库,并在每个请求中使用该缓存令牌。
请注意,我不知道你的TokenRepository.getAccessToken()函数在幕后做什么。但如果是 HTTP 请求,请注意它也会被拦截!如果您无法访问服务器,您将陷入尝试获取令牌的无限循环。
正如我稍后将解释的,我使用flutter_secure_storage包在应用程序中安全地保存新令牌,同时在第一个 HTTP 请求上加载该令牌以进行缓存。
错误:
我建议您使用jwt_decode包,在将过期令牌发送到服务器(onRequest)之前识别它们。这样,您将仅在过期时检索新令牌,并且每个 HTTP 请求将仅使用经过验证的令牌发送。
感谢 @FDuhen 评论,值得一提的是识别和处理 401 响应很重要(因为并非所有 401 都与令牌过期相关)。您可以通过全局行为的拦截器(例如将用户重定向回登录流程)或每个请求(并从您的 处理它ApiRepository)来执行此操作。为了示例的简单性,我不会重写onErrorInterceptor 函数。
现在让我们看一下我的代码。它并不完美,但希望它能帮助您更好地理解如何使用拦截器处理令牌。
首先,我创建了一个loadAccessToken()函数作为我的tokenRepository类的一部分,其目的是将令牌武装到缓存配置存储库中。
该函数的工作原理分为三个步骤:
在每个步骤 (1, 2) 中,我使用jwt_decoder包验证令牌未过期。如果它过期了,那么我从服务器获取它。
每次从服务器获取新令牌时,都需要使用flutter_secure_storage将其保存在本地(我们不想使用 shared_preferences 保存它,因为它不安全)。这样,您的令牌将被安全保存,并且检索速度会很快。
我的loadAccessToken功能:
/// Tries to get accessToken from [AppConfig], localSecureStorage or Keycloak
/// servers, and update them if necessary
Future<String?> get loadAccessToken async {
// get token from cache
var accessToken = _config.accessToken;
if (accessToken != null && !tokenHasExpired(accessToken)) {
return accessToken;
}
// get token from secure storage
accessToken =
await LocalSecureStorageRepository.get(SecureStorageKeys.accessToken);
if (accessToken != null && !tokenHasExpired(accessToken)) {
// update cache
_config.accessToken = accessToken;
return accessToken;
}
// get token from Keycloak server
final keycloakTokenResponse = await _accessTokenFromKeycloakServer;
accessToken = keycloakTokenResponse.accessToken;
final refreshToken = keycloakTokenResponse.refreshToken;
if (!tokenHasExpired(accessToken) && !tokenHasExpired(refreshToken)) {
// update secure storage
await Future.wait([
LocalSecureStorageRepository.update(
SecureStorageKeys.accessToken,
accessToken,
),
LocalSecureStorageRepository.update(
SecureStorageKeys.refreshToken,
refreshToken,
)
]);
// update cache
_config.accessToken = accessToken;
return accessToken;
}
return null;
}
Run Code Online (Sandbox Code Playgroud)
这是tokenHasExpired函数(使用jwt_decoder包):
bool tokenHasExpired(String? token) {
if (token == null) return true;
return Jwt.isExpired(token);
}
Run Code Online (Sandbox Code Playgroud)
现在,使用我们的拦截器处理访问令牌将变得更加容易。正如您在下面看到的(在我的拦截器示例中),我传递了一个单例AppConfig实例和一个tokenRepository包含loadAccessToken()我们之前讨论过的函数的实例。
我在覆盖功能上所做的onRequest就是
AppConfig)。loadAccessToken(首先从存储中加载,然后才从服务器令牌提供者加载) 。我的拦截器:
class ApiProviderTokenInterceptor extends Interceptor {
ApiProviderTokenInterceptor(this._config, this._tokenRepository);
final AppConfig _config;
final TokenRepository _tokenRepository;
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (options.headers['requires-token'] == 'false') {
// if the request doesn't need token, then just continue to the next
// interceptor
options.headers.remove('requiresToken'); //remove the auxiliary header
return handler.next(options);
}
var token = _config.accessToken;
if (token == null || _tokenRepository.tokenHasExpired(token)) {
token = await _tokenRepository.loadAccessToken;
}
options.headers.addAll({'authorization': 'Bearer ${token!}'});
return handler.next(options);
}
@override
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) {
return handler.next(response);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
// <-- here you can handle 401 response, which is not related to token expiration, globally to all requests
return handler.next(err);
}
}
Run Code Online (Sandbox Code Playgroud)
如果您在从服务器(在函数上)检索新令牌时遇到错误loadAccessToken()。然后处理错误tokenRepository并决定向您的客户呈现什么(一般性的内容,例如“我们的服务器有问题,请稍后再试”,应该没问题)。
您可以添加内置的 DioLogInterceptor来打印每个请求和响应(对调试非常有帮助)。
或者您可以使用Pretty_dio_logger包来打印漂亮的彩色请求和响应日志。
_ApiProvider(
dio
..interceptors.add(authInterceptor)
// ..interceptors.add(LogInterceptor())
..interceptors.add(
PrettyDioLogger(
requestBody: true,
requestHeader: true,
),
),
);
Run Code Online (Sandbox Code Playgroud)
我发现了一个这样的例子,我可以使用 Dio 拦截器刷新访问令牌。
class DioHelper {
final Dio dio;
DioHelper({@required this.dio});
final CustomSharedPreferences _customSharedPreferences =
new CustomSharedPreferences();
static String _baseUrl = BASE_URL;
String token = "";
void initializeToken(String savedToken) {
token = savedToken;
_initApiClient();
}
Future<void> initApiClient() {
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
options.headers["Authorization"] = "Bearer " + token;
return options;
}, onResponse: (Response response) {
return response;
}, onError: (DioError error) async {
RequestOptions origin = error.response.request;
if (error.response.statusCode == 401) {
try {
Response<dynamic> data = await dio.get("your_refresh_url");
token = data.data['newToken'];
_customSharedPreferences.setToken(data.data['newToken']);
origin.headers["Authorization"] = "Bearer " + data.data['newToken'];
return dio.request(origin.path, options: origin);
} catch (err) {
return err;
}
}
return error;
}));
dio.options.baseUrl = _baseUrl;
}
Future<dynamic> get(String url) async {
try {
final response = await dio.get(url);
var apiResponse = ApiResponse.fromJson(response.data);
if (apiResponse.status != 200) {
throw Exception(apiResponse.message);
}
return apiResponse.data;
} on DioError catch (e) {
// debugging purpose
print('[Dio Helper - GET] Connection Exception => ' + e.message);
throw e;
}
}
Future<dynamic> post(String url,
{Map headers, @required data, encoding}) async {
try {
final response =
await dio.post(url, data: data, options: Options(headers: headers));
ApiResponse apiResponse = ApiResponse.fromJson(response.data);
if (apiResponse.status != 200) {
throw Exception(apiResponse.message);
}
return apiResponse.data;
} on DioError catch (e) {
// debugging purpose
print('[Dio Helper - GET] Connection Exception => ' + e.message);
throw e;
}
}
}
Run Code Online (Sandbox Code Playgroud)
你能调查一下吗
| 归档时间: |
|
| 查看次数: |
10949 次 |
| 最近记录: |