-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix: example queued_interceptor_csrftoken.dart (#2134)
Provide a more suitable example for queued_interceptor_csrftoken.dart <!-- Write down your pull request descriptions. --> ### New Pull Request Checklist - [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/) - [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none - [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase) - [ ] I have added the required tests to prove the fix/feature I'm adding - [ ] I have updated the documentation (if necessary) - [x] I have run the tests without failures - [ ] I have updated the `CHANGELOG.md` in the corresponding package ### Additional context and info (if any) This example adds 2 different `QueuedInterceptorsWrapper` and the last one handles `onError` to update csrf token. --- fixes #2128
- Loading branch information
1 parent
d020224
commit 35533ac
Showing
1 changed file
with
153 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,92 +1,162 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
// ignore: dangling_library_doc_comments | ||
/// CSRF Token Example | ||
/// | ||
/// Add interceptors to handle CSRF token. | ||
/// - token update | ||
/// - retry policy | ||
/// | ||
/// Scenario: | ||
/// 1. Client access to the Server by using `GET` method. | ||
/// 2. Server generates CSRF token and sends it to the client. | ||
/// 3. Client make a request to the Server by using `POST` method with the CSRF token. | ||
/// 4. If the CSRF token is invalid, the Server returns 401 status code. | ||
/// 5. Client requests a new CSRF token and retries the request. | ||
import 'dart:developer'; | ||
|
||
import 'package:dio/dio.dart'; | ||
|
||
void main() async { | ||
final dio = Dio(); | ||
// dio instance to request token | ||
final tokenDio = Dio(); | ||
String? csrfToken; | ||
dio.options.baseUrl = 'https://seunghwanlytest.mocklab.io/'; | ||
tokenDio.options = dio.options; | ||
dio.interceptors.add( | ||
QueuedInterceptorsWrapper( | ||
onRequest: (options, handler) async { | ||
print('send request:path:${options.path},baseURL:${options.baseUrl}'); | ||
|
||
if (csrfToken == null) { | ||
print('no token,request token firstly...'); | ||
|
||
final result = await tokenDio.get('/token'); | ||
|
||
if (result.statusCode != null && result.statusCode! ~/ 100 == 2) { | ||
/// assume `token` is in response body | ||
final body = jsonDecode(result.data) as Map<String, dynamic>?; | ||
|
||
if (body != null && body.containsKey('data')) { | ||
options.headers['csrfToken'] = csrfToken = body['data']['token']; | ||
print('request token succeed, value: $csrfToken'); | ||
print( | ||
'continue to perform request:path:${options.path},baseURL:${options.path}', | ||
); | ||
return handler.next(options); | ||
/// HTML example: | ||
/// ``` html | ||
/// <input type="hidden" name="XSRF_TOKEN" value=${cachedCSRFToken} /> | ||
/// ``` | ||
const String cookieKey = 'XSRF_TOKEN'; | ||
|
||
/// Header key for CSRF token | ||
const String headerKey = 'X-Csrf-Token'; | ||
|
||
String? cachedCSRFToken; | ||
|
||
void printLog( | ||
int index, | ||
String path, | ||
) => | ||
log( | ||
''' | ||
#$index | ||
- Path: '$path' | ||
- CSRF Token: $cachedCSRFToken | ||
''', | ||
name: 'queued_interceptor_csrftoken.dart', | ||
); | ||
|
||
final dio = Dio() | ||
..options.baseUrl = 'https://httpbun.com/' | ||
..interceptors.addAll( | ||
[ | ||
/// Handles CSRF token | ||
QueuedInterceptorsWrapper( | ||
/// Adds CSRF token to headers, if it exists | ||
onRequest: (requestOptions, handler) { | ||
if (cachedCSRFToken != null) { | ||
requestOptions.headers[headerKey] = cachedCSRFToken; | ||
requestOptions.headers['Set-Cookie'] = | ||
'$cookieKey=$cachedCSRFToken'; | ||
} | ||
} | ||
|
||
return handler.reject( | ||
DioException(requestOptions: result.requestOptions), | ||
true, | ||
); | ||
} | ||
|
||
options.headers['csrfToken'] = csrfToken; | ||
return handler.next(options); | ||
}, | ||
onError: (error, handler) async { | ||
/// Assume 401 stands for token expired | ||
if (error.response?.statusCode == 401) { | ||
print('the token has expired, need to receive new token'); | ||
final options = error.response!.requestOptions; | ||
|
||
/// assume receiving the token has no errors | ||
/// to check `null-safety` and error handling | ||
/// please check inside the [onRequest] closure | ||
final tokenResult = await tokenDio.get('/token'); | ||
|
||
/// update [csrfToken] | ||
/// assume `token` is in response body | ||
final body = jsonDecode(tokenResult.data) as Map<String, dynamic>?; | ||
options.headers['csrfToken'] = csrfToken = body!['data']['token']; | ||
|
||
if (options.headers['csrfToken'] != null) { | ||
print('the token has been updated'); | ||
|
||
/// since the api has no state, force to pass the 401 error | ||
/// by adding query parameter | ||
final originResult = await dio.fetch(options..path += '&pass=true'); | ||
if (originResult.statusCode != null && | ||
originResult.statusCode! ~/ 100 == 2) { | ||
return handler.resolve(originResult); | ||
return handler.next(requestOptions); | ||
}, | ||
|
||
/// Update CSRF token from [response] headers, if it exists | ||
onResponse: (response, handler) { | ||
final token = response.headers.value(headerKey); | ||
|
||
if (token != null) { | ||
cachedCSRFToken = token; | ||
} | ||
} | ||
print('the token has not been updated'); | ||
return handler.reject( | ||
DioException(requestOptions: options), | ||
); | ||
} | ||
return handler.next(error); | ||
}, | ||
), | ||
); | ||
return handler.resolve(response); | ||
}, | ||
|
||
onError: (error, handler) async { | ||
if (error.response == null) return handler.next(error); | ||
|
||
/// When request fails with 401 status code, request new CSRF token | ||
if (error.response?.statusCode == 401) { | ||
try { | ||
final tokenDio = Dio( | ||
BaseOptions(baseUrl: error.requestOptions.baseUrl), | ||
); | ||
|
||
/// Generate CSRF token | ||
/// | ||
/// This is a MOCK REQUEST to generate a CSRF token. | ||
/// In a real-world scenario, this should be generated by the server. | ||
final result = await tokenDio.post( | ||
'/response-headers', | ||
queryParameters: { | ||
headerKey: '94d6d1ca-fa06-468f-a25c-2f769d04c26c', | ||
}, | ||
); | ||
|
||
if (result.statusCode == null || | ||
result.statusCode! ~/ 100 != 2) { | ||
throw DioException(requestOptions: result.requestOptions); | ||
} | ||
|
||
final updatedToken = result.headers.value(headerKey); | ||
if (updatedToken == null) { | ||
throw ArgumentError.notNull(headerKey); | ||
} | ||
|
||
FutureOr<void> onResult(d) { | ||
print('request ok!'); | ||
} | ||
cachedCSRFToken = updatedToken; | ||
|
||
/// assume `/test?tag=2` path occurs the authorization error (401) | ||
/// and token to be updated | ||
await dio.get('/test?tag=1').then(onResult); | ||
await dio.get('/test?tag=2').then(onResult); | ||
await dio.get('/test?tag=3').then(onResult); | ||
return handler.next(error); | ||
} on DioException catch (e) { | ||
return handler.reject(e); | ||
} | ||
} | ||
}, | ||
), | ||
|
||
/// Retry the request when 401 occurred | ||
QueuedInterceptorsWrapper( | ||
onError: (error, handler) async { | ||
if (error.response != null && error.response!.statusCode == 401) { | ||
final retryDio = Dio( | ||
BaseOptions(baseUrl: error.requestOptions.baseUrl), | ||
); | ||
|
||
if (error.requestOptions.headers.containsKey(headerKey) && | ||
error.requestOptions.headers[headerKey] != cachedCSRFToken) { | ||
error.requestOptions.headers[headerKey] = cachedCSRFToken; | ||
} | ||
|
||
/// In real-world scenario, | ||
/// the request should be requested with [error.requestOptions] | ||
/// using [fetch] method. | ||
/// ``` dart | ||
/// final result = await retryDio.fetch(error.requestOptions); | ||
/// ``` | ||
final result = await retryDio.get('/mix/s=200'); | ||
|
||
return handler.resolve(result); | ||
} | ||
}, | ||
), | ||
], | ||
); | ||
|
||
/// Make Requests | ||
printLog(0, 'initial'); | ||
|
||
/// #1 Access to the Server | ||
final accessResult = await dio.get( | ||
'/response-headers', | ||
|
||
/// Pretend the Server has generated CSRF token | ||
/// and passed it to the client. | ||
queryParameters: { | ||
headerKey: 'fbf07f2b-b957-4555-88a2-3d3e30e5fa64', | ||
}, | ||
); | ||
printLog(1, accessResult.realUri.path); | ||
|
||
/// #2 Make a request(POST) to the Server | ||
/// | ||
/// Pretend the token has expired. | ||
/// | ||
/// Then the interceptor will request a new CSRF token | ||
final createResult = await dio.post( | ||
'/mix/s=401/', | ||
); | ||
printLog(2, createResult.realUri.path); | ||
} |