인증/ 인가 (JWT, 암호화, 단방향 해쉬함수)

GY·2021년 12월 20일
0

Basic CS

목록 보기
18/28
post-thumbnail

🔑 인증 (Authentication)

로그인 할 때 거치는 단계로, 유저의 아이디와 비밀번호를 확인하는 절차이다.

다음과 같은 로그인 절차가 있다고 하자.
1. 아이디와 비번 생성 후 암호화 해 DB에 저장
2. 유저가 입력한 아이디와 비밀번호를 DB에 저장된 정보와 비교
3. 일치하면 access token을 클라이언트에게 전송
4. 로그인 성공 후 이 access token은 request에 첨부되어 서버에 전송해 로그인 정보를 유지할 수 있다.


🔒 인증, 왜 필요할까?

  • 우리 서비스를 누가쓰는지, 어떻게 사용하는지 추적이 가능하도록 하기 위한 목적이다.

🔒 인증에 필요한 비밀번호, 어떻게 관리해야 할까?

DB가 해킹되거나, 내부 인력들이 비밀번호를 볼 수 있는 경우에 대비하여 비밀번호는 암호화해 DB에 저장한다.
Database에 저장 시 개인정보는 해싱하여 복원할 수 없도록 한다.

해싱?

단방향 해쉬함수를 사용하여 값을 변환하는 것이다.
자세한 사항은 아래 암호화를 참고하자.

통신 시 개인정보를 주고받을 때 SSL을 적용하여 암호화 한다.
이것이 HTTPS인데, HTTP에 SSL을 적용하여 암호화를 했다는 의미이다.
중간에 해커가 통신을 가로채더라도 정보가 유출되지 않도록 할 수 있다.

암호화? 어떻게 하는 건데?


🔐 암호화

🔒단방향 해쉬

  • 단방향: 원본메시지는 암호화할 수 있지만, 암호화된 메시지로 원본 메시지를 알아낼 수는 없기 때문에 단방향이라고 한다.
  • 해쉬: 해쉬 함수는 자료구조에서 빠른 자료의 검색, 데이터의 위변조 체크를 위해서 쓰인다.
    복원이 불가능한 단방향 해쉬함수는 암호학적 용도로 사용한다.

단방향 해쉬 함수는 어떻게 암호화를 할까?

  • digest: 원본 메시지를 변환하여 암호화된 메시지인 digest를 생성한다. 이것을 저장하면 사용자의 패스워드를 직접 저장하는 위험을 피할 수 있다.
  • avalance: 입력 값의 일부가 변경될 때 digest가 완전히 달라져 원본 패스워드를 추론하기 어렵게 만드는 암호화 요소를 말한다.

예를 들어, hunter2라는 패스워드를 SHA-256이라는 해시 알고리즘으로 인코딩 하면 다음과 같다.

f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7

그런데 마지막 숫자만 바꿔서 hunter3이라는 패스워드를 입력해 똑같이 인코딩하면 digest는 다음과 같다.

fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a

그러나 허점이 있다.


🔒 단방향 해쉬의 허점

rainbow table attack

가능한 경우의 수를 모두 해시값으로 만든 것을 rainbow table이라고 하는데, 이를 이용해 해시값을 유추해주는 사이트도 존재한다.
동일한 메시지가 늘 동일한 다이제스트를 갖는다면, 해커가 가능한 많이 다이제스트를 확보해 비교하여 원본메시지를 찾을 수 있게 되는 원리이다.

속도

해시 함수는 본래 짧은 시간에 데이터를 검색하기 위해 설계된 것이기 때문에, 해커역시 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다. 따라서 빠른 시간 내에 원본 비밀번호를 유추해낼 위험이 있다.

허점을 보완하기 위해 생겨난 방법이
salting과 key stretching이다.
비밀번호와 임의로 생성한 문자열 salt를 해싱하여 이 해시값을 저장하는 방법이다.


🔒 salting & key stretching

소금을 치다 & 늘리다
입력한 비밀번호와 임의로 생성한 문자열인 salt를 합쳐서 이 해시값을 저장하는 것이다.
해석을 위해서 이 salt값도 물론 같이 저장해야 한다.

