이번 포스팅에서는 JWT 토큰을 이용한 벡엔드 통신 코드를 (견본) 직접 구현해보겠습니다.
직접 검증까지 해보고 싶지만 벡엔드 서버가 없어서 견본으로만 퉁치겠습니다.
본 포스팅에서는 구현을 다룰 것이기 때문에 JWT 통신은 설명은 다른 블로그 참고를 추천드립니다.
간단한 설명은 아래 이미지를 참고 바랍니다.
다만 여기서 JWT 발급 이후 사용자 서버에 API 요청을 보냈을 때 Access Token이 만료된 경우는 다음과 같은 과정을 통해 다시 API를 요청합니다.
1. Access Token이 만료되었을 시 이전에 발급 받은 Refresh Token을 가져온다
2. Refresh Token를 이용하여 서버에 Access Token을 재발급 API를 요청한다.
3-1. 재발급 실패 시 Refresh Token 또한 만료된 것이므로 로그인 화면으로 이동 시킨다.
3-2. 재발급 성공 시 처음 요청했었던 API를 다시 요청한다
구현을 위해 사용할 패키지는 다음과 같습니다.
1. flutter_secure_storage
- JWT 토큰을 안전하게 보관하기 위한 저장소 패키지입니다.
2. dio
- API를 요청할 때마다 JWT 토큰 처리 코드를 수동으로 추가 할 수 없기 때문에 해당 문제를 해결하기 위해 사용합니다.
class SecureStorage {
final FlutterSecureStorage storage;
SecureStorage({required this.storage});
/// 리프레시 토큰 저장
Future<void> saveRefreshToken(String refreshToken) async {
try {
await storage.write(key: 'REFRESH_TOKEN', value: refreshToken);
debugPrint('RefreshToken 저장 성공: $refreshToken');
} catch (e) {
debugPrint("RefreshToken 저장 실패: $e");
}
}
... 이후 생략
저장한 JWT 토큰들을 저장, 불러오는 SecureStorage 클래스입니다. 반복하여 사용되는 기능이기 때문에 따로 클래스로 선언하여 반복 사용하도록 하였습니다.
class DioFactory {
final SecureStorage storage;
DioFactory(this.storage);
Dio createDio() {
BaseOptions options = BaseOptions();
final dio = Dio(options);
dio.interceptors.add(TokenInterceptor(
storage: storage,
));
return dio;
}
}
class TokenInterceptor extends Interceptor {
final SecureStorage storage;
TokenInterceptor({required this.storage});
///요청 보내기 전
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
options.headers.remove('accessToken');
final accessToken = await storage.readAccessToken();
options.headers.addAll({
'Content-Type': 'application/json',
'authorization': 'Bearer $accessToken',
});
return handler.next(options);
}
///응답 시
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
return handler.next(response);
}
@override
void onError(DioException error, ErrorInterceptorHandler handler) async {
if (error.response?.data['statusCode'] == 404) {
debugPrint('dio 404 에러'); //엑세스 토큰 오류는 아닐 때
return handler.reject(error);
} else if (error.response?.data['statusCode'] == 401) {
debugPrint('dio 401 에러'); //엑세스 토큰 오류
final refreshToken = await storage.readRefreshToken();
if (refreshToken != null) {
Dio dio = Dio();
try {
final response = await dio.get(
'/getAccessToken',
options: Options(
headers: {
'Content-Type': 'application/json',
'authorization': 'Bearer $refreshToken',
},
),
);
debugPrint('리프레시 갱신 ${response.data['data']['accessToken']}');
final accessToken = response.data['data']['accessToken'];
final options = error.requestOptions;
// 요청의 헤더에 새로 발급받은 accessToken으로 변경하기
options.headers.addAll({
'authorization': 'Bearer $accessToken',
});
// 새롭게 발급 받은 엑세스 토큰으로 갱신
await storage.saveAccessToken(accessToken);
// 원래 보내려던 요청 재전송
final newResponse = await dio.fetch(options);
return handler.resolve(newResponse);
} on DioException catch (e) {
debugPrint('리프레시 토큰으로 토큰 갱신 실패 $e');
await Future.value([
storage.deleteToken(),
]);
return handler.reject(e);
}
}
return handler.reject(error);
}
debugPrint('dio 에러메시지 ${error.response}'); //데이터 X
return handler.reject(error);
}
}
가장 중요한 dio 코드입니다. SecureStorage를 주입받아 JWT 토큰을 관리하도록 하였습니다.
해당 코드는 3부분으로 나눠집니다.
1. API 요청전에 Header에 JWT 토큰을 첨부하는 onRequest
2. 응답 후 실행되는 onResponse (여기선 필요 없음)
3. Access Token가 만료 혹은 각종 이유로 API요청에 에러가 발생하였을 때 호출되는 onError
onError에서 발생하는 에러는 2가지 케이스가 있습니다.
1. 통신 자체에 오류가 발생한 경우
2. Access Token가 만료되어 오류가 발생한 경우
1번의 경우에는 알아서 잘처리하시면 됩니다. 저의 경우에는 권한이 없을 때 요청하면 에러를 서버에서 반환하도록해서 에러 코드에 따라서 View쪽에서 처리하였습니다.
JWT에 대한 포스팅이기 때문에 2번이 매우 중요하겠네요. (이후 설명에서 Token은 생략하도록하겠습니다)
Access 자체가 만료되어 통신에 실패한 경우 Refresh를 이용해서 재발급 받아야합니다.
재발급 받기위해 새로운 dio를 생성합니다. 이전 dio를 다시 사용하게 된다면 Refresh를 발급 받기 위해서 API요청을 보낼 때 onRequest에서 엑세서 토큰을 사용하게므로 안됩니다.
Access 재발급에 성공한 경우 이전 요청에 재발급 받은 Access를 첨부합니다.
그 후 return handler.resolve(newResponse);해당 코드를 이용해서 이전 API요청을 다시 보냅니다. 이러면 재요청 완료입니다.
다만 Access 재발급에 실패한 경우 Refresh 또한 만료된 케이스기 때문에 api 요청 자체가 실패한겁니다.
따라서 아래 코드가 실행됩니다.
on DioException catch (e) {
debugPrint('리프레시 토큰으로 토큰 갱신 실패 $e');
await Future.value([
storage.deleteToken(),
]);
return handler.reject(e);
}
JWT를 모두 삭제하고 에러를 반환합니다. 이때 만약 사용자를 로그인 화면으로 이동 시켜야하는 경우 해당 부분에 코드를 추가하시면 됩니다.
전체 코드는 링크 참고 부탁드립니다.
Flutter_Blog/lib/jwt at master · beomsuong/Flutter_Blog
Contribute to beomsuong/Flutter_Blog development by creating an account on GitHub.
github.com
사실 직접 실행하는 내용도 첨부하고 싶었는데 JWT용 벡엔드하기는 또 좀 그래서 아쉽네요....
잘못된 내용이나 추가적인 질문 있으면 댓글로 부탁드립니다.
'Flutter' 카테고리의 다른 글
위젯을 캡쳐하고 공유하기 (0) | 2024.07.04 |
---|---|
Flutter) env파일을 암호화가 될까? (0) | 2024.06.25 |
Flutter) 비동기 요청을 병렬로 처리하기 (0) | 2024.06.25 |
riverpod을 사용한 MVVM 예제를 작성해보자 (0) | 2024.06.22 |
Go_router에 URL 전달하기 (0) | 2024.06.05 |