오늘은 암호화 및 토큰 발행에 대한 기초적인 부분을 알아보고자 합니다.
일단 우린 암호화를 왜 해야할까요? 답은 간단합니다. 내가 아닌 다른 사람이 내 정보를 갈취할 수 없도록 보호하기 위함입니다. 그리고 지금은 법적으로도 개인정보를 암호화해서 관리하도록 하고 있고요.
그러므로 사람들을 대상으로 하는 서비스를 운영하고 있다면 정보의 암호화는 필수라고 할 수 있습니다.
암호화에는 단방향과 양방향, 크게 두 가지 종류가 있습니다. 간단히 말하면 단방향은 평문을 암호문으로 암호화만 할 수 있는 단방향성 알고리즘이고, 양방향은 평문을 암호화도 할 수 있고 암호문을 복호화도 할 수 있는 알고리즘 입니다. 복호화가 안되는 특성 때문에 비밀번호를 관리할 때는 단방향 알고리즘을 사용하는게 좋습니다.
복호화가 안된다면 정작 비밀번호를 입력했을 때 그 값이 맞는지 어떻게 확인하나 하는 생각이 들 수 있는데, 비밀번호 자체를 검증할 때는 입력받은 값을 암호화해서 저장된 암호화된 값과 비교해서 검증을 할 수 있습니다.
이번 글에서 코딩으로 작성해볼 방식도 단방향 알고리즘인데요, 주로 Hash 방식을 통해 구현됩니다.
임의의 길이를 갖는 임의의 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수를 Hash 함수라고 하고, 이 함수의 결과물이 Hash 값 입니다. Hash 함수는 그다지 복잡하지 않은 알고리즘으로 구현되어 상대적으로 CPU, 메모리 같은 시스템 자원을 덜 사용하고, 또 같은 입력 값에 대해서는 같은 출력값이 보장되어 위에서 말한 비밀번호 자체 검증을 가능하게 합니다.
그리고 Hash 함수 특성상 원래의 문장을 복호화할 수 없게 완전히 뭉개버리기 때문에 다시 원본을 복원하는게 불가능합니다. 이러한 특성 때문에 보안에 Hash 값이 많이쓰이는데, 아예 문제가 없진 않습니다. 현재까지 개발된 거의 모든 함수는 해시 충돌문제가 확인된 상태고, 문제가 아직 확인되지 않은 표준은 SHA-3가 유일하다고 합니다.
지금 사용해 볼 bcrypt
는 현재까지 사용되는 가장 강력한 해시 메커니즘입니다. 자세한 설명은 아래 글에서 확인해보세요!
비크립트 비교방법
이번 작업은 python
환경에서 진행합니다. 실습을 위해 새로운python
가상환경을 생성해주세요.
*가상환경 생성이 어려우신 분은 아래 글을 참조해주세요!
가상환경이 생성되었으면, 이제 가상환경에 들어가서 bcrypt
를 설치하고 파이썬을 실행합니다. 파이썬을 실행할 때 저 같은 경우는 python
만 치면 python2가 실행이 되서 python3
를 입력했습니다.
$ conda activate crypt # crypt라는 가상환경에 진입
(crypt) $ pip install bcrypt # bcrypt 설치
Successfully installed bcrypt-3.1.7 cffi-1.14.0 pycparser-2.19 six-1.14.0
(crypt) $ python3 # python3 실행
이제 시작해봅시다! bcrypt
를 임포트 해주고, 테스트로 사용할 비밀번호를 선언해줍니다. 그리고 비밀번호를 Hash화 해주는 메서드를 실행해줍니다. 이 메서드에서 bcrypt.gensalt()
는 해쉬의 복잡도를 결정해주는 요소이고 디폴트 값은 12로 잡혀있습니다. 지금은 아무것도 넣지 않아쓰니 12의 복잡도를 지니겠죠!
bcrypt.hashpw(password, bcrypt.gensalt())
$ import bcrypt
$ password = '12345678'
$ bcrypt.hashpw(password, bcrypt.gensalt())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/LeeJongMin/conda/envs/crypt/lib/python3.8/site-packages/bcrypt/__init__.py", line 61, in hashpw
raise TypeError("Unicode-objects must be encoded before hashing")
TypeError: Unicode-objects must be encoded before hashing
근데 뭔가 에러가 났습니다. 잘 풀리는 것 같더니 무슨 일 일까요?
문제는 비밀번호의 타입이었습니다. 에러 코드를 잘 읽어보면 유니코드 객체는 hashing 되기 전에 인코딩 되어야한다고 되어있습니다. 지금 상태의 비밀번호는 12345678
로 문자열입니다. 하지만 bcrypt
로 hashing 하기 위해서는 bytes
타입으로 인코딩을 해줘야합니다.
인코딩을 해서 다시 실행 해보겠습니다.
$ a = password.encode('utf-8') # password 를 `bytes` 타입으로 인코딩
$ a
b'12345678' # `bytes` 타입으로 인코딩 된 비밀번호
# 인코딩 된 비밀번호로 암호화
$ b = bcrypt.hashpw(a,bcrypt.gensalt())
b'$2b$12$ERVlEDmzU.6E7J1bS8Jvr.cZpvEU3EeckW7KMS/rC3Sv6n2p2aLla'
이렇게 비밀번호를 bcrypt
로 암호화 했습니다. 이제 이 값을 저장해두어야 하는데, bytes
타입으로는 저장을 못하기 때문에 스트링으로 다시 디코딩해서 저장해야합니다. (Http 통신에서도 bytes
타입을 못받기 때문이기도 합니다. json으로 통신.)
$ b.decode('utf-8') # 암호화된 비밀번호를 문자열로 디코딩
'$2b$12$8Zc91EZ2VlZjNzU0tPw34ujrWwutG5fGcDjFT8Nl9up3w1IDmma7.'
이제 사용자가 비밀번호를 입력한다고 가정하고, 방금 암호화해서 저장해놓은 비밀번호와 맞는지 확인해보겠습니다. 사용자가 입력한 암호와 암호화된 암호 모두 bytes
로 인코딩 된 상태여야 비교가 가능하다는 점도 참고하세요!
$ c = '12345678' # 사용자가 입력한 비밀번호
$ bcrypt.checkpw(c.encode('utf-8'),b) # bytes로 인코딩한 c와 암호화된 b와 비교
True # 비밀번호가 일치
d = "12345666" # 사용자가 잘못 입력한 비밀번호
$ bcrypt.checkpw(d.encode('utf-8'),b)
False # 비밀번호가 불일치
이렇게 사용자가 비밀번호를 잘 입력했는지 확인을 완료했습니다. 실제라고 치면 사용자는 로그인에 성공했다고 할 수 있습니다.
그런데 한 가지 궁금한 점이 생길 수 있습니다. 로그인한 사용자는 어떻게 계속 로그인 상태를 유지하면서 권한이 필요한 우리 서비스를 돌아다니는걸까요?
여기에는 토큰
이라는 비밀이 숨어있습니다.
JWT
는 JSON Web Token의 약자로 전자서명 된 URL-safe (URL로 이용할 수 있는 문자로만 구성된)의 JSON 입니다.
JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증합니다. 이 인증 과정을 통해 사용자는 로그인 상태로 우리 서비스를 돌아다닐 수 있습니다.
이제 토큰(token)을 생성해보겠습니다.
먼저 가상환경에 JWT
를 설치합니다. 저는 pip에 있는 pyjwt
를 설치했습니다.
$ pip install pyjwt # pyjwt 설치
Installing collected packages: pyjwt
Successfully installed pyjwt-1.7.1
이제 파이썬에서 jwt
를 임포트해서 토큰을 발행해보겠습니다.
토큰에는 [유저 정보]와 [secret key], [적용하고자 하는 알고리즘]
을 넣어서 발행합니다. 아래는 유저 아이디가 [1]
이고, 시크릿키는 [secretkey]
이며 [HS256]
알고리즘을 사용해 토큰을 발행하겠다는 명령어 입니다.
import jwt
$ jwt.encode({<유저정보>}, <시크릿키>, algorithm = '특정 알고리즘') # 명령어 구성
$ token = jwt.encode({'user_id': 1}, 'secretkey', algorithm = 'HS256') # 실제 명령어 작성
$ token # 발행된 토큰
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.B9XWphE_QU5UZAT8jhKg_jYNGmZOfSL1gcl-3kZeuoI'
이제 발행된 토큰을 프론트엔드 개발자에게 사용할 수 있도록 넘겨주면 되는데요, 이 토큰도 현재 타입이 bytes
로 되어있기 때문에 스트링 형태로 바꾸어 전달해줘야 합니다.
$ token.decode('utf-8') # 문자열로 디코딩
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.B9XWphE_QU5UZAT8jhKg_jYNGmZOfSL1gcl-3kZeuoI'
그리고 bcrpyt
와 다른 점은 토큰을 확인할 때, 토큰을 jwt
로 디코딩하면 기존에 입력했던 유저정보가 반환됩니다.
$ jwt.decode(token,'secretkey',algorithm = 'HS256')
{'user_id': 1} # 기존 유저정보 반환
그렇기 때문에 디코딩할 수 있는 정보에 대한 보안이 중요한데요, Django의 경우 secret key
가 내부 settings.py
에 기본적으로 작성되도록 되어있는데 이렇게 하면 보안에 좋지 않습니다. 실제 프로젝트 진행 시 따로 파일을 만들어 관리해야 좀 더 보안성을 높일 수 있습니다.
지금까지 암호화 및 토큰 발행에 대한 기초를 알아봤습니다. 이후 지식이 디벨롭되면 추가 시리즈로 글을 보완하도록 하겠습니다!
너무 정리 잘 해주셔서 감사해요! 잘 읽고 갑니다!