프로젝트에서 로그인 기능을 구현하게 되었습니다. 기본적인 flow 조차 알고 있지 못했기 때문에, 이 글은 여러가지 로그인 구현 과정 포스팅들을 읽고 참고하여 내용을 정리한 글입니다.
id
를 이용하는 방식일반적으로 XSS, CSRF 보안 공격 이슈 때문에 JWT를 이용하는 방식을 택하곤 합니다. JWT를 이용하는 방식도 완벽하진 않지만, 상대적으로 세션 id를 활용하는 방식보다 안전합니다.
JWT는 Json Web Token
의 약자로 암호화된 데이터 패키지라고 할 수 있습니다.
JWT는 3가지 구성요소로 나뉩니다.
1. Header
2. Payload
3. Signature
Json 형태인 각 부분은 Base64URL 형식으로 인코딩 되어 표현됩니다.
.으로 구분하여
aaaa.bbbb.cccc
형식으로 표현됩니다.
JWT는 URL에서 파라미터로 활용할 수 있도록 URL_safe한 Base64URL 인코딩을 활용하는 것입니다.
{ID, RefreshToken}
으로 저장하며, 클라이언트로 보냅니다.
accessToken
은 웹 브라우저 내 로컬변수,refreshToken
은 Http 응답헤더 중 set-Cookie를 통해 받아와cookie
에 안전하게 저장합니다. 서버에 저장할 경우DB 또는 Redis
같은 저장소에 저장합니다)
실질적인 인증 정보는 accessToken
이라고 할 수 있습니다.
accessToken은 무한히 유지되지 않는데요. 다음과 같은 경우에 만료됩니다.
💡 일정 시간이 지난다.
💡 페이지가 Refresh
💡 브라우저 창을 닫는다.
하지만, refreshToken을 활용하면 로그인을 지속적으로 유지할 수 있습니다. refreshToken을 서버에 보내면 새로운 accessToken을 받는 구조입니다.
로그인 과정에서 인증 정보 중 accessToken 만을 받는 것이 아닌 refreshToken을 서버에서 전달하는 이유는 accessToken은 새로이 계속 갱신되기 때문에, 만료되었다는 것을 인지하기 위해서 refreshToken을 같이 보내주는 것입니다. 이 refreshToken을 통해 새로운 accessToken을 받을 때, refreshToken도 새로이 갱신되어 인증 정보 안에 같이 담겨 클라이언트 사이드로 넘어오는 흐름입니다.
참고로 refreshToken 사용여부는 Option 입니다.
클라이언트 사이드에서 이 인증 정보를 받는데, 이 때 이 정보를 브라우저에 저장하는 방식은 3가지가 있습니다.
브라우저 저장소에 저장하며 JS 내 글로벌 변수로 읽기/쓰기 접근이 가능합니다.(setItem, getItem)
브라우저에 쿠키로 저장하며 클라이언트가 HTTP 요청(GET/POST ...)을 보낼 때마다 자동으로 쿠키가 서버에 전송됩니다. JS 내 글로벌 변수로 읽기/쓰기 접근이 가능합니다.
브라우저에 쿠키로 저장하지만, JS 내 접근이 불가능합니다. secure
를 적용하면 https 접속에서만 동작합니다.
위에서 언급했듯 1) 세션 id를 사용하는 방식 또는 2) JWT를 사용하는 방식 모두 완벽하지 않고, 보안 이슈가 존재합니다. 마찬가지로, 인증정보 저장방식에도 보안 이슈가 존재합니다.
- input 태그에서 입력된 값이 html/Javascript로 인식되지 않도록 서버에서 escape 처리를 합니다.
- url을 통해 Javascript가 수행되지 않도록 라우팅을 꼼꼼하게 관리합니다.
c.f) React는 공격자가 string에 html/Javascript를 담아 JSX에 삽입할 경우 자동으로 escape 처리합니다.
최선이라고 표현한 부분은 참조한 글에서 정리한 최선의 방법을 제시한 것에 동의하여 아래와 같이 정의합니다.
secure Cookie 또는 HttpOnly Cookie
에 저장해 CSRF 보안 공격을 방어합니다.Authorization Header
에 담아 보냅니다.백엔드는 HTTP response Set-Cookie 헤더에
refreshToken
값을 설정하고accessToken
을 JSON payload에 담아 보내줘야 한다.
Set-Cookie (http 응답 헤더의 일부분)
아래의 코드는 메인 로직을 간략화한 코드입니다.
// App.tsx
const onLogin = (email, password) => {
const inputData = {
email,
password
}
const refreshToken = {withCredentials : true};
axios.post(`${API_URL}/login`, inputData, refreshToken).then(res => {
const { accessToken } = res.data;
// header에 accessToken을 담아 보내도록 설정
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
return accessToken;
}).catch(error => {
console.error(error)
}
}
React root index.tsx
에서 axios에 withCredentials
를 true
로 설정해야 refreshToken
cookie를 주고 받을 수 있습니다.
위의 다이어그램에서 왜 refresh Token
을 set Cookie를 통해 저장하는 지 알 수 있습니다.
아래의 코드는 메인 로직을 간략화한 코드입니다.
// index.tsx
axios.defaults.baseURL = 'www.react.com' // 가정한 주소입니다.
axios.defaults.withCredentials = true; //refresh Token을 사용하기 위한 설정입니다. index.tsx에서 전역으로 axios에 default로 설정해주어도 됩니다.
// App.tsx
import axios from 'axios';
const JWT_EXPIRATION_TIME = 3600 * 1000 // 1시간을 s로 표현
const onLogin = (email, password) => {
const inputData = {
email,
password
}
axios.post(`${API_URL}/members/auth/login`, inputData)
.then(res => onLoginSuccess)
.catch(error => {
console.error(error)
}
}
const onRefresh = () => {
axios.post(`${API_URL}/members/auth/refresh-token`, inputData)
.then(onLoginSuccess)
.catch(error => console.error(error));
}
const onLoginSuccess = (res) => {
const { accessToken } = res.data;
// accessToken 설정
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
setTimeout(onRefresh, JWT_EXPIRATION_TIME);
}
// App.tsx
const onRefresh = () => {
axios.post(`${API_URL}/members/auth/refresh-token`, inputData)
.then(onLoginSuccess)
.catch(error => console.error(error));
}
const onLoginSuccess = (res) => {
const { accessToken } = res.data;
// accessToken 설정
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
setTimeout(onRefresh, JWT_EXPIRATION_TIME);
}
// App이 실행될 때마다 (생명주기 중 componentDidMount일 때) 다시 로그인 시도
useEffect(() => {
onRefresh();
}, []);
이렇게 어떻게 로그인 구현이 이루어지는지 개념과 보안이슈, 그리고 로그인 Flow 등에 대해서 정리해보았습니다. 직접적으로 제 코드에 녹여 구현해보지는 않은 단계이지만, 개념이라도 머릿속으로 정리되면, 프로젝트에 적용 시 큰 그림을 그린 이해하기 글을 통해 클라이언트 사이드에서 로그인 구현을 잘 해볼 수 있지 않을까 기대합니다.