인증(Authentication)과 인가(Authorization)은 웹개발자라면 반드시 알아야 할
기본지식 중 하나이다.
인증과 인가 절차에 대해 알고
인증의 필수요소인 패스워드 암호화
인가의 필수요소인 JWT에 대해 알아보자
- 유저 아이디와 비밀번호 생성
- 비밀번호를 암호화해서 데이터베이스에 저장
- 유저 로그인
- 입력한 비밀번호를 암호화 한 후,
암호화된 비밀번호와 데이터베이스에 저장된 유저의 비밀번호 비교- 일치할 경우 로그인 성공
- access token을 클라이언트에 전송
- 로그인 성공 후, 다음 요청부터는 access token을 첨부해서 서버에 전송
* 유저의 비밀번호는 절대 비밀번호 그대로 데이터베이스에 저장하지 않는다
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()를
실행했을 때 변환된 값이 나오는 걸 알 수 있다.
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'
클라이언트&서버, 서비스&서비스 사이 통신 시 권한 인가를 위해 사용하는 토큰이다
(쉽게말해 본인 확인 수단)
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 데이터를 암호화하여,
클라이언트와 서버간에 주고 받는 것이다.
인가는 유저가 요청하는 request를 실행할 수 있는 권한이 있는 유저인가를
확인하는 절차이다.
예를 들어, 해당 유저는 고객 정보를 볼 순 있지만, 수정은 안된다 같은..
인가도 JWT를 통해 구현할 수 있으며, 토큰을 통해 해당 유저의 권한도 확인 가능
인가의 절차는 아래와 같다
- 인증을 통해 토큰을 생성하되, 토큰에는 유저 정보를 확인할 수 있는 정보가
들어가야 한다( 예를 들어 ID같은..)- 유저가 request를 보낼 때 access token을 보낸다.
- 서버는 토큰을 복호화 한다.
- 복호화된 데이터를 통해 ID를 얻는다
- ID를 통해 데이터베이스에서 해당 유저의 권한을 확인한다.
- 유저가 충분한 권한을 가지고 있으면, 해당 요청을 처리한다.
- 가지고 있지 않다면 응답코드는 401
#우선 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
#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}