로그인 구현 Flow 이해하기

kyle kwon·2022년 11월 27일
0

React

목록 보기
12/15
post-thumbnail

Prologue

프로젝트에서 로그인 기능을 구현하게 되었습니다. 기본적인 flow 조차 알고 있지 못했기 때문에, 이 글은 여러가지 로그인 구현 과정 포스팅들을 읽고 참고하여 내용을 정리한 글입니다.



Login을 구현하는 방식?

  1. 세션 id를 이용하는 방식
  2. JWT(Json Web Token)을 이용하는 방식

일반적으로 XSS, CSRF 보안 공격 이슈 때문에 JWT를 이용하는 방식을 택하곤 합니다. JWT를 이용하는 방식도 완벽하진 않지만, 상대적으로 세션 id를 활용하는 방식보다 안전합니다.



JWT란?

JWTJson Web Token의 약자로 암호화된 데이터 패키지라고 할 수 있습니다.

JWT는 3가지 구성요소로 나뉩니다.
1. Header
2. Payload
3. Signature

Json 형태인 각 부분은 Base64URL 형식으로 인코딩 되어 표현됩니다.

.으로 구분하여 aaaa.bbbb.cccc 형식으로 표현됩니다.

JWT는 URL에서 파라미터로 활용할 수 있도록 URL_safe한 Base64URL 인코딩을 활용하는 것입니다.



Login Flow

  1. 유저가 이메일, 비밀번호를 입력하고 제출 버튼을 눌러 로그인을 시도합니다.
  2. 서버가 인증정보(accessToken, refreshToken, etc...)을 암호화된 데이터 패키지, 즉 JWT를 생성하고, DB에 {ID, RefreshToken}으로 저장하며, 클라이언트로 보냅니다.
  3. 받아온 인증정보 중 accessToken, refreshToken유저 인증에 사용되기 때문에 클라이언트 사이드에서 저장해 둡니다.

    accessToken은 웹 브라우저 내 로컬변수, refreshToken은 Http 응답헤더 중 set-Cookie를 통해 받아와 cookie에 안전하게 저장합니다. 서버에 저장할 경우 DB 또는 Redis 같은 저장소에 저장합니다)

  4. 유저에게만 보여줄 수 있는 정보에 접근할 때 accessTokenrequest header에 담아 서버에 보냅니다.
  5. 서버는 받은 accessToken이 유효한 지 유효성 검사를 합니다.


실질적인 인증 정보는 accessToken이라고 할 수 있습니다.
accessToken은 무한히 유지되지 않는데요. 다음과 같은 경우에 만료됩니다.

💡 일정 시간이 지난다.
💡 페이지가 Refresh
💡 브라우저 창을 닫는다.

하지만, refreshToken을 활용하면 로그인을 지속적으로 유지할 수 있습니다. refreshToken을 서버에 보내면 새로운 accessToken을 받는 구조입니다.

로그인 과정에서 인증 정보 중 accessToken 만을 받는 것이 아닌 refreshToken서버에서 전달하는 이유는 accessToken은 새로이 계속 갱신되기 때문에, 만료되었다는 것을 인지하기 위해서 refreshToken을 같이 보내주는 것입니다. 이 refreshToken을 통해 새로운 accessToken을 받을 때, refreshToken도 새로이 갱신되어 인증 정보 안에 같이 담겨 클라이언트 사이드로 넘어오는 흐름입니다.

참고로 refreshToken 사용여부는 Option 입니다.



인증정보 저장방식

클라이언트 사이드에서 이 인증 정보를 받는데, 이 때 이 정보를 브라우저에 저장하는 방식은 3가지가 있습니다.

01 LocalStorage

브라우저 저장소에 저장하며 JS 내 글로벌 변수로 읽기/쓰기 접근이 가능합니다.(setItem, getItem)

브라우저에 쿠키로 저장하며 클라이언트가 HTTP 요청(GET/POST ...)을 보낼 때마다 자동으로 쿠키가 서버에 전송됩니다. JS 내 글로벌 변수로 읽기/쓰기 접근이 가능합니다.

브라우저에 쿠키로 저장하지만, JS 내 접근이 불가능합니다. secure를 적용하면 https 접속에서만 동작합니다.

위에서 언급했듯 1) 세션 id를 사용하는 방식 또는 2) JWT를 사용하는 방식 모두 완벽하지 않고, 보안 이슈가 존재합니다. 마찬가지로, 인증정보 저장방식에도 보안 이슈가 존재합니다.

  1. input 태그에서 입력된 값이 html/Javascript로 인식되지 않도록 서버에서 escape 처리를 합니다.
  2. url을 통해 Javascript가 수행되지 않도록 라우팅을 꼼꼼하게 관리합니다.
    c.f) React는 공격자가 string에 html/Javascript를 담아 JSX에 삽입할 경우 자동으로 escape 처리합니다.


보안 이슈에 대응한 최선의 로그인 구현 Flow

최선이라고 표현한 부분은 참조한 글에서 정리한 최선의 방법을 제시한 것에 동의하여 아래와 같이 정의합니다.

  1. JWT를 이용한 인증 방식을 활용합니다.
  2. refreshTokensecure Cookie 또는 HttpOnly Cookie에 저장해 CSRF 보안 공격을 방어합니다.
  3. accessToken은 웹 브라우저 내 로컬 변수에 저장하여 사용합니다. (만료 시간이 있으므로 갱신됩니다)
  4. 클라이언트에서 API requestaccessTokenAuthorization Header에 담아 보냅니다.

백엔드는 HTTP response Set-Cookie 헤더에 refreshToken 값을 설정하고 accessToken 을 JSON payload에 담아 보내줘야 한다.

아래의 코드는 메인 로직을 간략화한 코드입니다.


// 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에 withCredentialstrue로 설정해야 refreshToken cookie를 주고 받을 수 있습니다.



로그인 만료되기 전 또는 페이지 리로드 시 로그인 연장하는 Flow

위의 다이어그램에서 왜 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);
}

페이지 Refresh 될 때 로그인 연장하는 리액트 코드

// 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();
}, []);



Conclusion

이렇게 어떻게 로그인 구현이 이루어지는지 개념과 보안이슈, 그리고 로그인 Flow 등에 대해서 정리해보았습니다. 직접적으로 제 코드에 녹여 구현해보지는 않은 단계이지만, 개념이라도 머릿속으로 정리되면, 프로젝트에 적용 시 큰 그림을 그린 이해하기 글을 통해 클라이언트 사이드에서 로그인 구현을 잘 해볼 수 있지 않을까 기대합니다.



참조

  1. https://code-machina.github.io/2019/07/29/HTTP-Header-Summary-Part-1.html

  2. https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글