요즘도 강의를 꾸준히 듣고 있는데 최근 Dio에 대해 정리 했을당시 중요하다고 생각했던 interceptor와 최근 공부한 retrofit을 사용하여 토큰을 관리하는 부분에 대해 배웠다. 중요한 부분 같아서 배운내용에 대해 한번 정리를 하고 가려고 한다.
우선 이번정리의 중심인 interceptor에 대해서 간단히 정리해보고 코드를 자세히 정리해 보겠다.
Dio Interceptor
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 요청 전처리
// 예: 헤더 추가, 토큰 갱신
return handler.next(options);
},
onResponse: (response, handler) {
// 응답 후처리
// 예: 응답 로깅, 에러 처리
return handler.next(response);
},
onError: (DioError e, handler) {
// 에러 처리
// 예: 에러 로깅, 다시 시도
return handler.next(e);
},
));
dio interceptor의 기능들이다. 응답,에러는 모두 요청에대한 처리로 RequestOptions로 요청에 접근이 가능하다. 위 기능들을 사용하여 토큰을 관리하는 코드를 정리해보겠다.
코드 정리
Future<List<RetrofitSampleModel>> getRepositoryData() async {
final dio = Dio();
// 인터셉터 추가
dio.interceptors.add(CustomInterceptor(storage: storage));
final repository = CommonRepository(dio);
return repository.getData();
}
우선 retrofit을 사용하여 api 통신을 통해 데이터를 가져올 메소드이다. dio interceptor를 통하여 api 통신 전,후,에러 상황의 처리를 위한 interceptor를 추가해주었다. 다음은 interceptor를 작성해보겠다.
import 'package:delivery_app/common/const/data.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class CustomInterceptor extends Interceptor {
final FlutterSecureStorage storage;
CustomInterceptor({required this.storage});
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
print('[REQ] [${options.method}] ${options.uri}');
// 보내려는 요청의 헤더
if (options.headers['accessToken'] == 'true') {
// 헤더 삭제
options.headers.remove('accessToken');
// 실제 토큰 대체
final token = await storage.read(key: ACCESS_TOKEN_KEY);
options.headers.addAll({'authorization': 'Bearer $token'});
}
// 보내려는 요청의 헤더
// if (options.headers['refreshToken'] == 'true') {
// 헤더 삭제
// options.headers.remove('refreshToken');
// 실제 토큰 대체
// final token = await storage.read(key: REFRESH_TOKEN_KEY);
// options.headers.addAll({'authorization': 'Bearer $token'});
// }
// TODO: implement onRequest
return super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print(
'[REP] [${response.requestOptions.method}] ${response.requestOptions.uri}');
// TODO: implement onResponse
return super.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// AccessToken 만료시 RefreshToken으로 재발급
print('[ERR] [${err.requestOptions.method}] ${err.requestOptions.uri}');
final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY);
if (refreshToken == null) {
return handler.reject(err);
}
final isStatus401 = err.response?.statusCode == 401;
final isPathRefresh = err.requestOptions.path == '/auth/token';
// accessToken을 필요로 한다면
if (isStatus401 && !isPathRefresh) {
final dio = Dio();
// AccessToken 재발급
try {
final resp = await dio.post(
'http://$testApi/token',
options: Options(
headers: {'authorization': 'Bearer $refreshToken'},
),
);
// 재발급 받은 AccessToken 등록
final accessToken = resp.data['accessToken'];
final options = err.requestOptions;
options.headers.addAll({'authorization': 'Bearer $accessToken'});
await storage.write(key: ACCESS_TOKEN_KEY, value: accessToken);
final response = await dio.fetch(options);
return handler.resolve(response);
}
catch (e) {
return handler.reject(err);
}
}
return handler.reject(err);
}
}
api통신 요청전처리 메소드이다. onRequest()를 이용하여 요청시 options를 통해 headers에 강제로 넣어준 'accessToken' : 'true' 또는 'refreshToken' : 'true' 라는 값이 확인하여 값이 있다면 삭제하고 , 실제 토큰을 내부storage에서 가져 와서 authorization : 'Bearer $Token'으로 변경하여 토큰 관리를 수월하게 할 수있다.
api통신 요청후처리 메소드이다. onResponse()를 이용하면 토큰이 정상적이어서 , 요청이 성공적으로 처리 되었는지를 알수 있다.
api통신 에러처리 메소드이다. onError()를 통하여 accessToken만료를 확인할수 있다. 특정 작업 요청시 AccessToken이 만료되어 error 발생시 , refreshToken을 이용하여 재발급 하게 되고 , 에러 발생에대한 RequestOptions를 통헤 options의 헤더에 접근하여 accessToken을 바꿔주고 , 내부 storage의 accessToken도 바꿔준다. 그렇게 되면 재요청시 accessToken이 정상이 되어 요청이 정상적으로 처리 된다.
dio interceptor를 통하여 토큰의 관리를 하기위해 특정 api 요청시 어노테이션 Headers를 통하여 강제로 'accessToken' : 'true' 를 넣어준다.
@RestApi(baseUrl: 'https://test.api.com')
abstract class CommonRepository {
factory CommonRepository(Dio dio, {String baseUrl}) = _CommonRepository;
@GET("/posts")
@Headers({'accessToken': 'true'})
Future<List<RetrofitSampleModel>> getData(
@Query("_page") int page,
@Query("_limit") int limit,
);
}
이론으로 정리했던 로그인시 , 토큰 관리(토큰 만료,토큰재발급)에 대해서 dio와 retrofit를 통해 코드로 구현해 보았다. 이론으로만 정리 했을때 어떻게 사용해야 되는지 감이 잘 오지 않았는데 interceptor를 통하여 요청전,응답,에러 과정을 쪼개어 구현해보니 훨씬 이해가 쉬웠다.
참고
https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%A4%EC%A0%84/dashboard