해당 참고자료를 주로 참고하여 코드 및 글을 작성했음을 밝힙니다! :)
어쩌다 보니 프로젝트에서 인증 부분을 거의 다 담당하게 되어..
프런트엔드에서 interceptors
까지 구현해 보게 되었다.
axios 문서에는 다음과 같이 설명이 되어 있다.
then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다.
요청이나 응답이 도착하기 전, 이를 intercept(가로채서)하여 가공하는 과정을 거칠 수 있게 해 주는 것인 듯!
이번 프로젝트에서는,
토큰을 headers에 담아 요청을 보냈는데, 토큰이 만료되어 401 Unauthrorized
에러가 돌아올 경우,
해당 응답을 가로채서, 토큰을 갱신하는 로직을 거친 후 다시 요청을 보내주는 데 사용해 볼 예정!
글의 순서는
0. utility/auth.js에 작성한 토큰 갱신 함수
1. setInterceptors 함수 - request
2. setInterceptors 함수 - response
3. setInterceptors 전체 코드
4. index.js에서 axios instance 넣어주기
5. axios 요청에 적용하기
로 해 볼 예정!
우선, 프런트엔드 단에서, 기존에 있던 utility
디렉토리 아래에 interceptors
라는 디렉토리를 생성해 주었다.
코드 정리에 앞서, 본 프로젝트는 프런트엔드에서 React-Native
를 사용 중이라,
interceptors.js
파일을 작성할 때, localStorage
가 아닌 AsyncStorage
로 작성하였다는 점,
그리고, interceptors
로직 부분은 setInterceptors
라는 함수 안에
두 경우를 구분해서 작성하였다는 점을 밝히고 시작하는 게 좋겠다!
해당 파일에는, 인증 관련하여 프런트엔드 단에서 사용하는 함수들을 작성해 두었다.
tokenRefresh
말고도 작성해 둔 함수들이 있지만,
이번 글에서는 tokenRefresh
만을 사용하므로 해당 함수만 글에 작성하였다!
import AsyncStorage from "@react-native-async-storage/async-storage";
import LOCAL_HOST from "../../features/local.js";
import axios from 'axios';
export const tokenRefresh = async (accessToken, refreshToken) => {
const refresh = await axios(`http://${LOCAL_HOST}:3000/auth/token/refresh`, {
method:"POST",
headers:{
Accept:"application/json",
Authorization: `Bearer ${accessToken}`,
Refresh:refreshToken,
}
}).then((res) => {
console.log('res.data', res.data);
return res.data;
}).catch((error) => {
console.log('Token refresh error', error.message);
return error;
})
return refresh;
}
Expo
를 개발 과정에서 사용 중이라,
외부 기기에서 요청을 보내는 경우 로컬호스트의 IP 주소를 명시해 주어야 하는 것 같은데,
다른 IP에 연결할 때마다 여러 군데에서 IP 주소를 바꿔주기는 번거로워서
로컬 호스트 주소만을 작성하는 별도 파일 local.js
를 두어 관리 중이다!
tokenRefresh
함수는 accessToken
과 refreshToken
을 받아서
headers에 담아 토큰을 갱신하는 api에 요청을 반환하고, 반환값을 돌려주는 함수이다!
백엔드 단에서 실제로 토큰을 갱신하는 로직은 #4. JWT 사용하기 (2)에 작성해 두었다 :)
이제 tokenRefresh
함수를 사용한, 본격적인 interceptors
작성하기!!
import AsyncStorage from "@react-native-async-storage/async-storage";
import {tokenRefresh} from '../auth.js';
import axios from 'axios';
export default function setInterceptors(instance) {
instance.interceptors.request.use(
async (res) => {
const access = await AsyncStorage.getItem('accessToken');
const refresh = await AsyncStorage.getItem('refreshToken');
if (access) {
res.headers['Authorization'] = `Bearer ${access}`;
}
const newConfig = {...res};
newConfig.url = `${res.url}`;
return newConfig;
}
);
request 쪽은 원래 따로 작성 안 하려다가 에러가 나길래 디버깅 과정에서 제대로 이해를 못하고 따라 친 것에 가까워서.. 조금 더 정리를 하고 내용 작성할 예정
instance.interceptors.response.use((response) => {
return response
}, async function(error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest.retry) {
const accessToken = await AsyncStorage.getItem('accessToken');
const refreshToken = await AsyncStorage.getItem('refreshToken');
const newData = await tokenRefresh(accessToken, refreshToken);
if (newData.status === 'New Access Token granted') {
originalRequest.retry = true;
AsyncStorage.setItem('accessToken', newData.tokens.access);
const content_regex = /"content":".+?"/
const latitude_regex = /"latitude":[0-9\.]+/
const longitude_regex = /"longitude":[0-9\.]+/
const originData = {
content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
}
originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
originalRequest.data = originData;
return axios(originalRequest);
} else {
return error;
}
}
return Promise.reject(error);
})
return instance;
}
request
로직과 동일하게, 응답이 정상적(2xx 범위에 있는 상태 코드)일 경우에는
첫 번째 인자로 받는 함수를 실행한다!
응답이 정상적일 경우에는 그대로 response
를 반환해 줌!
그렇지 않을 경우, 2번째 인자로 받는 함수를 실행하게 된다.
error에서 처음에 보낸 요청을 error.config
로 가져와서,
if (error.response.status === 401 && !originalRequest.retry)
해당 부분에서 응답 코드가 401인지, 다시 요청된 적 있는 응답인지 검사한다.
응답 코드가 401이고, interceptors
에 의해 다시 요청된 바가 없는 응답이라면
AsyncStorage
에서 accessToken
과 refreshToken
을 가져와
accessToken
을 갱신하는 요청을 보내준다.
위 요청이 정상적으로 처리되어, 새로운 accessToken
이 발급되었다면
originalRequest.retry = true;
재시도된 요청임을 표시해 주고,
AsyncStorage.setItem('accessToken', newData.tokens.access);
새로 발급된 accessToken
을 저장!
const content_regex = /"content":".+?"/
const latitude_regex = /"latitude":[0-9\.]+/
const longitude_regex = /"longitude":[0-9\.]+/
const originData = {
content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
}
아래 부분은 originalRequest
의 데이터를,
다시 보낼 요청에 포함시켜 주는 과정에서..
originalRequest
의 데이터가 왜인지 Object
가 아니라 string
타입이라..
정규표현으로 무식하게 데이터를 뽑아내는 과정에서 작성한 코드이다..
근데 다른 팀원이, 그냥 JSON 타입으로 파싱하면 될 거라고 해서 이 글 쓰고 나서 코드 수정하고..
글도 수정할 예정..ㅎㅎ
여튼 originalRequest
에
headers에는 신규 발급한 accessToken
을 넣어주고
data에는 data
를 넣어주고
이렇게 가공한 originalRequest
를 다시 반환해 주면 된다.
originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
originalRequest.data = originData;
return axios(originalRequest);
P. S. 그리고 setInterceptors
함수 자체는 꼭 마지막에 다음과 같이
interceptors를 적용한 instance를 반환해 줘야 한다..!!
return instance;
이거 빼먹었다가 undefined 에러 계속 났는데 이유를 한참 만에 알아버림..ㅠ
instance 반환을 포함한 전체 코드는 다음과 같다.
import AsyncStorage from "@react-native-async-storage/async-storage";
import {tokenRefresh} from '../auth.js';
import axios from 'axios';
export default function setInterceptors(instance) {
instance.interceptors.request.use(
async (res) => {
const access = await AsyncStorage.getItem('accessToken');
const refresh = await AsyncStorage.getItem('refreshToken');
if (access) {
res.headers['Authorization'] = `Bearer ${access}`;
}
const newConfig = {...res};
newConfig.url = `${res.url}`;
return newConfig;
}
);
instance.interceptors.response.use((response) => {
return response
}, async function(error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest.retry) {
const accessToken = await AsyncStorage.getItem('accessToken');
const refreshToken = await AsyncStorage.getItem('refreshToken');
const newData = await tokenRefresh(accessToken, refreshToken);
if (newData.status === 'New Access Token granted') {
originalRequest.retry = true;
AsyncStorage.setItem('accessToken', newData.tokens.access);
console.log(originalRequest);
const content_regex = /"content":".+?"/
const latitude_regex = /"latitude":[0-9\.]+/
const longitude_regex = /"longitude":[0-9\.]+/
const originData = {
content:content_regex.exec(originalRequest.data)[0].split(':')[1].slice(1,-1),
latitude:latitude_regex.exec(originalRequest.data)[0].split(':')[1],
longitude:longitude_regex.exec(originalRequest.data)[0].split(':')[1]
}
originalRequest.headers['Authorization'] = `Bearer ${newData.tokens.access}`;
originalRequest.data = originData;
return axios(originalRequest);
} else {
return error;
}
}
return Promise.reject(error);
})
return instance;
}
위에 작성한 setInteceptors
는 instance를 인자로 받으니,
axios instance를 생성해서 인자로 넣어 줘야 한다!
import axios from 'axios';
import LOCAL_HOST from '../../../features/local.js'
import setInterceptors from './interceptors.js';
const defaultURL = LOCAL_HOST;
function noAuth() {
const instance = axios.create({
baseURL: defaultURL + ':3000/'
});
return setInterceptors(instance);
}
export const noAuthInstance = noAuth();
인증이 되어 있지 않은 경우 사용하는 함수라 noAuth
인 듯!
백엔드 서버가 3000번 포트에서 열리기 때문에 로컬 호스트에 포트 번호 3000을 추가해서
baseURL
을 작성하고, axios instance
를 만들어 주었다.
그리고 setInterceptors
에 axios instance
를 넣은 반환값인,
interceptors
가 적용된 반환값을 noAuthInstance
로 보내줌!
...
const PostWrite = async () => {
console.log("Postwrite request sent");
if (checkIfTokenExists) {
const accessToken = await AsyncStorage.getItem("accessToken");
await noAuthInstance.post(`http://${LOCAL_HOST}:3000/drops`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
data: {
content,
latitude,
longitude,
},
})
.then((res) => {
console.log('response got', res);
console.log(`${res.data.data.content} 내용으로 ${res.data.msg}!`);
})
.catch((e) => console.log(e));
} else {
alert('유효한 사용자가 아닙니다.');
}
};
...
#4. 에서 export 해 준 axios instance
인 noAuthInstance
를 가져와서,
위와 같이 요청을 보내주면
interceptors 적용은 마무리 !!
구현하면서 필요한 내용은 거의 다 알았다고 생각했는데, 글로 정리하려니 막상 모르는 내용이 많다..ㅠ
이 글은 더 공부해서 수정해야 할 듯..!