원본 패스워드를 알아내더라도 salting된 다이제스트를 대상으로 패스워드 일치 여부를 확인하기 어렵다. 또한 사용자 별로 다른 솔트를 사용한다면 동일한 패스워드를 사용하는 사용자의 다이제스트가 다르게 생성되기 때문에 해킹 위험을 줄일 수 있다.

이것도 일단 무작위 대입해서 찾아낼 수는 있는 거 아니야?

무작위 대입을 통한 해시값 계산에 소요되는 시간을 많이 늘리기 위한 방법이 key stretching이다.


key stretching

앞서 설명한 salting과 해싱을 여러번 반복해서 키 자체를 늘리고,
원본 값을 유추하기 어렵게 만드는 것이 바로 Key Stretching이다. 해시함수가 연산 시간이 빠르다는 단점이 있다면 key stretching으로 이 단점을 보완할 수 있다.

솔트를 추가한 패스워드에 여러 단계의 해시함수를 적용하여 다이제스트를 생성하는 과정이다. 아래 이미지를 참고하자.

일반적으로 원래는 1초에 50억개 이상의 다이제스트를 비교할 수 있지만, key stretching을 적용하면 같은 장비가 50억개가 아닌 1초에 5번 정도만 비교할 수 있게 된다.
물론 컴퓨터의 성능이 더 좋아진다면 key stretching을 할 때 값을 몇번 더 반복하게 만들어 더 보완할 수도 있다.

이런 로직은.. 구현하기 어렵겠는걸?


🔒 bcrypt

패스워드 저장을 목적으로 설계된, salting과 key stretching을 적용하기 편하도록 해주는 라이브러리이다.
hash 결과값에 salt값과 해시값, 반복횟수를 같이 보관하기 때문에 비밀번호 해싱을 적용하는데 있어 DB설계를 복잡하게 할 필요가 없다.



🔑 인가 (Authorization)

해당 유저가 request에 해당하는 권한이 있는지 확인하는 절차

HTTP 특징

  • request/response 요청과 응답
  • stateless: 이전 요청을 기억하지 못하는 성질

이전 포스팅에서 다루었듯이, stateless한 성질을 보완하기 위해 우리는 headers에 메타데이터를 보낸다.
예를들어 사용자가 로그인했다는 것을 headers에 보내는 것이다. 이것을 인가라고 한다.
신분증/패스의 개념으로 생각하면 된다.

전통적으로는 세션을 사용했었다.

세션은 서버에 로그인 되어있는 채로 지속되어 있는 상태의 한 단위를 세션이라고 한다.

서버에서는 세션을 사용해 브라우저가 세션ID란 이름의 쿠키로 저장하고, 요청 때마다 이것을 함께 보낸다.
서버는 메모리에서 이것을 확인해 매칭되는 결과값이 있을 때 인가한다.

그러나 이 방식은 서버가 재부팅되어야 하는 상황이 되면 메모리의 정보가 없어지고 로그인 상태가 지속되지 못한 다는 단점이 있다.
서비스의 규모가 커져서 서버가 여러대 운영될 경우, 각 요청마다 필요한 정보가 각각의 서버에 해당 정보가 없을 수 있다는 단점도 존재한다.

이 단점을 보완하기 위해 만들어진 것이 토큰 방식의 JWT이다.


🔐 JWT (JSON web token)

access token을 생성하는 방법중 가장 널리 사용되는 기술 중 하나이다.
유저 정보를 담은 JSON데이터를 암호화 해서 클라이언트와 서버간에 주고받는 것을 말한다.

  • 첫번째 요청에서 Token을 발행받았다면,
  • 두번째 요청할때 이 발행받은 Token을 headers에 포함해 요청을 보낸다.

"나 아까 로그인했어. 너네가 발행한 인증서야, 봐! 다음 요청해도 되지?"라고 요청하는 것으로 상상해보면 이해하기 쉽다.


header.payload.signature

