로그인 JWT(accessToken, refreshToken)
플로우
- websequencediagrams 스크립트
title JWT login process (evaluation-iic)
frontend->backend: login (/auth/access_token)
backend -> backend : 유저 확인 (access_token, refresh_token 발급)
backend->frontend: login 응답 (access_token, refresh_token)
frontend -> frontend : localStorage에 저장(access_token, refresh_token)
frontend-> backend: 데이터 요청 (ex) GET /evaluation/group) with access_token
backend -> backend : access_token 검증
alt if access_token valid
backend -> frontend : 데이터 응답
else else access_token invalid
backend -> frontend : 401 Error 응답
frontend -> backend : 토큰 재발급 요청(/auth/refresh_token) with refresh_token
backend -> backend : refresh_token 검증
alt if refresh_token valid
backend -> frontend : 토큰 재발급 응답(access_token, refresh_token)
frontend -> frontend : localStroage 저장(access_token, refresh_token)
else else refresh_token invalid
backend -> frontend : 401 Error 응답
frontend -> frontend : 로그아웃 처리 및 redirect to login 페이지
사전준비
- msw 를 사용하여 401 응답을 가정
- axios 로 비동기 요청 처리
- axios instance 를 생성하여, interceptor 정의 하여 사용
- src/axiosInstance.js
import axios from 'axios'; const axiosInstance = axios.create({ baseURL: 'https://myapp-dev-api.myapp.com', headers: { 'Content-Type': 'application/json', }, });
- 로그인 로직에서는 pure axios를 사용 → 로그인 요청에는 access_token을 체크하지 않기 때문
import axios from 'axios';
axios.defaults.baseURL = 'https://myapp-dev-api.myapp.com';
// login 시는 accessToken 체크 안함
export const postLogin = ({ id, password }) => {
return axios.post('/auth/access_token', {
id,
password,
});
};
- 로그인 성공시 localStorage에 응답받은 토큰(access_token, refresh_token) 담기
const loginMutation = useMutation({
mutationFn: postLogin,
onSuccess: ({ data }) => {
setUserInfoState({
...data,
userId: formData.id,
accessLv: getAccessLv(data.scope),
});
localStorage.setItem('accessToken', data.access_token);
localStorage.setItem('refreshToken', data.refresh_token);
toast.success(`Logged in as ${formData.id}`, {
theme: 'colored',
});
navigate('/main');
},
onError: () => {
console.log('실패');
},
});
토큰 플로우 테스트
1. 토큰 테스트를 위한 임시 테스트 버튼 생성
- 버튼 온클릭 함수
- 데이터 get 요청 보내는 함수
- 로그인 이후 모든 request 에서는 axiosInstance 를 사용
const tokenTest = async () => {
const result = await axiosInstance.get('/mydata/question/apply/');
console.log(result);
};
2. axiosInstance.js 의 baseURL 을 잠시 localhost:3000으로 변경
- msw 사용하기 위해 변경
const axiosInstance = axios.create({
// baseURL: 'https://myapp-dev-api.myapp.com',
baseURL: 'http://localhost:3000',
headers: {
'Content-Type': 'application/json',
},
});
- axios 의 baseURL 도 localhost:3000으로 변경 → msw 사용하기 위해
axios.defaults.baseURL = '<http://localhost:3000>'
- 현재는 request에 interceptor를 통해서 access_token 을 헤더에 담기 처리
axiosInstance.interceptors.request.use(
function (config) {
// 요청시 AccessToken 을 헤더에 담기
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
} else {
localStorage.removeItem('user');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
return config;
},
function (error) {
// return request error
return Promise.reject(error);
}
);
3. msw handlers.js 에 작업
rest.get('/mydata/question/apply/', async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
result: false,
})
);
}),
- 일반적인 get 요청을 하는데, axiosInstance 로 요청을 하면, request.interceptor를 거쳐서 로컬 스토리지에 있는 access_token 을 헤더에 담아서 요청하게 됨
4. 버튼을 눌러서 정상 동작 확인
- header 에 access_token 이 담겨 있는 것을 확인
5. access_token이 만료되었다는 fake 응답을 리턴
- msw handlers.js 에 작업
- 이후 /auth/refresh_token 요청에서 msw 로 새로운 토큰을 ‘test-access-token’ 으로 발급해줄 예정
- 현재 토큰은 만료되었다고 가정, 이후 ‘test-access-token’이 유효한 토큰이라고 가정
- 일단 현재시점의 요청으로는 401을 리턴
rest.get('/mydata/question/apply/', async (req, res, ctx) => {
console.log('msw-get-request', req.headers.get('authorization'));
const authCheck = req.headers.get('authorization');
if (authCheck === 'Bearer test-access-token') {
return res(
ctx.status(200),
ctx.json({
result: false,
})
);
}
return res(ctx.status(401));
}),
6. response.interceptor 지정
- 401 받으면 바로 이어서 refresh_token으로 토큰 재발행 요청
- 200 성공시 localStorage 새로운 토큰으로 업데이트
- 기존에 요청했던 작업 재요청 → failedQueue에 담겨있는 작업 처리
let isRefreshing = false; // 중복 발행 방지 flag
let failedQueue = []; // 할 일 리스트
// 할 일 처리하는 큐
const processQueue = (error, token = null) => {
failedQueue.forEach((taskPromise) => {
if (error) {
taskPromise.reject(error);
} else {
taskPromise.resolve(token);
}
});
failedQueue = [];
};
axiosInstance.interceptors.response.use(
function (response) {
return response;
},
async (error) => {
const {
config,
response: { status },
} = error;
const originalRequest = config;
// 401 access 권한 에러 발생시
if (status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return axios(originalRequest);
})
.catch((err) => {
return err;
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem('refreshToken');
return new Promise((resolve, reject) => {
axios
.post('/auth/refresh_token', { refreshToken: refreshToken })
.then(({ data }) => {
localStorage.setItem('accessToken', data.access_token);
localStorage.setItem('refreshToken', data.refresh_token);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
resolve(axios(originalRequest));
processQueue(null, data.accessToken);
})
.catch((err) => {
processQueue(err, null);
reject(err);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
})
.then(() => {
isRefreshing = false;
});
});
}
// return other error
return Promise.reject(error);
}
);
- 로컬 스토리지 변화 확인 : accessToken / refreshToken 값 확인
- 요청 콘솔 로그
- 데이터 get 요청 (/evaluation/question/apply/) Failed 401
- 토큰 재발행 요청 (/auth/refresh_token) 200 성공
- 새로 발급받은 access_token 으로 데이터 get 재요청(/evaluation/question/apply/) 200 성공
7. 만약 refresh_token 도 만료라면?
- /auth/refresh_token 에서 401 을 또 주는 상황
- 그럼 axiosInstance.interceptors.response의 catch 에서 잡히기 때문에, 로컬 스토리지의 모든 토큰을 지우고 login 페이지로 리다이렉트
rest.post('/auth/refresh_token', async (req, res, ctx) => {
console.log('msw-refresh-token-request', req);
return res(ctx.status(401));
})
- 로컬 스토리지 확인 : accessToken, refreshToken 키값 삭제됨
8. 테스트 완료후 baseUrl 돌려놓기
'programming > Web' 카테고리의 다른 글
소셜 로그인 - 카카오 로그인 CORS 에러 발생할 때 - NextJS (frontend) (1) | 2024.02.07 |
---|---|
Frontend 관점 HTTP 응답 상태 코드(RESTful API) (1) | 2024.02.05 |
AWS amplify 로 React 배포하기 (+ blank 빈 페이지 보이는 에러 잡기) (0) | 2023.04.20 |
구글 로그인 라이브러리 변경 - 4. 라이브러리를 사용하지 않고 google OAuth 로 direct 요청 방식 구현 (1) | 2023.01.23 |
구글 로그인 라이브러리 변경 - 2. 구글 로그인 라이브러리에서 변경되는 내용 (0) | 2023.01.22 |