TIL35 - 인증/인가

Kiyong Lee·2021년 9월 21일
0

Django

목록 보기
10/35
post-custom-banner

인증/인가

인증(Authentication)과 인가(Authorization)은 웹개발자라면 반드시 알아야 할
기본지식 중 하나이다.

인증과 인가 절차에 대해 알고
인증의 필수요소인 패스워드 암호화
인가의 필수요소인 JWT에 대해 알아보자


1. 인증(Authentication)


1-1. 인증을 위한 로그인 절차

  1. 유저 아이디와 비밀번호 생성
  2. 비밀번호를 암호화해서 데이터베이스에 저장
  3. 유저 로그인
  4. 입력한 비밀번호를 암호화 한 후,
    암호화된 비밀번호와 데이터베이스에 저장된 유저의 비밀번호 비교
  5. 일치할 경우 로그인 성공
  6. access token을 클라이언트에 전송
  7. 로그인 성공 후, 다음 요청부터는 access token을 첨부해서 서버에 전송

1-2. 인증을 위한 유저 비밀번호 암호화

* 유저의 비밀번호는 절대 비밀번호 그대로 데이터베이스에 저장하지 않는다
     1. 해킹당할 시, 비밀번호가 그대로 노출되기 때문
     2. 외부해킹이 아니더라도, 내부에서도 볼 수 있기 때문

  • 유저의 비밀번호는 꼭 암호화해서 저장해야 한다.

  • 비밀번호 암호에는 단방향 해쉬함수가 일반적으로 쓰인다.
     1. 단방향 해시 함수는 원본 메시지를 변환하여,
         암호화된 메시지인 다이제스트(digest)를 생성한다.
         원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만,
         암호화된 메시지로는 원본 메시지를 구할 수 없기 때문에 단방향이라 함
     2. 유사한 비밀번호라도 해쉬함수를 사용했을 때 완전히 다른 값이
         나오는데,이러한 효과를 avalance라고 한다.
#hash함수 예시

    In [21]: import hashlib
    
    #sha는 해쉬 함수 집합을 말하며 256은 256비트로 축약한다는 의미이다.
    #즉, m이라는 변수에 256비트로 축약한 해쉬 함수 집합을 대입하겠다는 뜻
    In [22]: m = hashlib.sha256()
    
    #update는 객체로 변환하겠다는 의미이다.
    In [23]: m.update(b"test password")

    In [24]: m.hexdigest()
    Out[24]: '0b47c69b1033498d5f33f5f7d97bb6a3126134751629f4d0185c115db44c094e'

    In [25]: m = hashlib.sha256()

    In [26]: m.update(b"test password2")

    In [27]: m.hexdigest()
    Out[27]: 'd34b32af5c7bc7f54153e2fdddf251550e7011e846b465e64207e8ccda4c1aeb'

이걸 파이썬으로 직접 찍어보면 아래와 같다

update()를 한 후 m을 출력해보면, 객체이고 위치가 어딘지 알 수 있으며
"test password"를 암호화된 메시지로 반환하겠다는 의미인 hexdigest()를
실행했을 때 변환된 값이 나오는 걸 알 수 있다.


1-3. Bcrypt

bcrypt는 레인보우 테이블 공격을 방지하기 위한 암호 해시 함수이다.

pip install bcrpyt

단방향 함수에도 아래와 같은 취약점이 있다.

Rainbow table attack
미리 해쉬값들을 계산해놓은 테이블을 Rainbow table이라고 한다.
이 테이블에서 해시함수를 이용해 원 비밀번호를 추출하는데
테이블이 공격받아 해킹당해버린다면?

그리고 해시함수는 패스워드를 저장하기 위해 설계된 것이 아닌,
짧은 시간에 데이터를 검색하기 위해 설계된 것이다.
그렇기 때문에, 공격자들은 해킹할 대상의 다이제스트를 임의의 문자열과
빠르게 비교를 할 수 있게 된다.
만약 사용자가 패스워드를 짧거나, 복잡하지 않게 만들었다면
해킹하는 건 금방이다.

그에 따른 보완점이 아래 2가지가 주로 사용된다

1. salting
실제 비밀번호 이외에 추가적으로 랜덤 데이터를 더해서 해시값을 계산하는 방법
2. Key Streching
단방향 해쉬값을 계산 한 후 그 해쉬값을 또 해쉬하고, 이를 계속 반복
일반적인 장비로 1초에 50억개 이상의 다이제스트를 비교할 수 있지만,
해당 방법은 1초에 5번정도만 비교할 수 있다고 한다.

이 두가지를 구현한 해쉬 함수 중 가장 널리 사용되는 것이 bcrypt이다.

bcrypt는 암호 해시 함수로서, rainbow table attack을 방지하기 위해 만들어진,

처음부터 비밀번호를 단방향 암호화 하기 위해 만들어진 해쉬함수이다.

