-
Notifications
You must be signed in to change notification settings - Fork 3
로그인 관련 토큰 관리 전략
‘Ask-It’ 에서 클라이언트의 로그인 관련 토큰, 즉 Access Token과 Refresh Token을 관리하는 전략에 대해 기술합니다.
‘Ask-It’ 에서는 사용자 인증이 필수적인 여러 기능이 존재합니다. 예를 들어, 세션 생성하기나 참여했던 세션 기록 보기 등이 있습니다.
이를 위해 ‘Ask-It’ 에서는 토큰 기반 인증 방식을 사용하며, 다음과 같은 요구 사항이 있었습니다.
- Access Token의 유출 가능성을 최소화해야 합니다.
- 유효기간 만료 시 자동으로 Access Token을 갱신해 사용자의 끊김 없는 경험을 보장해야 합니다.
- 여러 API 요청마다 반복되는 인증 로직을 간소화해야 합니다.
따라서 이를 해결하기 위해 최적화된 토큰 관리 전략을 설계했습니다.
Access Token을 인메모리에서 관리하는 방식과 스토리지를 이용하는 방식에는 차이가 있습니다. 여기에서 보안성과 사용 편의성의 균형을 고려해 선택했습니다.
Access Token을 메모리에서만 유지하며, 브라우저가 종료되면 토큰도 사라지는 방식입니다.
-
장점
- XSS 공격으로부터 안전합니다.
- Access Token이 JavaScript 코드에서 접근 가능한 스토리지에 저장되지 않으므로 탈취될 위험이 낮아집니다.
- 생명주기
- 토큰이 메모리에서만 유지되므로 브라우저 탭이나 창을 닫으면 자동으로 사라져 세션이 종료됩니다. 이를 통해 해당 데이터가 브라우저에 장기적으로 저장되지 않습니다.
- XSS 공격으로부터 안전합니다.
-
단점
- 유지
- 브라우저가 새로고침을 하거나 페이지를 닫으면 메모리도 초기화되므로 Access Token을 잃게 됩니다.
- Refresh Token을 활용해 새로고침 시 토큰을 다시 가져오는 추가 로직이 필요합니다.
- 복잡성
- 새로고침과 같은 상태 초기화 상황을 처리하려면 Refresh Token을 사용해 Access Token을 재발급받는 로직이 필요합니다.
- 유지
Access Token을 localStorage
또는 sessionStorage
와 같은 브라우저 저장소에 저장하는 방식입니다.
-
장점
- 편의성
- 새로고침, 브라우저 닫기 후 재열기 시에도 Access Token이 유지됩니다.
- Refresh Token 요청 없이 바로 API 요청이 가능하므로 사용자 경험이 부드럽습니다 (Access Token을 캐싱하는 듯한 효과).
- 간단한 구현
- 브라우저 저장소에서 바로 읽어올 수 있어 추가적인 상태 관리 로직이 필요 없습니다.
- 상태 초기화를 신경 쓸 필요 없이 저장소에 의존하면 됩니다.
- 편의성
-
단점
- 보안 취약점
- 위에서 언급한 내용처럼, XSS 공격에 의해 토큰이 탈취될 수 있습니다.
- 성능
- 스토리지에서 검색하고 읽고 쓰는 것은 느리지만, 사실 이 프로젝트에서 해당 행위가 많지는 않아서 큰 이유가 되지는 않았습니다.
- 보안 취약점
특징 | 인메모리 관리 | 스토리지 이용 |
---|---|---|
보안 | XSS에 강함 | XSS에 취약 |
유지 가능성 | 새로고침 및 브라우저 종료 시 토큰 초기화 | 새로고침 및 종료 후에도 유지 |
편의성 | Refresh Token 기반 토큰 재발급 필요 | 간단한 구현 가능 |
사용 사례 | 보안이 최우선인 애플리케이션 | 사용자 경험이 더 중요한 경우 |
따라서, ‘Ask-It’ 의 프론트엔드에서는 인메모리 + Refresh Token 기반 관리를 선택했습니다.
-
Access Token은 메모리에서 관리
- 보안을 위해 Access Token은 메모리에 저장하여 XSS 공격에 대비합니다.
-
Refresh Token은 HTTP-Only 쿠키로 관리
- 브라우저에서 접근할 수 없는 쿠키로 Refresh Token을 저장하여 CSRF 공격 방지와 세션 연속성을 유지합니다.
-
새로고침 처리
- 새로고침 시 Refresh Token을 통해 Access Token을 다시 발급받고 전역 상태를 복구합니다.
토큰 관리와 갱신을 위해 일관된 구조를 설계하고 구현했습니다.
‘Ask-It’ 에서는 전역 상태로 토큰을 관리합니다. AuthStore
라는 스토어를 하나 만들고, 그 내부에 Access Token 값과 현재 로그인 상태인지를 알 수 있는 액션을 추가했습니다.
interface AuthStore {
userId?: number;
accessToken?: string;
isLogin: () => boolean;
setAuthInformation: ({
userId,
accessToken,
}: {
userId?: number;
accessToken?: string;
}) => void;
clearAuthInformation: () => void;
}
- 로그인시에는
setAuthInformation
이라는 액션을 통해 유저 아이디와, 토큰 값을 저장하도록 했습니다. - 로그아웃시에는
cleareAuthInformation
이라는 액션을 통해 메모리에서 유저와 관련된 상태를 제거합니다.
Refresh Token은 서버에서 HTTP-Only 쿠키로 설정되어 클라이언트가 직접 접근하지 못합니다.
새로고침이나 초기 진입시 항상 Refresh Token의 여부와 상관없이 서버로 토큰 재발급을 시도하도록 설계를 했습니다.
beforeLoad: () => {
const { isLogin, setAuthInformation } = useAuthStore.getState();
if (!isLogin())
refresh()
.then((res) => {
setAuthInformation(res);
})
.catch(console.error);
},
- 위와 같은 방식으로 새로고침되거나 새롭게 들어오는 경우 렌더링 전 서버로 토큰 재발급을 시도하고 전역 상태를 업데이트합니다.
- 이러한 방식으로 라우팅으로 접근할 수 있는 페이지에서 해당 경우를 처리하고, 전역 상태를 복구해야 하는 경우 해당 로직들이 추가로 포함되어 있습니다.
‘Ask-It’ 프론트엔드 개발에서는 axios
를 사용하고 있습니다. 이는 요청, 응답을 받았을 때 특정 작업을 추가하기 위함입니다. 인터셉터를 활용하기 위해 사용하고 있습니다.
import axios from 'axios';
import { useAuthStore } from '@/features/auth';
import { PostRefreshResponseDTO } from '@/features/auth/auth.dto';
axios.interceptors.request.use(
(config) => {
const nextConfig = { ...config };
const { accessToken } = useAuthStore.getState();
if (accessToken) nextConfig.headers.Authorization = `Bearer ${accessToken}`;
return nextConfig;
},
(error) => Promise.reject(error),
);
axios.interceptors.response.use(
(response) => response,
async (error) => {
const {
config,
response: { status, data },
} = error;
if (status === 401 && data.message === '유효하지 않은 액세스 토큰입니다.') {
const originalRequest = config;
const response = await fetch('/api/auth/token', {
method: 'POST',
credentials: 'include',
});
const { accessToken, userId } =
(await response.json()) as PostRefreshResponseDTO;
const { setAuthInformation, clearAuthInformation } =
useAuthStore.getState();
if (accessToken) {
setAuthInformation({ accessToken, userId });
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
}
clearAuthInformation();
}
return Promise.reject(error);
},
);
- 위 코드에서 모든 요청 시에 전역 상태에 접근해서 Access Token이 있다면 해당 토큰을 삽입하고 요청을 전송하고 있습니다.
- ‘Ask-It’ 은 익명 사용자도 있기 때문에, 토큰 없이도 많은 API가 정상적으로 동작해야 하므로 데이터의 유무에 따라 토큰을 삽입하는 정도로 처리하고 있습니다.
- 이후 응답에서 Access Token의 만료로 인한 실패 시 토큰 재발급을 1회 시도하고 성공하면 같은 요청에 헤더를 새롭게 추가하여 다시 시도합니다. 만약 재발급에 실패하면 현재 로그인 상태를 모두 지우고 에러를 그대로 외부로 반환하고 있습니다.
- 이 코드를 통해 로그인, 로그아웃 시 결과를 전역 상태에 담기만 하면 모든 요청과 응답에 대해 일관된 처리를 할 수 있었습니다.
-
보안성 강화
- 브라우저 저장소를 사용하지 않아 Access Token의 유출 가능성을 줄였습니다.
- Refresh Token은 HTTP-Only 쿠키로 설정해 CSRF 공격으로부터 안전하게 보호했습니다.
-
사용자 경험 개선
- 토큰 갱신과 전역 상태 복구를 자동화하여 사용자 세션이 끊기지 않는 매끄러운 경험을 제공했습니다.
-
개발 효율성 증가
-
axios
인터셉터를 활용해 인증 로직을 일관되게 구성했습니다. - API 호출에는 인증 및 인가와 관련된 로직이 제거되어 쉽게 개발할 수 있었습니다.
-