인증 & 인가

ㅎㅎ·2021년 6월 28일
0

📌 인증(Authentication)

  • 인증은 identification을 확인하는 절차이다.즉 회원가입과 로그인을 말한다. 인증이 필요한 이유는 우리 서비스를 누가 쓰는지 어떻게 사용하는지추적이 가능하도록 하기 위해서이다.

로그인 절차

  • 유저 아이디와 비번 생성 ➡️ 유저 비번 암호화해서 DB에 저장 ➡️ 유저 로그인(아이디,비밀번호 입력) ➡️ 유저가 입력한 비밀번호와 회원가입할때 암호화해서 DB에 지정된 유저 비밀번호 비교 ➡️ 일치하면 로그인 성공 ➡️ 로그인 성공하면 access token을 클라이언트에게 전송 ➡️ 유저의 로그인 성공 후 다음부턴 access token을 첨부해서 request 서버에 전송하므로 유저는 매번 로그인하지 않아도 된다.

유저 비밀번호 암호화

  • 유저의 비밀번호는 절대 비밀번호 그대로 DB에 그대로 저장하지 않는다.
    • 해킹을 당하면 유저의 비밀번호도 그대로 노출 된다. 외부 해킹이 아니더라도 내부 개발자나 인력의 유저들이 비밀번호를 볼 수 있다.
  • 비밀번호 암호에는 단방향 해쉬 함수(one-way hash function) 이 일반적으로 쓰인다.

- 단방향 해쉬?

  • 단반향 해시 함수는 원본 메세지를 변환하여 암호화된 메시지인 다이제스트(digest) 를 생성한다. 원본 메세지를 알면 암호화된 메시지를 구하기를 쉽지만, 암호화돤 원본 메시지를 구할수 없어서 단방향이라고 한다.
  • 본래 해쉬(hash) 함수는 자료구조에서 빠른 자료의 검색, 데이터의 위변조 체크를 위해 쓰이지만, 복원이 불가능한 단뱡향 해쉬함수는 암호학적 용도로 사용한다.
  • 해싱은 난독화를 의미한다.
  • 예들 들어, "test password"를 hash256이라는 해쉬 함수를 사용하면 0b47c69b1033498d5f33f5f7d97bb6a3126134751629f4d0185c115db44c094e이라는 값이 나온다. 그리고 비슷한 비밀번호를 넣으면 완전히 다른 값이 나오는데 이러한 효과를 avalance라고 하며 이는 해킹을 어렵게 만드는 하나의 요소이다.

  • 이러한 단방향 해쉬 함수도 몇가지 취약점이 존재한다.
    • Rainbow table attack - 미리 해쉬값들을 계산해 놓은 테이블을 rainbow table이라고 한다.
    • 위에서 언급했듯이 해쉬 함수는 패스워드를 저장하기 위해서가 아닌 짧은 시간의 데이터를 검색하기 위해 설계되었다. 따라서 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다. 이런 방식으로 패스워드를 추측하면 패스워드가 충분히 길지 않거나 복잡하지 않을 경우 비교하는데 그리 긴 시간이 걸리지 않는다.
    • 따라서 이런 해쉬 함수의 취약점들을 보안하기 위해 2가지 보완점들이 사용된다. - salting, key stretching

📌 Bcrypt

  • salting, key stratching 대표적 라이브러리. 다양한 언어를 지원하고 있으며, 사용이 간편하여 쉽게 적용이 가능하다. 처음부터 비밀번호를 단방향 암호화하기 위헤 만들어진 해쉬함수이다.
  • bcrypt는 hash 결과값에 소금값과 해시값 및 반복횟수를 같이 보관하기 때문에 비밀번호 해싱을 적용하는데 있어 db설계를 복잡하게 할 필요가 없다.

salting & key stretching

  • salting은 입력한 비밀번호와 임의로 추가적으로 생성한 문자열(salt)를 합쳐서 해시값을 계산하는 방법이다.
  • key stretching은 단방향 해쉬값을 계산한 후 그 해쉬값을 또 또 해쉬 하고, 또 이를 반복한다. 최근에는 일반적인 장비로 1초에 50억개 이상의 디이제스트를 비교할 수 있지만, 키 스트레칭을 적용하여 동일한 장비에서 1초에 5번 정도만 비교할수 있게 한다.

