Nodejs, Express, mysql을 이용하여 로그인을 구현하면서 찾아보고, 배운 것을 정리하고자 합니다.
로그인 기능을 제공하기 위해서는 사용자의 정보를 지키는 보안이 필수라고 생각했습니다.
처음으로 서버를 구현해보면서,
- 로그인한 사용자의 정보를 Session에는 어떻게 저장할 것인지,
- 이를 어디서 활용할 것인지,
- 사용자의 비밀번호는 어떻게 암호화 (복호화도?) 할 것인지
와 같은 고민을 해보았습니다.
= Json Web Token
클라이언트와 서버, 서비스와 서비스 간의 통신 시 Authorization을 위해 사용하는 Token입니다. 이때, Json 포맷을 이용하여 사용자의 속성 정보를 저장한다.
클라이언트와 서버 간 정보를 주고 받을 때, HTTP request header에 JSON Token을 포함한 후 서버는 별도의 인증 과정 없이 Header에 포함된 jwt 정보를 통해 인증을 수행한다.
jwt는 header, payload, signature 세 파트로 나누어진다.
{
"alg": "HS256",
"typ": "JWT"
}
alg
는 Signature를 해싱하는 알고리즘 방식을 지정하며, Signature 및 Token 검증에 사용된다.typ
은 Token의 타입을 지정한다.이와 같은 JSON 객체를 문자열로 직렬화하고 UTF-8
과 Base64 URL-safe
로 인코딩하게 되면 header를 생성할 수 있다.
Base64URLSafe(UTF-8('{"alg": "HS256", "typ": "JWT"}')) → header!
{
"iss": "seongukbaek",
"sub": "test",
...
}
Token 정보를 표현하기 위해 이미 정해진 종류의 데이터
- iss: 토큰 발급자(issuer)
- sub: 토큰 제목(subject)
- aud: 토큰 대상자(audience)
- exp: 토큰 만료 시간(expiration), NumbericDate 형식이어야 함(ex. 1480849147370) = unix time
- nbf: 토큰 활성 날짜(not before), 해당 날짜가 지나기 전의 토큰은 활성화되지 않음
- iat: 토큰 발급 시간(issued at), 토큰 발급 이후 경과 시간을 알 수 있음
- jti: jwt 토큰 식별자(jwt ID), 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용
사용자 정의 Claim
{
"https://baeksulog.netlify.app": true
}
사용자 정의 Claim
{
"isLogin": true
}
Token을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.
Base64 URL-Safe
- URI에서 parameter로 사용될 수 있도록 "+", "/", "=" 를 제외한 인코딩 방식이다.
점을 구분자로 하여 header, payload, signature를 합치면 jwt 가 완성된다.
주된 이점은 사용자 인증에 필요한 모든 정보는 Token 자체에 포함되어 별도의 인증을 위한 저장공간이 필요하지 않다는 점이다. (서버 기반의 인증 시스템은 별도 저장공간이 필요하다.)
jwt 에 대해 찾아보다보니, Refresh Token
이라는 개념에 대해 알게 되었다.
기본 jwt 방식으로 Access Token을 하나만 두는 경우 해당 Token이 탈취되었을 때, 보안의 취약하다는 문제가 발생한다.
유효 기간을 길게 하면서, 보안에 덜 취약한 방법이 없을까? 라는 질문에 대한 답으로, Refresh Token 이 등장한다.
아래는 Access Token, Refresh Token을 이용한 로그인 기능의 흐름도이다.
이러한 방식을 사용하면,
저장위치에 있어 정답은 없는 것 같다.
그저 개발 환경에 있어 최선책을 사용하는 것이 답인 것 같다.
먼저, 보안에 있어 문제가 되는 몇 가지를 알고 가야한다.
Cross Site Script의 약자로, 이미 CSS
라는 약자가 존재해 XSS
로 지어졌다.
게시판이나 웹 메일 등에 js
와 같은 script code를 삽입해 개발자가 의도치 않은 기능이 동작하게 해 치명적인 공격이다.
XSS에는 Reflected XSS
, Stored XSS
, DOM Based XSS
가 있다.
자세한 내용은 참고에서 확인할 수 있다.
Cross Site Request Forgery의 약자이다.
정상적인 request를 가로채 피해자인 척 하고 서버에 변조된 request를 보내 악의적인 동작을 수행하는 공격을 의미한다.
자세한 내용은 참고에서 확인할 수 있다.
XSS 예방이 최소한의 조치
js로 의도하지 않은 request를 날린다던가 localStorage, 변수 값 등 모든 것이 탈취 가능하기에 XSS 공격 방지가 웹 보안의 시작이라고 할 수 있다.
Cookie, LocalStorage 를 이용하면서 위와 같은 보안 문제를 맞닥뜨리게 된다.
CSRF 공격에는 안전하다.
XSS에 취약하다.
XSS 공격으로부터 LocalStorage에 비해 안전하다.
httpOnly
옵션을 사용하면 js에서 Cookie에 접근 자체가 불가능하다.httpOnly
옵션은 서버에서 설정할 수 있음)하지만 XSS 공격으로부터 완전히 안전한 것은 아니다.
httpOnly
옵션으로 Cookie의 내용을 볼 수 없다 해도 js로 request를 보낼 수 있으므로 자동으로 request에 실리는 Cookie의 특성 상 사용자의 컴퓨터에서 요청을 위조할 수 있기 때문이다.httpOnly
Cookie도 안전하진 않다.CSRF 공격에 취약하다.
LocalStorage & Cookie 의 문제점을 피하기 위해 가장 좋은 방법은 서버사이드에 저장하는 것이다.
"즉, DB에 Refresh Token을 저장하고 저장되는 index를 Cookie에 저장한다."
이때, 구글링을 통해 Redis 또는 일반적인 DB에 저장하는 방식을 발견했다.
이미 사용자의 정보를 Mysql
을 사용하여 저장하고 있으므로, DB에 저장하는 방식을 사용하긴 할 것이다.
하지만 Redis에 대해 알아보자면,
메모리 기반의 key-value 구조 데이터 관리 시스템으로, 비관계형 데이터베이스이다.
String
, Set
, Sorted Set
, Hash
, List
데이터 형식 지원장점
이러한 특징이 있고 자세한 내용은 참고에서 확인할 수 있다.
위에서 다룬 사항들을 종합해보았을 때, 아래와 같은 방식과 흐름으로 구현할 예정이다.
- Access Token, Refresh Token 을 사용
- Access Token은 짧은 유효기간으로, Cookie에만 저장 (
httpOnly
)- Refresh Token은 비교적 긴 유효기간으로, DB에 저장
- 사용자는 이미 등록된
id
,pwd
를 입력- 서버에서 POST를 받아 DB에 등록되어 있는 정보인지 조회
- 등록되지 않은 정보인 경우,
res.redirect('/')
Refresh Token
을 생성(jwt.sign()
)하고, DB에 해당Refresh Token
을 저장
- 저장한 후, Auto Increase 되는 token id를 받아옴
Refresh Token
을 생성(jwt.sign()
)- 생성한 Token들을
Cookie(httpOnly)
에 저장하고 클라이언트에게 전달
큰 흐름으로는 아래와 같은 기능들이 필요할 것이라 판단했다.
User
DB에 저장되어 있는 사용자인지 확인jwt.sign()
)middleware
Status 400
returnjwt.verify()
로 Token의 유효성 검사verify()
매개변수 3개Status 401
returnjwt.verify()
로 Token의 유효성 검사user의 id
(or else) 를 이용하여 새로운 Access Token과 Refresh Token 생성