본문 바로가기

Issue

JWT 토큰 자동 갱신: 안전한 인증 시스템 구축하기 (React)

JWT 토큰 기반 인증의 중요성 및 클라이언트 사이드에서의 관리

올해 대학교 구성원 및 외부 인원들과 함께 팀 활동으로 규모가 그리 크지 않은 시장을 타겟팅하는 새로운 플랫폼 구축을 진행하는 프로젝트를 진행하고 있다. 해당 프로젝트를 진행하며 웹의 클라이언트 사이드를 위주로 맡고 있고, 백엔드의 서브 개발자로 참여하고 있다.
이 프로젝트에서 마찬가지로 사용자 인증 및 로그인 관련 로직은 JWT 토큰 기반 인증 방식을 사용하게 되었고, 그 과정 속 발생한 두가지 문제에 대해 다뤄보려고 한다.

 

(이번 글에서는 JWT(JSON Web Token)에 대한 개념을 정리하기보단 다루면서 생겼던 이슈에 대한 해결 방법을 위주로 다뤄보려고 한다.)

 

문제정의

만료된 JWT 토큰으로 인한 사용자 경험 저하 문제

JWT 토큰은 보안과 효율성을 위해 일정 기간 후 만료되도록 설계되었다. 이는 토큰이 탈취되더라도 제한된 시간 동안만 유효하게 하여 잠재적인 보안 위협을 최소화하는 데 목적이 있다. 그러나, 이러한 토큰의 만료 메커니즘은 사용자가 서비스를 사용하는 도중에 갑자기 만료된 토큰을 통한 API 요청을 통해 인증 오류가 발생하여 중요한 작업이 중단되거나 사용자 경험을 저하시키는 문제를 발생시킬 수 있다. 그리하여 토큰 만료 기간이 짧은 Access Token장기적인 사용자 세션을 관리할 수 있는 Refresh Token을 사용하여 인증방식을 구현하였다. 이렇게 하여 클라이언트 사이드에서 사용자 경험을 저하시키지 않고, 어떻게 토큰의 refresh 로직을 수행시킬 수 있는지에 대한 고민이 필요했다.

 

보안상 취약점과 Refresh Token의 안전한 관리의 중요성

위와 같이 Refresh Token을 사용하여 장기적인 사용자 세션을 관리할 수 있도록 구성하였지만, Refresh Token이 탈취당한다면, 공격자는 사용자의 인증 정보를 장기간 도용할 수 있는 위험이 있다. 따라서, Refresh Token을 안전하게 관리하는 것이 애플리케이션 보안 유지의 핵심적인 요소이다.

 

(기존에 진행했던 프로젝트에서도 몇번 JWT인증 방식을 사용하였지만 백엔드 개발로써 참여하게 된 프로젝트에서는 클라이언트 사이드에서의 보안 유지 방식에 대해 크게 알아보지 못했었고, 기존에 프리랜서 계약을 통해 회사와 진행했던 프로젝트는 사내 서버 시스템에 사용될 예정이라 외부의 공격자에 대해 크게 고려하지 않고 로컬 스토리지를 사용하여 개발을 진행하여, 이번 프로젝트를 진행하며 클라이언트 사이드에서의 JWT 토큰의 관리 방식에 대한 새로운 문제 정의를 해보게 되었다.)

 

해결과정 : JWT 토큰 자동 갱신 및 관리 전략

자동 토큰 갱신 로직의 구현

보안과 사용자 경험을 모두 고려한 JWT 토큰 관리 방안의 핵심은, Access Token의 만료를 효율적으로 처리하는 자동 갱신 로직이다. Axios 인터셉터를 활용하여 Access 토큰이 만료되기 전에 자동으로 Refresh Token을 사용해 새로운 Access Token을 요청하는 프로세스를 구현할 수 있었다. 이 접근 방식을 통해 애플리케이션의 API 요청 로직과 밀접하게 동작하여, 토큰 만료로 인한 인증 오류 및 사용자 경험 저하 없이 항상 사용자 인증 상태를 유지할 수 있게 되었다.

 

별도의 토큰 갱신용 Axios 인스턴스