bcrypt 실습

#가상환경에서 설치 후에
pip install bcrypt
#python shell
>>>import bcrypt
>>>password = "1234"

bcrypt.hashpw(password, bcrypt.gensalt())

라고 치면 TypeError: Unicode-objects must be encoded before hashing. 이라는 오류가 발생한다. 해싱 전에 반드시 인코딩 되어야 한다는 뜻이다. 여기서 인코딩(코드화, 암호화)은 str -> 유니코드로, 디코딩(복호화)은 유니코드 -> str을 의미한다.

>>> encoded_password = password.encode('utf-8') #패스워드 인코딩하기
>>> encoded_password
b'1234'

#타입 확인
>>> type(encoded_password)
<class 'bytes'>
>>> type(password)
<class 'str'>

#다시 디코딩 하면 원래 비밀번호 확인 가능
>>> decoded_password = encoded_password.decode('utf-8')
>>> decoded_password
'1234' 

- bcrypt.hashpw

#패스워드 해쉬하기
>>> hashed_password =  bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
>>> hashed_password
b'$2b$12$Ix7J4Nzf5hToF3tafBAqy.4DkF3xnnDuBmPf2P0M4LwLazvCG.fmO'

# 솔트를 만들때마다 계속 다른 솔트값이 랜덤으로 만들어짐을 확인할 수 있음. 
>>> bcrypt.gensalt()
b'$2b$12$MthmJbSBXc3CldcFOprFIu'
>>> bcrypt.gensalt()
b'$2b$12$pxqbHoWwx5JZafjdoSEt6u'
>>> bcrypt.gensalt()
b'$2b$12$KHm7ZMg2rqwWqpm/BvZJ3u'
#솔트값을 만들어 따로 넣어주는 것도 가능함.
salt = bcrypt.gensalt()
>>> salt
b'$2b$12$x4x5sJDkfTRtvCSu.FzSaO'
>>> hashed_password = bcrypt.hashpw(password.encode('utf-8'),salt)
>>> hashed_password
b'$2b$12$x4x5sJDkfTRtvCSu.FzSaOxbZQBHBm7obaSNQF14P4bMPoSu6uK5y'
#위의 솔트값이 hashed_password에 추가되어 있는 것을 확인 가능함.

주의할 점은 데이터베이스에 저장할때 문자열이 아닌 해쉬된 패스워드를 디코딩해서 저장해야함!

- bcrypt.checkpw

>>> bcrypt.checkpw('1234'.encode('utf-8'),hashed_password)
True
>>> bcrypt.checkpw('123'.encode('utf-8'),hashed_password)
False

로그인할때 '1234'라는 패스워드를 받았다고 가정했을때, 인코딩한 '1234'와 데이터베이스에서 str형태로 저장된 비밀번호를 인코딩해 가져온 값이 둘이 동일한지 체크해야한다.

그리고 bcrypt는 단방향 해쉬 알고리즘이라서 솔틍하고, 스트레칭하면 복호화가 불가능에 가깝지만,jwt는 복호화하면 페이로드에 암호정보를 확인할수 있어서 사용자가 로그인하면 백엔드 서버가 토큰을 프론트엔드에 발행하고, 프론트엔드는 api를 요청할때 토큰을 넣어서 보맨면 백엔드는 그것을 확인하고 인가를 제공한다. 토큰을 사용하게 되면 백엔드 서버는 토큰 정보를 해석하는 데코레이터를 만들어 인가가 필요한 엔드포인트들을 사용할 수 있다.

- JSON web token

>>> encoded_jwt = jwt.encode({'user_id':5 },'secret' , algorithm="HS256")
>>> encoded_jwt
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo1fQ.K4UUqOCE7e7M4yo9LTs2gUaJURg-8DAajx95oj-QIgQ'

