Implement OAuth ticket refresh in Flutter based on Dio
1. Background
Currently, the project is developing an App with Flutter. Dio framework is adopted as the network request framework in this project, and OAuth2 protocol is adopted for user login. As we all know, in OAuth2 protocol, the user obtains the Access_token at the first login, and then uses refresh_Token to obtain a new Access_token after the access_token expires. The difficulty is that the service cannot normally return data if the ticket expires during the user’s use. In this case, the ticket needs to be refreshed automatically and the refresh process needs to be transparent to the user.
Having said that, there are several solutions:
- Simple way: Calculate the expiration time when the user opens the App and always make sure the ticket is updated before expiration.
The problem with this method is that if the account is logged in to another terminal, the ticket of the current terminal will be invalidated in advance (see service implementation, generally speaking, one account can only be allowed to log in to one terminal).
- Formal mode: When the user accesses the service on the App end, the service end verifies the Access_Token. When the ticket is invalid, the corresponding status code is displayed. The App end parses the return status code, executes the request to refresh the ticket according to the prompt of the status code, and resends the current access request of the user after obtaining the new ticket.
The following is the second way, which is to refresh the Token directly.
2. Token refresh process
The Token refresh process is as follows:
- Users query services through the App
- The App invokes the App server interface for query
- The App server invokes the OAuth server interface to verify whether the Access_token is invalid
- OAuth An error message is displayed when the access_Token is invalid
- The App server returns an OAuth server error message to the App server
- After receiving the note expiration prompt, the App invokes the OAuth server interface to refresh the Access_Token
- OAuth The server returns a new access_token
- The App saves the new Access_token to the local cache
- The App server resends the query service request to the App server with the new Access_token
- The App server verifies the access_Token and returns the service query structure to the App server
- The App side shows the service query structure to the user
As can be seen from the figure, the key part lies in step 6 refreshing the Token and step 9 resending the request. The implementation of these two parts is described below.
3. Refresh Token
Because it is uncertain which request triggers the ticket invalidation, the request can be intercepted globally and the response information can be analyzed and judged. Dio has interceptors that can intercept requests.
1. Add TokenInterceptor
Dio adds interceptor
Dio dio = new Dio(); // with default Options
// Set default configs
dio.options.baseUrl = baseURL ?? ApiPath.baseURL;
dio.options.connectTimeout = 100000; // 100s
dio.options.receiveTimeout = 100000; // 100s
dio.interceptors.clear();
// Add TokenInterceptor
dio.interceptors.add(TokenInterceptor(dio));
Copy the code
Interceptors are only valid for a single Dio instance, so interceptors are not shared between different Dio instances. So we need to make sure we’re using the same Dio instance for all requests in the project.
TokenInterceptor class
class TokenInterceptor extends Interceptor {
Dio _dio;
bool isReLogin = false;
Queue queue = new Queue();
TokenInterceptor(this._dio);
@override
Future onRequest(RequestOptions options) async {
return options;
}
@override
Future onResponse(Response response) async {
bool needRefreshToken = _checkIfNeedRefreshToken(response);
if(! needRefreshToken) {return super.onResponse(response);
}
// TODO sends a Token refresh request
return super.onResponse(response);
}
/// Check whether the Token needs to be refreshed
bool _checkIfNeedRefreshToken(Response<dynamic> response) {
if (response.data == null || response.data.isEmpty) {
return false;
}
var responseMap =
response.data is String ? jsonDecode(response.data) : response.data;
var head = responseMap['head'];
if (head == null) {
return false;
}
var statusCode = head['code'];
if(statusCode ! =99999 || "No log-in user found"! = responseMap['data']) {
return false;
}
return true; }}Copy the code
TIP: Because our server implementation put the error of Token verification failure in normal Response JSON, we parse it in onResponse. In general, onError is judged and processed according to the status code.
2. Send a Token refresh request
// Lock the ticket first to prevent other requests from executing while the ticket is refreshed
dio.interceptor.request.lock();
dio.interceptors.responseLock.lock();
dio.interceptors.errorLock.lock();
/// Refresh Token logic is performed here
/ / releases the lock
dio.interceptors.errorLock.unlock();
dio.interceptor.response.lock();
dio.interceptors.responseLock.unlock();
Copy the code
Most of the code given above is used to refresh the Token on the Internet, but this implementation method will repeatedly execute the refresh Token in the concurrent execution of multiple requests, or even block the Token all the time.
When multiple requests are sent simultaneously on a page, the Token refresh process in the above manner is shown as follows:
Obviously, this locking approach is not suitable for multi-request concurrent execution scenarios.
3. Use a queue to refresh the Token
Reference link: github.com/flutterchin…
Queue is a package that allows multiple futures to be executed sequentially. In this case, you only need to let the requests go into the ticket refresh mode one by one, and only one request can actually refresh the ticket. The specific process is shown in the figure below:
onResponse
Future onResponse(Response response) async {
bool needRefreshToken = _checkIfNeedRefreshToken(response);
if(! needRefreshToken) {return super.onResponse(response);
}
/ / reference https://github.com/flutterchina/dio/issues/590
// Check for if the token were successfully refreshed
bool success = await queue.add(() async {
var requestToken = response.request.data['head'] ['token'];
var globalToken = UserUtil.token.access_token;
If the token is consistent, the token needs to be updated. If the token is inconsistent, the token has already been updated in other requests
if (requestToken == globalToken) {
// Note: Refresh the Token using a separate Dio instance (avoid being repeatedly intercepted by the TokenInterceptor). Do not share the Dio instance with other requests.
return await locator<UserService>()
.refreshToken(UserUtil.user.user_name, UserUtil.user.user_sfzh);
}
return true;
});
// token refresh succeed
if (success) {
// Retry the request
return _retry(response);
}
// token refresh failed
showTipDialog(
'Error message'.'Failed to refresh user ticket, please restart application! ', NavigatorUtils.navigatorKey.currentContext);
return super.onResponse(response);
}
Copy the code
refreshToken
Future<bool> refreshToken(String userName, String idCardNo) async {
try {
Token token = await _doLogin(userName, idCardNo);
boolsucceed = token ! =null&& token.access_token ! =null;
if (succeed) {
UserUtil.token.access_token = token.accessToken;
}
return succeed;
} on FetchDataException catch (e, stackTrace) {
Log.e("Refresh user token failed. userName: $userName, idCardNo: $idCardNo", e,
stackTrace);
}
return false;
}
Copy the code
4. Resend the request
Once the Token refresh is done above, all you need to do is take the new Access_token and resend the request.
Future<Response<dynamic>> _retry(Response<dynamic> Response) {// RequestOptions options = response.request; // update token var data = options.data; data['head']['token'] = UserUtil.token.access_token; Return _dio.post(options.path, options: options, data: data, queryParameters: options.queryParameters); }Copy the code
5. Complete sample code
TokenInterceptor
import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:hbgaydsj/src/common/utils/navigator_utils.dart'; import 'package:hbgaydsj/src/common/utils/user_util.dart'; import 'package:hbgaydsj/src/locator.dart'; import 'package:hbgaydsj/src/modules/user/service/user_service.dart'; import 'package:hbgaydsj/src/ui/widgets/tip/tip_dialog.dart'; import 'package:queue/queue.dart'; class TokenInterceptor extends Interceptor { Dio _dio; bool isReLogin = false; Queue queue = new Queue(); TokenInterceptor(this._dio); @override Future onRequest(RequestOptions options) async { return options; } @override Future onResponse(Response response) async { bool needRefreshToken = _checkIfNeedRefreshToken(response); if (! needRefreshToken) { return super.onResponse(response); } / / / https://github.com/flutterchina/dio/issues/590 / reference Check for the if the token were successfully refreshed bool success = await queue.add(() async { // refreshTokens returns true when it has successfully retrieved the new tokens. // When the Authorization header of the original request differs from the current Authorization header of the Dio instance, // it means the tokens where refreshed by the first request in the queue and the refreshTokens call does not have to be made. var requestToken = response.request.data['head']['token']; var globalToken = UserUtil.token.access_token; // The token is consistent with the need to update the token, If (requestToken == globalToken) {return await locator<UserService>() .refreshToken(UserUtil.user.user_name, UserUtil.user.user_sfzh); } return true; }); // token refresh succeed if (success) { return _retry(response); } // Token refresh failed showTipDialog ', NavigatorUtils.navigatorKey.currentContext); return super.onResponse(response); } Future<Response<dynamic>> _retry(Response<dynamic> Response) {RequestOptions options = response.request; // update token var data = options.data; data['head']['token'] = UserUtil.token.access_token; return _dio.post(options.path, options: options, data: data, queryParameters: options.queryParameters); } / / / determine whether need to refresh Token bool _checkIfNeedRefreshToken (Response < dynamic > Response) {if (Response. Data = = null | | response.data.isEmpty) { return false; } var responseMap = response.data is String ? jsonDecode(response.data) : response.data; var head = responseMap['head']; if (head == null) { return false; } var statusCode = head['code']; if (statusCode ! = 99999 | | "did not find the login user!" " = responseMap['data']) { return false; } return true; }}Copy the code
6. Summary
When doing ticket refresh, most online schemes are realized based on DIO lock, but it is often found that the actual test cannot meet the requirements, and too much time is spent in this area.
In fact, once you clear your mind, using synchronous queues can also solve the problem quickly, and you don’t have to worry about most dio locking solutions.
7. About the author
The author is a programmer who loves learning, open source, sharing, spreading positive energy, and has a lot of hair. – Warmly welcome everyone to pay attention to, like, comment exchange!
- CSDN: blog.csdn.net/u010920692
- The Denver nuggets: juejin. Cn/user / 323739…
- Garden: blog www.cnblogs.com/zhangzhxb/