Access 토큰의 자동 갱신 과정에서는 별도의 토큰 갱신용 Axios 인스턴스를 사용하여, 기본 API 요청 인스턴스와의 충돌 및 재귀 동작을 방지하고 보안성을 강화하였다. 기존에는 Axios 인스턴스를 재활용하여 재귀 동작이 수행되거나, 헤더에 담기는 토큰 정보가 Access Token인지 Refresh Token인지 조작해야되는 등 많은 어려움이 존재했다. 이 별도의 인스턴스는 Refresh 토큰을 사용해 새 액세스 토큰을 요청하는 전용 경로로, API 요청 인터셉터에 의해 자동으로 호출되지 않도록 설정하였다. 이 방식을 사용하여 반복 요청과 잠재적인 재귀 호출 문제를 예방할 수 있었다.

 

간략한 코드 예시

// 수정 후: 별도의 토큰 갱신용 axios 인스턴스 사용
import axios from "axios";

// 공용 axios 인스턴스
const axiosClient = axios.create({
    baseURL: 'http://example.com/api',
});

// 토큰 갱신용 axios 인스턴스
const tokenRefreshInstance = axios.create({
    baseURL: 'http://example.com/api',
});

tokenRefreshInstance.interceptors.request.use((config) => {
	// RefreshToken을 헤더에 구성하는 로직 ..
});

axiosClient.interceptors.request.use(async (config) => {
	const User = // 사용자 인증 정보를 가져오는 로직
    if ( ... ) {
    	// 토큰 만료 시
        const newAccessToken = await refreshAccessToken(User.refreshToken);
        // refreshAccessToken 메서드에서는 토큰 갱신용 axios 인스턴스를 활용하여 요청 전송
        if (newAccessToken) {
            config.headers['Authorization'] = `Bearer ${newAccessToken}`;
        }
    } else {
        // 토큰이 만료되지 않았다면 Header에 기존 Access Token 활용
        config.headers['Authorization'] = `Bearer ${User.accessToken}`;
    }
    return config;
});

 

 

보안 고려사항

위에서도 언급했듯 Refresh Token의 안전한 관리는 JWT 인증 시스템의 보안성을 크게 좌우한다. 기본적인 개발을 진행함에 있어 이런 보안적인 측면을 꼼꼼하게 확인하고 정확하게 이해하고 넘어가는 것이 옳다고 생각하였고, 이를 해결할 방법을 모색하게 되었다.

먼저, 기존 클라이언트 사이드에서 AccessToken 및 Refresh Token은 Redux 스토어를 통해 애플리케이션 전반에 걸쳐 토큰 정보를 중앙집중식으로 관리함으로써 인증 관련 로직에 갱신된 토큰 정보를 즉시 반영할 수 있도록 구성해두었다. 하지만, Refresh Token이 Redux 스토어에서 관리되는 것은 보안적으로 너무 취약하다고 판단되었다. 클라이언트 사이드에서 관리할 경우에도 HttpOnly, Secure 쿠키 옵션을 사용해 XSS 및 CSRF 공격으로부터 보호하도록 구성하거나 아니면 아예 서버 사이드에서만 Refresh Token을 관리하는 방법도 괜찮을 것 같았다.  또한, Redis를 사용하여 만료 및 폐기(로그아웃 등)된 토큰에 대한 정보 관리를 철저하게 진행하고, 이상 징후가 감지될 경우 서버 사이드에서 Refresh Token을 즉시 폐기하고 재발급하는 로직도 필요할 것 같았다.

 

결론 및 요약

JWT 토큰의 자동 갱신 및 관리 전략은 애플리케이션의 보안과 자동 로그인과 같은 사용자 경험을 동시에 향상시키는 핵심 요소이다. Axios 인터셉터, 별도의 토큰 갱신용 인스턴스, Redux를 통한 상태 관리 방법을 적절히 조합하여 JWT 토큰의 자동 갱신 기능을 구현할 수 있었고, Refresh Token의 보안적인 측면을 위해 팀 내부적인 회의를 거쳐 Refresh Token를 서버 사이드에서 관리하게 하거나 클라이언트 사이드에서 안전하게 관리할 수 있는 방법을 구축하고 백엔드의 보안적인 로직을 추가하는 방향으로 진행될 것 같다.