중요한 점은 절대 유저의 패스워드와 같은 개인정보를 넣으면 안된다. 유저id는 단순한 숫자로 보기 때문에 가능하다.

>>> jwt.decode(encoded_jwt, 'secret' , algorithms='HS256')
{'user_id': 5}

이렇게 JWT를 디코드 메소드로 복호화하면 유저아이디를 확인할 수 있다.




📌 인가(Authorization)

  • 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차이다. 사용자가 서버에 로그인하면 해당 사용자가 맞는지 확인하는 과정이 바로 인가이다. 예를들어 해당 유저는 고객 정보를 볼 수 있지만 수정을 할 수 없다.
  • 서버는 사용자가 로그인 했을 경우, 로그인 했다는 것을 어떻게 할수 있을까? 바로 프론트에서 header에 메타데이터를 보내서 확인한다. 이 JSON Web Token을 JWT라고 한다.

Authorization 절차

  1. 인가 절차를 통해 access token을 생성한다. access token에는 유저 정보를 확인할 수 있는 정보가 들어가 있어야 한다.(ex) user id) ➡️ 유저가 request를 보낼때 access token을 첨부해서 보낸다.
  2. 서버에서 유저가 보낸 access token을 복호화 한다.
  3. 복화화된 데이터를 통해 user_id를 얻는다.
  4. user id를 사용해서 db에 있는 해당 유저의 권한을 확인한다.
  5. 유저가 충분한 권한을 가지고 있으면 해당 요청을 처리한다.
  6. 유저가 권한을 가지고 있지 않다면 401 또는 다른 에러 코드를 보낸다.

JWT(JSON Web Token)

  • 위의 로그인 절차에서 보듯이 유저가 로그인 성공한 후!에는 (로그인할때 백에서 만든)access token이라는 암호화된 유저 정보를 첨부해서 request를 보내게 된다. 그러면 서버에서는 access token을 복호화해서 해당 유저 정보를 얻게 된다. 이렇게 복호화해서 얻은 유저 아이디를 통해 해당 유저가 누구인지 알 수 있다.
  • 이런 절차의 목적은 유저가 매번 로그인하지 않도록 하는 것이다. 이러한 acess token을 생성하는 대표적인 방법은 JWT이다. JWT는 말 그대로 유저 정보를 담을 JSON 데이터를 암호화해서 클라이언트와 서버간에 주고 받는다.
  • 예를 들어 access_tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E 복호화하면 다음과 같은 정보를 얻을 수 있고, 얻은 유저 아이디를 통해 해당 유저가 누구인지 알 수 있다.
{
    user_id = 1 
}
  • access_token을 생성하는 방법은 다양한데, 그중 널리 쓰이는 방법이 JWT이다.

구조

  • 헤더: 토큰의 타입과 해시알고리즘 정보가 들어간다.
  • 내용(payload) : exp와 같이 만료시간을 나타내는 공개 클레임, 클라이언트와 서버간 협의하에 사용하는 비공개 클레임, 그리고 위 두가지 요소를 조합하여 작성한 뒤 BASE6 인코딩하여 두번째 요소로 위치한다. ex) {"use-id":"1", "exp": 1539517391}
  • 서명:
    • JWT가 원본 그대로라는 것을 확인할 때 사용하는 부분, 시그니처는 BASE64URL 인코드된 header와 payload 그리고, JWT secret(별도 생성) 을 헤더에 지정된 암호 알고리즘으로 암호화하여 전송한다.(즉, 서명에는 앞의 두개를 암호화시킨것과 시크릿키가 들어있다.)
    • 프론트엔드가 JWT(토큰)를 백엔드 API 서버로 전송하면 서버에서는 전송받은 JWT(토큰)의 서명부분을 복호화해 서버에서 생성한 JWT가 맞는지 확인한다. 즉 계약서의 위변주를 막기위해 서로 사인한 것과 비슷
    • header와 payload에는 BASE764 인코딩한 것이므로(암호화 X) 누구나 원본을 볼수 있으니 개인정보를 담아서는 안 된다.

0개의 댓글