In [40]: import bcrypt

In [41]: bcrypt.hashpw(b"secrete password", bcrypt.gensalt())
Out[41]: b'$2b$12$.XIJKgAepSrI5ghrJUaJa.ogLHJHLyY8ikIC.7gDoUMkaMfzNhGo6'

In [42]: bcrypt.hashpw(b"secrete password", bcrypt.gensalt()).hex()
Out[42]: '243262243132242e6b426f39757a69666e344f563852694a43666b516545469397448446c4d366635613542396847366d5132446d62744b70357353'

1-4. JWT(Json Web Tokens)

클라이언트&서버, 서비스&서비스 사이 통신 시 권한 인가를 위해 사용하는 토큰이다
(쉽게말해 본인 확인 수단)

pip install pyjwt

앞서 언급했듯이, 로그인에 성공하면 access token을 다음 요청부터 보내게 된다.

POST /auth HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
    "username": "joe",
    "password": "pass"
}

위와 같은 로그인 요청을 보냈다고 하면

HTTP/1.1 200 OK
Content-Type: application/json

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDQ0OTE3NjQwLCJuYmYiOjE0NDQ5MTc2NDAsImV4cCI6MTQ0NDkxNzk0MH0.KPmI6WSjRjlpzecPvs3q_T3cJQvAgJvaQAPtk1abC_E"
}

아래와 같은 토큰이 발행되는 것이다.

위 토큰을 복호화하면 아래와 같은 정보를 얻는다
(복호화 : 디코딩이라고도 하며, 부호화된 정보를 되기 전으로 되돌리는 방식)

{
    user_id = 1 
}

이 토큰을 생성하는 여러가지 방법 중 하나가 JWT이고,
말 그대로 유저정보를 담은 JSON 데이터를 암호화하여,
클라이언트와 서버간에 주고 받는 것이다.


2. 인가(Authorization)

인가는 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를
확인하는 절차이다.

예를 들어, 해당 유저는 고객 정보를 볼 순 있지만, 수정은 안된다 같은..

인가도 JWT를 통해 구현할 수 있으며, 토큰을 통해 해당 유저의 권한도 확인 가능


인가의 절차는 아래와 같다

  1. 인증을 통해 토큰을 생성하되, 토큰에는 유저 정보를 확인할 수 있는 정보가
    들어가야 한다( 예를 들어 ID같은..)
  2. 유저가 request를 보낼 때 access token을 보낸다.
  3. 서버는 토큰을 복호화 한다.
  4. 복호화된 데이터를 통해 ID를 얻는다
  5. ID를 통해 데이터베이스에서 해당 유저의 권한을 확인한다.
  6. 유저가 충분한 권한을 가지고 있으면, 해당 요청을 처리한다.
  7. 가지고 있지 않다면 응답코드는 401

3. 실습


3-1. bcrypt

#우선 pip install bcrypt 부터 하자
>> import bcrypt

#임의의 비밀번호로 1234 설정
>> password = 1234

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

#해싱 이전에 인코딩이 되어야 한다고 한다.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/lib/python3.9/site-packages/bcrypt/__init__.py", line 80, in hashpw
    raise TypeError("Unicode-objects must be encoded before hashing")
TypeError: Unicode-objects must be encoded before hashing

#그래서 비밀번호를 utf-8로 변환해야 함
>> 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 #1234 출력, 이게 복호화(디코딩) 과정임

#인코딩한 비밀번호를 해쉬함수를 이용해 암호화
>> hashed_password = bcrypt.hashpw(encoded_password, bcrypt.gensalt())
b'$2b$12$gsnwtVxxHCrmD4kbrk0TgeOPNmc.nyZVc3dFW/dPFvVt9x3bKyaxG'

#gensalt() 할 떄 마다 값이 바뀜
>> bcrypt.gensalt()
b'$2b$12$GXdmcw9FZPph5su3.yBRYO'

>> bcrypt.gensalt()
b'$2b$12$sh2rV25SDdtGd9N60luFfO'

#gensalt를 변수로 담아서 사용도 가능
>> salt = bcrypt.gensalt()

>> hahsed_passwrord = bcrypt.hashpw(encoded_password, salt)
>> hashed_password
b'$2b$12$xIgne44WCx5W4GzU1Nf2geEpUySrZDjyEoXjwtUmOiO3QpdpxVqxS'

#해싱을 통해 암호화된 비밀번호를 복호화하여 체크
>> bcrypt.checkpw('1234'.encode('utf-8') , hashed_password)
True

>> bcrypt.checkpw('12345'.encode('utf-8') , hashed_password)
False

3-2. jwt

#pip install pyjwt부터 하자

>> import jwt

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

>> jwt.decode(encoded_jwt, secret, algorithms'HS256')
{'user_id':5}
profile
ISTJ인 K-개발자
post-custom-banner

0개의 댓글