회사 업무에서 통합검색 화면
을 개발하던 도중, 로그인처리 인증(JWT) 와 최근검색어에 대해서 브라우저 스토리지에 저장하게 되었다. 최근검색어 같은 경우 상관 없지만 로그인 처리 인증(JWT)
에 대해선 보안이 중요하다 생각하였고, 고민해 보게 되었다.
Json Web Token의 약자로 모바일이나 웹의 사용자 인증을 위해 사용하는 암호화된 토큰을 의미한다.
JWT 정보를 API 요청을 통해 request에 담아 사용자의 정보 확인 및 수정
등 개인적인 작업들을 수행할 수 있다.
공격자(해커)가 클라이언트 브라우저에 Javascript를 삽입해 실행하는 공격이다. 다시 말하면 공격자가 의도하는 악의적인 js 코드를 피해자 웹 브라우저에서 실행시키는 것
이다.
정상적인 request를 가로채 피해자인 척 하고 백엔드 서버에 변조된 request를 보내 악의적인 동작을
수행하는 공격을 의미한다. (피해자 정보 수정 하거나 정보를 열람한다.) 다시 말하면 공격자가 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행하는 공격
이다.
브라우저 저장소는 XSS, CSRF 공격에 취약할 수 있다.
JWT를 저장하기 위한 장소에는 2가지 정도가 있다. localStorage와 cookie.
CSRF 공격에는 안전하다.
자동으로 request에 담기는 쿠키와는 다르게 js 코드에 의해 헤더에 담기므로 XSS를 뚫지 않는 이상 공격자가 정상적인 사용자인 척 request를 보내기가 어렵다.
XSS에 취약하다.
공격자가 localStorage에 접근하는 Js 코드 한 줄만 주입하면 localStorage를 공격자가 접근할 수 있다.
XSS 공격으로부터 localStorage에 비해 안전하다.
쿠키의 httpOnly 옵션을 사용하면 Js에서 쿠키에 접근 자체가 불가능하다. 그래서 XSS 공격으로 쿠키 정보를 탈취할 수 없다.(httpOnly 옵션은 서버에서 설정할 수 있음)
CSRF 공격에 취약하다.
자동으로 http request에 담아서 보내기 때문에 공격자가 request url만 안다면 사용자가 관련 link를 클릭하도록 유도하여 request를 위조하기 쉽다.
refresh Token을 사용한다.
refreshToken만을 secure httpOnly 쿠키에 저장해 CSRF 공격을 방어할 것이다. accessToken은 웹 어플리케이션 내 로컬 변수에 저장해 사용하며, API를 요청할 때 Authorization 헤더에 넣어 보내준다.(ex) axios default header(Authorization)
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 => {
// ... 에러 처리
});
}
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;
}
(1) 리프레시 됐을 때 (또는 사이트 창을 껐다가 다시 켰을 때)
(2) accessToken이 만료됐을 때
이렇게 두 가지 상황에서 사용