TIL DAY 18 || Django Authentication using Bcrypt

Authentication

유저의 identification 을 확인하는 절차를 인증(Authentication) 이라고 한다.

로그인 절차

  1. 유저가 아이디와 패스워드 생성
  2. 서버는 유저 패스워드를 암호화 해서 DB에 저장
  3. 유저가 아이디와 패스워드를 입력하여 로그인 시도
  4. 유저가 입력한 패스워드를 암호화 한 것과 암호화되서 DB에 저장된 유저의 비밀번호와 비교.
  5. 일치하면 로그인 성공
  6. 로그인 성공하면 access token을 클라이언트에게 전송.
  7. 유저는 로그인 성공으로 부터 받아온 access token 을 request header 에 넣어 서버에 전송함으로서 매번 로그인 해도 되지 않도록 한다. (stateless 한 HTTP 의 특징)

패스워드 암호화(Hashing)

패스워드를 암호화 하는 방식에는 여러가지가 있지만 우리가 흔히 쓰는 방식은 단방향 암호화 이다.

양방향 암호화는 해쉬된 값을 복호화 할 수 있지만 단방향 암호화는 복호화가 불가능하다.

우리가 사이트 비밀번호를 까먹었을 때 비밀번호 찾기를 누르면 원래 비밀번호를 알려주는게 아니라 새로 다시만들라고 하는 것도, 복호화가 불가능하기 때문에 아예 새로 만들라고 하는 것이다.

avalance 한 성격 또한 단방향 암호화의 특징이다.
'test' 를 해쉬했을 때와, 'test1' 을 해쉬했을 때의 값은 전혀 비슷하지 않고 완전히 다른데 이를 avalance 한 효과 라고 한다.

Bcrypt

물론 단방향 암호화에도 단점이 존재하긴 한다.

  • Rainbow table attack 이라고 해서 미리 해쉬값들을 계산해 놓은 테이블을 Rainbow table 이라고 한다.

  • 해시 함수는 원래 패스워드를 저장하기 위해서 설계된 것이 아니라 짧은 시간에 데이터를 검색하기 위해 설계된 것

  • 처리 속도가 최대한 빠르도록 설계됨

  • 덕분에 공격자는 매우 빠른 속도로 Rainbow table 의 해쉬된 값들과 해킹할 대상의 암호화 된 다이제스트를 비교 가능

  • MD5를 사용한 경우 일반적인 장비를 이용하여 1초당 56억 개의 다이제스트를 대입 가능

  • 따라서 salting 과 key stretching 기법을 이용하여 이런 취약점을 보완할 수 있다.

Salting

실제 패스워드 이외에 추가적으로 랜덤 데이터를 넣어서 함께 해시하는 방법이다.

Key Stretching

이미 한번 계산 된 단방향 해쉬 값을 해쉬하고 또 해쉬하고 반복하는 방법이다.

해쉬 반복이 추가될 때 마다 다이제스트 값을 비교하는 속도가 엄청나게 느려진다.

Salting + Key Stretching

Salting 과 Key Stretching 로직을 동시에 수행하도록 구현한 해쉬 함수중 가장 널리 사용되는 것이 bcrypt 이다.

Sign-up - password storing

  • bcrypt.gensalt() 로 랜덤으로 생성한 Salt 값과
  • bytes 로 인코딩 된 password 와 함께 해시한다.
  • 해시된 값을 다시 utf-8 로 encoding 하여 str 로 db에 저장한다.
  • 패스워드 저장 공간을 넉넉하게 charfield(max_length=100) 으로 설정해주었다.

Login - password comparing

  • bcrypt.checkpw() 를 통해 입력받은 utf-8 string 값을 bytes 로 encoding 해서 첫 번째 인자로 넣고, db 에 있는 hash 된 string 값을 다시 bytes 로 encoding 하여 두번 째 인자로 넣어 비교한다.
  • 이 때 bcrypt 는 해쉬된 값에서 salt 값을 따로 가져 온 뒤 첫번 째 인자에 salt 를 넣고 같은 알고리즘으로 해시해서 결과값을 비교한다.

마주한 문제들

db 내부에 패스워드를 db 내에서 비교하지 않고,
db 에서 패스워드를 꺼낸 다음에 값을 비교해야 되는 상황이기 때문에

다음과 같이 User instance 를 가져온다음,
해당하는 password 를 bcrypt.checkpw() 메소드로 비교할 계획이였다.

User.objects.filter(Q(email=email) | Q(username=username) | Q(phone_number=phone_number))

이 과정을 수행하던 중 문제가 발생했다.

원래는 username, email, phone_number 셋 중 하나만 입력해도 로그인에 성공할 수 있게 하려고 했었다.

그래서 테스트를 해보던 중

username=<유저이름>
phone_number=''
을 넘겨보았을 때 문제가 발생했다.

한 명의 유저 객체만 가져와야 하는데 여러 객체가 반환이 되었다.

phone_number 컬럼의 옵션이 unique=True 이지만 동시에 null=True 인 경우

phone_number 를 등록하지 않은 유저의 phone_number 값은 db 상에서 null 이다.

따라서 만약 phone_number 를 입력을 받지 않았는데
Q(phone_number=None) 또는
Q(phone_number='')

을 수행하면 phone_number 를 등록하지 않은 유저가 몽땅 filter 에 걸리게 된다.

그래서 다음과 같이 email, username, phone_number 별로 해서 User 객체를 가져오면 그 객체를 사용하는 것으로 로직을 변경했다.

좋은 웹페이지 즐겨찾기