JWT의 안전한 저장소

KyungminLee·2021년 4월 27일
2

쿠키와세션, JWT

목록 보기
3/3
post-thumbnail

회사 업무에서 통합검색 화면을 개발하던 도중, 로그인처리 인증(JWT) 와 최근검색어에 대해서 브라우저 스토리지에 저장하게 되었다. 최근검색어 같은 경우 상관 없지만 로그인 처리 인증(JWT)에 대해선 보안이 중요하다 생각하였고, 고민해 보게 되었다.

🙄1. JWT란

Json Web Token의 약자로 모바일이나 웹의 사용자 인증을 위해 사용하는 암호화된 토큰을 의미한다.
JWT 정보를 API 요청을 통해 request에 담아 사용자의 정보 확인 및 수정 등 개인적인 작업들을 수행할 수 있다.

😈2. 보안은 어떻게 뚫리는가?

XSS(Cross Site Scripting)

공격자(해커)가 클라이언트 브라우저에 Javascript를 삽입해 실행하는 공격이다. 다시 말하면 공격자가 의도하는 악의적인 js 코드를 피해자 웹 브라우저에서 실행시키는 것이다.

CSRF(Cross Site Request Forgery)

정상적인 request를 가로채 피해자인 척 하고 백엔드 서버에 변조된 request를 보내 악의적인 동작을
수행하는 공격을 의미한다. (피해자 정보 수정 하거나 정보를 열람한다.) 다시 말하면 공격자가 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행하는 공격이다.

브라우저 저장소는 XSS, CSRF 공격에 취약할 수 있다.

😵3. 브라우저 저장소와 보안 이슈

JWT를 저장하기 위한 장소에는 2가지 정도가 있다. localStorage와 cookie.

localStorage에 저장

👍 장점

CSRF 공격에는 안전하다.
자동으로 request에 담기는 쿠키와는 다르게 js 코드에 의해 헤더에 담기므로 XSS를 뚫지 않는 이상 공격자가 정상적인 사용자인 척 request를 보내기가 어렵다.

👎 단점

XSS에 취약하다.
공격자가 localStorage에 접근하는 Js 코드 한 줄만 주입하면 localStorage를 공격자가 접근할 수 있다.

cookie에 저장

👍 장점

XSS 공격으로부터 localStorage에 비해 안전하다.
쿠키의 httpOnly 옵션을 사용하면 Js에서 쿠키에 접근 자체가 불가능하다. 그래서 XSS 공격으로 쿠키 정보를 탈취할 수 없다.(httpOnly 옵션은 서버에서 설정할 수 있음)

👎 단점

CSRF 공격에 취약하다.
자동으로 http request에 담아서 보내기 때문에 공격자가 request url만 안다면 사용자가 관련 link를 클릭하도록 유도하여 request를 위조하기 쉽다.

🤔4. 어떻게 저장하는게 안전할까?

refresh Token을 사용한다. refreshToken만을 secure httpOnly 쿠키에 저장해 CSRF 공격을 방어할 것이다. accessToken은 웹 어플리케이션 내 로컬 변수에 저장해 사용하며, API를 요청할 때 Authorization 헤더에 넣어 보내준다.(ex) axios default header(Authorization)

  • JWT로 유저 인증
  • refreshToken을 secure httpOnly 쿠키로, accessToken은 JSON payload로 받아와서 웹 어플리케이션 내 로컬 변수로 이용

프론트서버에서 처리방법

import React from "react";
import ReactDOM from "react-dom";
import axios from "axios";

import App from "./App";

axios.defaults.baseURL = "https://www.abc.com";
axios.defaults.withCredentials = true;
onLogin = (email, password) => {
	const data = {
		email,
		password,
	};
	axios.post('/login', data).then(response => {
		const { accessToken } = response.data;

		// API 요청하는 콜마다 헤더에 accessToken 담아 보내도록 설정
		axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

		// accessToken을 localStorage, cookie 등에 저장하지 않는다!

	}).catch(error => {
		// ... 에러 처리
	});
}

5. refresh token 재 발급

api/postAxios

export const defaultAxios: AxiosInstance = axios.create({
  baseURL: `${SERVER_ADDRESS}`,
  headers: {
    access_token: cookies.get('access_token'),
  },
});

defaultAxios.interceptors.request.use(checkToken);	//추가(axios interceptor 처리)

api/post

import { defaultAxios } from 'api/postAxios';

const requestPost = async (postList) => {
  const res = await defaultAxios.post('/post', postList);
}

api/checkToekn

import axios, { AxiosRequestConfig } from 'axios';
import * as jwt from 'jsonwebtoken';
import cookies from 'js-cookie';

export const checkToken = async (config: AxiosRequestConfig) => {
  let accessToken = cookies.get('access_token');
  const decode = jwt.decode(accessToken);
  const nowDate = new Date().getTime() / 1000;
  
  // 토큰 만료시간이 지났다면
  if (decode.exp < nowDate) {
    const { data } = await axios.post(`${SERVER_URL}/token`, { accessToken }, {
        headers: {
          access_token: getToken(),
        },
      });
    // 리프레쉬 토큰 발급 서버 요청
    
    const { refreshToken } = data.data;
    
    accessToken = refreshToken;
  }
  
  config.headers['access_token'] = accessToken;
  return config;
}

6. 궁금한 사항

  • axios.defaults.headers.common['Authorization']의 값이 새로고침을 하거나 다른 페이지로 옮겨갈때 유지가 안되는데 해결방법 ?
    => 로그인과 별개로 refreshToken 값을 쿠키에서 읽어와 새로운 refreshToken과 accessToken을 리턴해주는 /refresh-token API가 추가로 필요

(1) 리프레시 됐을 때 (또는 사이트 창을 껐다가 다시 켰을 때)
(2) accessToken이 만료됐을 때
이렇게 두 가지 상황에서 사용

참고자료

프론트에서 안전하게 로그인 처리하기

JWT는 어디에 저장해야 할까?

profile
끊임없이 발전해가는 개발자.

0개의 댓글