웹사이트를 구현하는데 있어 특히 회원가입 및 로그인 시 로그인 이후에도 특정 정보를 확인하거나 기타 등등 .. 비밀번호나 기타 민감한 정보들은 쉽게 공개되어서는 안된다. 따라서 필요한 부분에 암호화 기능을 추가하여 정보를 은닉해야 하고, 특정 영역에 대해서는 인증된 사용자만 보게끔 해주어야 한다. 이번 포스팅에선 인증(Authentication) & 인가(Authorization) 및 암호화에 관해 간단히 정리해 본다.
앞서 언급했다 시피 회원 비회원을 구분해야 하는 웹사이트인 경우 인증 및 인가 기능이 필수적으로 구현이 되어야 한다. 마이페이지 회원정보 수정 등과 같은 페이지(Private API) 뿐만 아니라 회원만 볼 수 있는 게시판 페이지(Public API) 등이 대표적인 예다.
access token
을 클라이언트에 전송access token
을 들고 request를 웹서버에 전송하면서 매번 login이 유도되지 않고 서비스를 이용할 수 있도록 한다.비밀번호는 말 그대로 비밀번호 이기에 관련 되지 않는 사용자를 제외하고는 해당 서비스 개발자도 쉽게 볼 수 있어서는 안된다. 따라서 단방향 해쉬 함수(one-way hash function) 기반의 단방향 암호화 알고리즘을 적용하여 암호화가 되어 저장된 순간 이후에는 원래 비밀번호를 알 수 없게끔(사용자만 아는) 처리해야 한다.
파이썬에 내장되어 있는 hashlib을 이용하여 단방향 해쉬 함수의 암호화 테스트를 해 보면,
>>> import hashlib
>>> m = hashlib.sha256()
>>> m.update(b"testtest")
>>> m.hexdigest()
'37268335dd6931045bdcdf92623ff819a64244b53d0e746d438797349d4da578'
>>> m = hashlib.sha256()
>>> m.update(b"test")
>>> m.hexdigest()
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
단지 4글자만 달라졌을 뿐인데 암호화 한 결과가 많이 달라지게 된다. 이런 부분이 실제 해킹을 어렵게 하는 요인이 될 수 있다.
그러나 이런 단방향 해쉬 함수도 몇가지 취약점이 있는데, 대표적으로 rainbow table attack(rainbow table : 미리 해쉬값들을 계산해 놓은 테이블) 등과 같은 공격에 취약하다. 해시 함수의 본래 목적은 데이터에 빠르게 접근하기 위해 (시간복잡도 : O(1)) 설계되었다. 대표적인 파이썬의 자료구조인 dictionary나 set이 해시를 기반으로 만들어진 자료구조이다. 이렇게 빠른 접근성이 되려 1:1로 빠르게 접근 가능할 수만 있다면 임의의 문자열 다이제스트(해시로 암호화 된 메세지)와 해킹할 대상의 다이제스트를 비교할 수 있다. 시간이 좀 걸릴 뿐이지 패스워드가 단순하거나 별로 길지 않다면 충분히 취약점으로 작용할 수 있는 부분이다.
이를 해결하기 위해 2가지 대표적인 보완점들이 있다
파이썬에서 단방향 해쉬 함수 중 위 두가지의 보완점이 적용된 패키지로 bcrypt가 있으며 실제 웹개발 시 많이 사용되는 암호화 패키지이다. 아래의 코드는 실제 python 상에서 bcrypt를 사용한 예제이다.
(이미지출처: https://d2.naver.com/helloworld/318732)
import bcrypt
>>> password = 'pass1234'
>>> bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
b'$2b$12$jZhdXPbL6VIVM2AElkkKrO9rMBBKdjqC5mZ3r2cmC8uuWnmF3CZ6u'
>>> bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).hex()
'2432622431322455354742385043484b4f50316c73664d3553685a6f2e4a6a2e6d47754c6a643976344434536d376b6750327a336e6845596d497543'
(참고 : hashpw()
에서 gensalt()
를 통해 salting을 하고 있는데 기본값은 랜덤이다)
>>> import bcrypt
>>> password = '1234'
>>> encrypt_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
>>> encrypt_pw = encrypt_pw.decode('utf-8')
>>> bcrypt.checkpw(account_data['password'].encode('utf-8'), account.password.encode('utf-8'))
True
access token
을 사용한 인가 방식들 중 대표적으로 JWT(JSON Web Token) 방식이 주로 많이 쓰인다(access token을 통해 해당 user가 누군지 알 수 있으므로 해당 유저의 권한(permission)도 확인이 가능하다)Authentication 절차를 통해(로그인 등) access token
을 생성한다. access token
에는 user 정보를 확인할 수 있는 정보가 들어가 있다.
user가 request를 보낼 때 access token
을 첨부해서 보낸다(Front-end/Client)
서버에서는 user가 보낸 access token
내용을 복호화 한다
복호화된 데이터를 통해 user id나 기타 user 정보를 식별 할 수 있는 데이터를 얻는다
user id를 사용해 DB에서 해당 유저의 권한(permission)을 확인한다.
해당 요청에 대한 권한을 가지고 있는 user라면 해당 요청을 처리해 준다.
권한이 없는 user라면 401에러 등으로 response를 보낸다.
토큰 기반의 인증 방식 중 HTTP Authorization header나 URI query parameter등 공백을 사용할 수 있는 곳에서 사용되는 JSON 객체 방식의 토큰. 다시 말해 JWT는 (user)인증 정보를 JSON 형식으로 담고 암호화를 하여 인증이 필요한 서비스를 이용할 시 HTTP header를 통해 서버와 클라이언트 간에 JWT만 무사히 주고 받아도 특정 인가가 필요한 서비스를 지속 이용하게 해준다
JWT에 대해서 정리하는 것만 해도 꽤나 많은 것을 언급해야 하기 때문에 이 지면에선 생략하지만 해당 링크에 굉장히 잘 설명 되어 있으니 참고.
>>> import jwt
>>> jwt.encode({'user_id': 1}, 'secret', algorithm = 'HS256')
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.DHk2kqJnk-mJg-7xcN4NwkaFJRUh01K3vY2V6g8o3bE'
>>> jwt.encode({'user_id': 1}, 'secret', algorithm = 'HS256')
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.DHk2kqJnk-mJg-7xcN4NwkaFJRUh01K3vY2V6g8o3bE'
>>> token = jwt.encode({'user_id': 1}, 'wecode', algorithm = 'HS256')
>>> token
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.DHk2kqJnk-mJg-7xcN4NwkaFJRUh01K3vY2V6g8o3bE'
>>> jwt.decode(token, 'wecode', alghrithm = 'HS256')
{'user_id': 1}
지금까지 django framework 기반에서 end-point를 구현해 왔으며 유효한 응답에 대해서 JsonResponse 방식을 사용했고 인가도 이와 유사한 JWT기반에서 JSON 방식으로 주고 받는 것을 구현할 것이다.
http -v http://127.0.0.1:8000/account/sign-in user_account=test2 password=1234
POST /account/sign-in HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 45
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/2.0.0
{
"password": "1234",
"user_account": "test2"
}
access token
을 받게 된다HTTP/1.1 200 OK
Content-Length: 120
Content-Type: application/json
Date: Sat, 15 Feb 2020 06:11:25 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMX0.mCFOjhRCDXjtYs2u1slOC8S2e4ifO4O0nwnnjecpl7o"
}
access token
을 복호화 하면 다음과 같이 나온다(user정보가 해시 값에 담겨 있다)>>> jwt.decode(b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMX0.mCFOjhRCDXjtYs2u1slOC8S2e4ifO4O0nwnnjecpl7o', SECRET_KEY, algorithm='HS256')
{'user_id': 11}
user는 서버에 특정 요청을 할 때 해당 access token
을 HTTP header에 첨부하여 요청을 하며 access token
을 서버 측에서 복호화를 하여 권한이 맞는 user일 경우 해당 정보를 얻게 되는 것이다.
다음 포스트에서 실제 django api 구현 코드에 인증 인가 기능을 간단히 구현해 보도록 하겠다.
책 깔끔한 파이썬 탄탄한 백엔드 (송은우 저)
velopert blog page(인증/인가 방식) : https://velopert.com/2350
velopert blog page(JWT) : https://velopert.com/2389
JWT에 관한 블로그 글 참고 : https://blog.outsider.ne.kr/1160