🔒 header

  1. 토큰의 타입: 언제나 JWT이다.
  2. alg (해시알고리즘 정보): signature 부분을 만드는데 사용될 알고리즘을 지정한다. 여러 암호화 방식 중 하나를 지정할 수 있다.
    BASE64 방식으로 인코딩해 들어간다.

참고: 인코딩할 뿐, 암호화 한 것은 아니다.


🔒 payload

payload(내용)은 3가지의 Claim으로 구성되어있고, 이것을 조합하여 작성한 뒤 BASE 64방식으로 인코딩한다.

  • Registered Claim: 만료시간을 나타내는 exp와 같이 미리 정의된 집합
  • Public Claim: 공개용 정보 전달을 목적으로 하는 클레임
  • Private Claim: 클라이언트와 서버간 협의하에 사용하는 클레임

이 토큰을 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지, 서비스가 사용자에게 이 토큰을 통해 공개하고자 하는 범위는 어디까지인지(관리자, 회원등급 등)의 정보가 이 페이로드에 담겨있다.

Claim?

토큰에 담긴 이러한 사용자 정보와 같은 데이터들을 클레임이라고 한다.

암호화가 되어 있지 않다면 보안에 너무 취약한 게 아닐까?

이 문제를 보완하기 위해 header와 signature를 사용한다.


🔒 signature

JWT가 원본 그대로라는 것을 확인하는 용도
BASE64형태로 인코딩된 header와 payload그리고 별도로 생성한 JWT secret(서버에서만 알고 있는 비밀 값)을 헤더에 지정된 암호 알고리즘으로 암호화하여 전송한다.

  • 프론트엔드가 JWT를 백엔드 API서버로 전송하면 서버에서는 전송받은 JWT의 서명 부분을 복호화하여 서버에서 생성한 JWT가 맞는지 확인한다.

계약서의 위조/변조를 막기위해 서로 사인하는 것으로 이해하면 된다.

주의할 점은, header와 payload는 암호화 한 것이 아니므로 누구나 원본을 볼 수 있으므로 개인정보를 담아서는 안된다는 것이다.

그렇다면... JWT만으로 기존의 세션의 단점을 보완해, 완전한 인가를 구현할 수 있는 걸까?

아니다. JWT 또한 단점이 있다.

기본적으로 HTTP 프로토콜은 stateless 즉 상태를 기억하지않는 특성을 가지고 있다면,
세션은 모든 사용자들의 상태를 기억하고 있는 stateful한 특성을 가지고 있다.
이것이 세션의 단점이기도 하지만 동시에 장점이 되기도 하는데, 기억하고 있는 상태들을 언제든 제어할 수 있기 때문이다.

예를 들어 하나의 기기에서만 로그인이 가능하도록 만들어야 할 경우 기존의 기억하고 있는 세션을 종료하면 된다.
하지만 JWT는 이러한 것이 불가능하다. 따라서 만약 해킹을 당했을 경우 해당 토큰을 무효화하기 힘들다.

그러면 어떻게 보완할 수 있을까?

로그인시 액세스 토큰의 만료시간을 아주짧게 설정하고, 2주정도로 길게 만료시간을 설정한 리프레시 토큰을 함께 보내준다. 이 리프레시 토큰은 상응값을 데이터베이스에도 저장한다.

액세스 토큰의 수명이 다하면 리프레시 토큰을 보내고, 이것을 통해 서버에서 액세스 토큰을 새로 보내주는 것이다.

이렇게 하면 앞서말했던 부분을 보완할 수 있다. 예를 들어 토큰이 탈취당했을 때 해당 토큰이 금방 만료되어 쓰지 못하도록 하고, 리프레시 토큰을 서버에서 지워 새로 갱신하지 못하도록 하는 것이다.
즉, 리프레시 토큰을 사용할 경우 서버가 강제로 만료시키는 등의 조치를 취할 수 있다.

하지만 ...

그 액세스토큰이 짧은 시간동안 살아있는 동안에는 마찬가지의 문제점을 해결할 수 없다는 한계가 있다.
따라서 이를 고려해 인가방식을 선택해야 한다.



Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글