-
Notifications
You must be signed in to change notification settings - Fork 0
JWT 토큰 재발급 효율적으로 처리하기 V2
Sieun Ju edited this page Dec 3, 2023
·
4 revisions
- 네트워크 통신하다가 특정 타임때 쓰레드 잠깐 중단하고 동작 처리이후 재개 하는걸 구현합니다.
- AKA, JWT 재발급 할때 가장 효율적으로 처리하도록 구현합니다.
- 단순 Authenticator 에서 401 (토큰 만료) 일때 API를 재발급 하는게 아닌 효율적으로 처리하도록 합니다.
- Authenticator 에서 처리하게 되면 다중 쓰레드 환경에서 재발급 API를 여러번 호출하는 이슈가 있음
- 작업한 UI 클래스는
NetworkV2Fragment
입니다.
- OkHttp Builder 에서 지원하는 함수들과 OkHttp Docs 참고하여 OkHttp 라이브러리 구조에 대해서 다이어그램을 완성합니다.
- 해당 기능에 필요한 API 를 개발합니다.
- JWT 발급 받는 API
- 서버에서 특정 시간 이후 API Response 주는 API 몇개 구성
- 기존 성능에 큰 지장이 없도록 코드 구성이후 성능 테스트를 진행합니다.
OkHttp 구조 | 로그 |
---|---|
![]() |
![]() |
- Interceptor와 Authenticator 은 멀티 쓰레드 환경에서 이미 Runnable "run" 하기 때문에 해당 클래스에서 처리하면 효율적으로 처리할수가 없습니다.
- 디테일한 Log를 찍어보면서 OkHttp 구조에 대해서 다이어 그램 완성 (상위 표 참고)
- Dispatcher 라는 곳에서 ThreadPoolExecutor 를 매개변수로 받는데 여기서 쓰레드를 제어하여 요구사항에 맞게 API 제어가 가능하도록 설계
- Custom ThreadPoolExecutor 를 생성 쓰레드 작업을 진행하기전 호출하는 함수인
beforeExecute
에서 제어 하도록 구현 - Java ThreadPoolExecutor 에서 제공하는 API 들은 오래간만에 자바 Docs 가서 참고
- 오라클 도큐먼트 예제
class PausableThreadPoolExecutor extends ThreadPoolExecutor {
private boolean isPaused;
private ReentrantLock pauseLock = new ReentrantLock();
private Condition unpaused = pauseLock.newCondition();
public PausableThreadPoolExecutor(...) { super(...); }
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
pauseLock.lock();
try {
while (isPaused) unpaused.await();
} catch (InterruptedException ie) {
t.interrupt();
} finally {
pauseLock.unlock();
}
}
public void pause() {
pauseLock.lock();
try {
isPaused = true;
} finally {
pauseLock.unlock();
}
}
public void resume() {
pauseLock.lock();
try {
isPaused = false;
unpaused.signalAll();
} finally {
pauseLock.unlock();
}
}
}
-
beforeExecute
에서 특정 시간, 특정 API 호출하고 정상적으로 받으면 다시 쓰레드 재개하도록 구현 (JWT Refresh API) - 클라이언트 입장에서 네트워크 환경은 제각각이라 특정 API 호출하는게 실패하면 다시 요청할수 있는
재시도 정책
을 추가 구현
- 기존
RxErrorHandlingCallAdapter
설정값 이슈- 네트워크 통신시 CacheThread 로 자동 처리되도록 하는 설정
RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())
을 했더니 ThreadPoolExecutor 에서 beforeExecute 호출이 안됨 단, 직접 구현한 코루틴은 해당 함수 호출하는 것으로 확인 - 해당 힌트를 얻어 내부 RxJava3CallAdapterFactory 코드 분석
- 네트워크 통신시 CacheThread 로 자동 처리되도록 하는 설정
RxJava3CallAdapter.java
@Override
public Object adapt(Call<R> call) {
Observable<Response<R>> responseObservable =
isAsync ? new CallEnqueueObservable<>(call) : new CallExecuteObservable<>(call);
// do something..
return RxJavaPlugins.onAssembly(observable);
}
- 상위 코드에서 isAsync 에 따라서
CallEnqueueObservable
orCallExecuteObservable
나뉨 - RxJava3CallAdapterFactory.createWithScheduler 로 처리하는 경우
isAsync
= false,
RxJava3CallAdapterFactory.create 로 처리하는 경우isAsync
= true 로 구성이 되고,
CallEnqueueObservable 와 CallExecuteObservable 는 OkHttp enque 와 execute 를 Rx 스타일로 처리하는 Wrapper 클래스임을 확인 - RxErrorHandlingCallAdapter 에
create()
함수로 변경하고 작업하는 구간에 subscribeOn 을 넣어서 네트워크 통신시 CacheThread 를 처리하도록 대응
- 요구조건에 맞게 기존 성능과 비슷해야함 (큰 저하는 이루어져서는 안됨)
- JWT 토큰이 만료 되기 N초 전에 API 통신하려던 쓰레드들 잠시 중단하고 JWT 토큰을 재발급 받은 이후 다시 재개하는지 확인
- JWT 재발급 API 는 특정 시간에 한번만 호출되어야 하며 재발급 토큰으로 HTTP Header 에 다시 셋팅되어 처리되는지 확인
- 쓰레드를 제어한다고 해서 Single Thread로 네트워크 통신하는 쓰레드를 제어해서는 안되며, API 를 병렬적으로 처리할때 제대로 구현되는지 확인
- 10분동안 JWT 관련 API를 병렬로 호출 했을때
Authenticator
로그가 찍히는지 확인 (절대 해당 로그가 찍히면 안됨) - 토큰 Refresh API 는 만료되기 5초 전에 호출 (빡쎈 테스트를 위함)
- JWT Refresh API를 5초 이후에 받을수 있도록 API 스펙 구성
router.post("/refresh", (req, res) => {
// Refresh Token..
// var token = req.header("Authorization");
var email = req.body.email;
var timeDelay = req.body.delay ? req.body.delay : 0;
var expiredTime = req.body.expiredTime ? req.body.expiredTime : "5m";
// 만료 1분
var refreshToken = jwt.sign(
{
type: "JWT",
nickname: email,
},
jwtSecret,
{
expiresIn: expiredTime,
issuer: "sieun ju",
algorithm: "HS256",
}
);
setTimeout(function () {
res
.status(200)
.send({
status: true,
data: {
payload: {
token: refreshToken,
},
},
})
.end();
}, timeDelay);
});
- 로그를 제대로 볼수 있게 tag 는 "Network_Test" 로 셋팅
- 주요 클래스 위치에 Log 처리
PauseAbleThreadPoolExecutor
,TokenAuthenticator
,HeaderInterceptor
,RefreshTokenInterceptor
로그 추가
- 주요 클래스 위치에 Log 처리
TokenAuthenticator.kt
Timber.tag("Network_Test").e("Authenticator ${req.url.encodedPath}")
PauseAbleThreadPoolExecutor.kt
Timber.tag("Network_Test").w("Dispatcher 토큰을 재발급합니다. ${t.name}")
HeaderInterceptor.kt
val str = StringBuilder()
str.appendLine("Code ${response.code} Request ${response.request.url.encodedPath} (${tookMs}ms)")
str.appendLine("Authorization: ${req.header("Authorization")}")
Timber.tag("Network_Test").d(str.toString())
RefreshTokenInterceptor.kt
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val str = StringBuilder()
str.append("Code ${response.code} Request ${response.request.url.encodedPath} (${tookMs}ms)")
Timber.tag("Network_Test").w(str.toString())
- JWT 토큰 만료시 HTTP Status Code 401 로 리턴하는 API 추가
router.get("/jwt/test", (req, res) => {
var token = req.headers.authorization;
let decoded = null;
try {
let timeDelay =
req.query.timeDelay !== null && req.query.timeDelay !== undefined
? req.query.timeDelay
: 0;
decoded = jwt.verify(token, jwtSecret);
setTimeout(function () {
res
.status(200)
.send({
status: true,
data: {
payload: {
message: "JWT Token Test",
},
},
})
.end();
}, timeDelay);
} catch (err) {
res
.status(401)
.send({
status: false,
message: err,
})
.end();
}
});