[Django] Account 로그인/회원 가입 부분 암호화, 인증/인가 기능 추가 - 1

ybear90·2020년 2월 16일
1

Django

목록 보기
10/12

웹사이트를 구현하는데 있어 특히 회원가입 및 로그인 시 로그인 이후에도 특정 정보를 확인하거나 기타 등등 .. 비밀번호나 기타 민감한 정보들은 쉽게 공개되어서는 안된다. 따라서 필요한 부분에 암호화 기능을 추가하여 정보를 은닉해야 하고, 특정 영역에 대해서는 인증된 사용자만 보게끔 해주어야 한다. 이번 포스팅에선 인증(Authentication) & 인가(Authorization) 및 암호화에 관해 간단히 정리해 본다.

인증(Authentication) & 인가(Authorization)

앞서 언급했다 시피 회원 비회원을 구분해야 하는 웹사이트인 경우 인증 및 인가 기능이 필수적으로 구현이 되어야 한다. 마이페이지 회원정보 수정 등과 같은 페이지(Private API) 뿐만 아니라 회원만 볼 수 있는 게시판 페이지(Public API) 등이 대표적인 예다.

인증(Authentication)

  • user의 id와 pw를 확인하는 절차
  • 인증을 위해 user의 id와 pw를 생성할 수 있는 기능도 필요

로그인 절차

  1. (회원가입을 하지 않은 상태라면)user id와 pw생성
  2. user password 단방향 해시 암호화 -> 해당 password DB에 저장
  3. 만든 userid, password 입력
  4. userid를 찾았다면, 입력한 password와 DB에 암호화 되어 저장된 password와 비교
  5. 일치하면 login
  6. login 성공 시 access token을 클라이언트에 전송
  7. 로그인 성공 시 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가지 대표적인 보완점들이 있다

  • Salting : 소금을 치는 것 처럼 해결하는 방식. 실제 은닉할 비밀번호 이외에 랜덤한 더미 값들을 더해서 해시 값을 계산하는 방법으로 실제 어느 부분이 진짜 비밀번호이고 어느 부분이 더미 데이터인지 알 수 없게 만드는 방법이다. rainbow table attack을 무효화 시킬 수 있는 방법
  • Key Stretching : 해시 함수의 빠른 실행속도에 따른 취약점을 보완하기 위해 단방향 해시 값을 계산하고 그 해시 값의 해시값을 계산하여 여러 번 반복하는 방식. 해당 방법을 적용하면 1초에 50억개 이상의 다이제스트를 비교할 수 있는 부분애서 동일한 장비에서 1초에 5번 정도만 비교할 수 있게 할 정도로 공격에 따른 비교 속도를 늦출 수 있다. 그래서 이론적으로는 정보를 탈취 할 수 없지만 그 소요 시간을 엄청 길게하는 방식이라 보면 된다.

파이썬에서 단방향 해쉬 함수 중 위 두가지의 보완점이 적용된 패키지로 bcrypt가 있으며 실제 웹개발 시 많이 사용되는 암호화 패키지이다. 아래의 코드는 실제 python 상에서 bcrypt를 사용한 예제이다.

bcrypt_examples
(이미지출처: 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을 하고 있는데 기본값은 랜덤이다)

  • 실제 bcrypt 암호화 한 비밀번호 일치 여부 확인하는 예제
>>> 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

인가(Authorization)

  • user의 request에 대해 실행할 수 있는 권한이 있는지 없는지를 확인하는 절차
  • 예를 들면 로그인 유무 별로 볼 수 있는 페이지, 비회원은 게시판 열람만 되게끔 한다 등의 기능
  • 인가의 기능이 제대로 활용되지 않는다면 각 페이지별 보안 기준이 모호해 지거나 인증이 필요한 페이지 별로 매번 로그인 해야 하는 번거로움이 생길 수 있다.
  • 서버 측에서 user의 세션 정보를 메모리, 디스크, DB등의 저장을 하는 서버 기반 인증/인가 방식이 있고 리소스 및 효율을 개선해서 나온 token 기반의 인가 방식이 있다. access token 을 사용한 인가 방식들 중 대표적으로 JWT(JSON Web Token) 방식이 주로 많이 쓰인다(access token을 통해 해당 user가 누군지 알 수 있으므로 해당 유저의 권한(permission)도 확인이 가능하다)

access token을 통한 인가의 절차

  1. Authentication 절차를 통해(로그인 등) access token을 생성한다. access token에는 user 정보를 확인할 수 있는 정보가 들어가 있다.

  2. user가 request를 보낼 때 access token을 첨부해서 보낸다(Front-end/Client)

  3. 서버에서는 user가 보낸 access token내용을 복호화 한다

  4. 복호화된 데이터를 통해 user id나 기타 user 정보를 식별 할 수 있는 데이터를 얻는다

  5. user id를 사용해 DB에서 해당 유저의 권한(permission)을 확인한다.

  6. 해당 요청에 대한 권한을 가지고 있는 user라면 해당 요청을 처리해 준다.

  7. 권한이 없는 user라면 401에러 등으로 response를 보낸다.

access token을 사용하는 이유

  • 매번 id, password 에 대한 무거운 bcrypt 연산을 진행하면서 비교하기보다 저장할 필요 없이 가볍게 user 식별 정보를 hash암호화 하여 인가하면 된다.
  • 실제 id와 password가 cookie상에 저장되지 않고 특정 서버에 특화 되어 있기 때문에 다른 서버에서 재사용 될 수 없다.

JWT(JSON Web Token)

토큰 기반의 인증 방식 중 HTTP Authorization header나 URI query parameter등 공백을 사용할 수 있는 곳에서 사용되는 JSON 객체 방식의 토큰. 다시 말해 JWT는 (user)인증 정보를 JSON 형식으로 담고 암호화를 하여 인증이 필요한 서비스를 이용할 시 HTTP header를 통해 서버와 클라이언트 간에 JWT만 무사히 주고 받아도 특정 인가가 필요한 서비스를 지속 이용하게 해준다

JWT에 대해서 정리하는 것만 해도 꽤나 많은 것을 언급해야 하기 때문에 이 지면에선 생략하지만 해당 링크에 굉장히 잘 설명 되어 있으니 참고.

  • jwt 사용 하여 token을 만들고 decode하는 예제, 암호화 할 JSON, SECRET_KEY, 암호화 알고리즘 필요
>>> 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 방식으로 주고 받는 것을 구현할 것이다.

  • 클라이언트에서 user가 로그인을 했을 때
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 구현 코드에 인증 인가 기능을 간단히 구현해 보도록 하겠다.

Reference

profile
wanna be good developer

0개의 댓글