개발기록장

[Web] 인증과 인가(1) -인증이란? (+bcrypt) 본문

TIL/Web

[Web] 인증과 인가(1) -인증이란? (+bcrypt)

yangahh 2021. 2. 7. 16:54

 

인증과 인가는 API에서 가장 자주 구현되는 기능 가운데 하나이다.

자주 구현되는 기능이니 만큼 아주 중요한 개념이니 절대 잊지 말자

 


 

1. 인증이란?

  • 인증(Authentication)이란 유저의 identification를 확인하는 과정이다. 즉, 아이디와 비밀번호를 확인하는 절차이다.
  • 인증은 누가 우리 서비스를 쓰는지, 어떻게 쓰는지를 추적하기 위해 필요하다.
  • 인증을 하기 위해서는 먼저 유저의 아이디와 비밀번호를 생성하는 기능 그리고 그 유저의 아이디와 비밀번호를 확인하는 기능이 필요하다.
  • 쉽게 말하면 회원가입과 로그인으로 인증을 구현한다.

 

** 로그인 절차와 token

회원가입이 된 유저가 아이디, 비밀번호를 입력하면 서버에서는 입력한 정보가 DB에 저장된 정보와 일치하는지 확인을 하고,

일치하면 로그인 성공과 함께 클라이언트에 access token을 전송한다.

이렇게 로그인에 성공한 유저는 인증된 유저로써 이 서버의 로그인이 필요한 서비스를 이용할 시 request에 access token을 첨부해서 서버에 전송하여 매번 로그인해도 되지 않도록 한다.

 

인증 = who are you? 를 확인하는 절차

 

 

2. 비밀번호 관리

  • 이러한 인증에 중요한 것은 바로 비밀번호이다.
  • 비밀번호는 보안을 위해 반드시 암호화되어서 DB에 저장되어야 한다.
  • 비밀번호가 암호화되지 않은 채로 DB에 저장이 되었을 경우, DB가 해킹당하면 유저의 정보가 모두 노출될 수 있고,
  • 해킹의 경우가 아니더라도 내부 개발자 등에 의해 유저의 정보가 노출될 가능성이 있기 때문이다.

 

 

 

3. 비밀번호 암호화 방법

- 해시 함수란?

  • 임의의 길이의 데이터를 고정된 길이의 데이터로 반환시켜주는 함수이다.
  • 입력값의 길이가 달라도 출력값은 언제나 고정된 길이로 반환한다.
  • 동일한 값이 입력되면 언제나 동일한 출력 값을 보장한다.
  • 해시함수는 암호학적 해시 함수와 비 암호학적 해시함수로 구분된다.

 

- 단방향 암호화

  • 암호화에는 양방향 암호화와 단방향 암호화가 있는데, 비밀번호를 암호화할 때는 주로 단방향 암호화를 사용한다.
  • 단방향 암호화란, 평문을 암호문으로 바꾸는 '암호화'는 가능하지만, 암호문을 평문으로 바꾸는 '복호화'는 불가능한 암호화를 말한다.
  • 단방향 암호화에는 주로 해시 함수를 이용하는데 이를 단방향 해시 함수(one-way hash function)라고 한다.
  • 단방향 해시 함수는 입력값을 문자와 숫자를 임의로 나열한 일정한 길이의 다이제스트(Digest) 형태로 변환시켜준다. 여기서 다이제스트란, 해시함수를 통해 생성된 암호화된 메시지이다.
  • 단방향 해쉬함수에는 MD5, SHA-1, SHA-256 등이 있다.

 

해시 함수를 이용한 단방향 암호화

 

- 단방향 암호화의 취약점

1) 무차별 대입 공격(brute-force attack)에 취약

  • 해시 함수는 원래 짧은 시간에 데이터를 검색하기 위해 설계된 것이다. 그렇게 대문에 해시 함수는 본래 처리 속도가 최대한 빠르도록 설계되었다.
  • 이런 속성 때문에 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다. 
  • 이런 방식으로 패스워드를 추측하면 패스워드가 충분히 길거나 복잡하지 않은 경우에는 그리 긴 시간이 걸리지 않는다.

 

2) Rainbow table attack에 취약

  • 단방향 해시 함수는 같은 값을 해싱하면 언제나 같은 다이제스트가 나온다.
  • 이 점을 이용하여 사용자들이 많이 쓰는 패스워드를 미리 해싱하여 결과값들을 모아 둔 테이블을 Rainbow table이라고 한다.

 

 

- 취약점을 보안하기 위한 기법

1) Salting

  • 소금을 친다는 뜻의 salting은 이름처럼 실제 비밀번호의 앞, 뒤 아무 곳에 랜덤 데이터를 더해서 해시값을 계산하는 방법을 말한다. 
  • 사용된 salt 값은 나중에 비밀번호 일치를 확인하기 위해 같이 저장된다. 
  • Rainbow table attack을 방지할 수 있는 효과가 있다.

 

2) Key Stretching

  • 해시 암호화를 여러 번 반복하는 기법이다. 
  • 무차별 대입 공격을 방지하는 효과가 있다.

 

 

4. bcrypt를 이용한 암호화 구현

  • bcrypt는 Salting과 Key Stretching을 구현할 수 있는 라이브러리로 다양한 언어를 지원한다.
  • bcrypt는 처음부터 비밀번호를 단방향 암호화하기 위해 만들어진 해쉬 함수이며 가장 널리 쓰인다.
  • bcrypt는 hash 결과값에 salt값과 해시 값 및 반복 횟수를 같이 보관하기 때문에 비밀번호 해싱을 적용하는 데 있어 DB설계를 복잡하게 할 필요가 없어서 암호화를 구현하는데 매우 편리하다.
  • bcrypt를 통해 해싱된 결과 값(Digest)의 구조는 아래와 같다.

 

 

이제 파이썬에서 bcrypt를 이용하여 비밀번호 암호화를 구현해보자.

 

  1) 먼저 pip로 bcrypt를 설치한다.

pip install bcrypt

 

  2) '12345678'이라는 비밀번호를 암호화하는 과정은 아래와 같다.

>>> import bcrypt


### 평문
>>> password = '12345678'


### bcrypt를 이용하여 암호화 (bcrypt.gensalt()는 salt값을 만든는 함수)
>>> hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())

암호화할 때는 bcrypt의 hashpw 함수를 이용한다.

hashpw의 인자 값으로 암호화 할 패스워드와 salt값을 넣어줄 수 있는데, 이때 암호화 할 패스워드를 평문 그대로 넣으면 아래와 같은 에러가 발생한다.

에러를 읽어보면, unicode 객체는 반드시 인코딩해서 넣어야 된다고 적혀있다.

 

안내에 따라 password를 인코딩하여 다시 hashpw 함수에 적용해본 결과는 아래와 같다.

>>> hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

>>> hashed_password
b'$2b$12$XXdgXRMbmhCcXOwfZsc3XuAEGb6OC2ZrmanIrLWf9on6Z1iGHpWAm'


# 암호화 된 password를 DB에 저장하기 좋은 형태로 string으로 다시 decode
>>> hashed_password = hashed_password.decode('utf-8')

>>> hashed_password
'$2b$12$XXdgXRMbmhCcXOwfZsc3XuAEGb6OC2ZrmanIrLWf9on6Z1iGHpWAm'

hashed_password는 byte 타입으로 저장이 된다.
하지만 DB에 저장할 때는 byte가 아닌 string 타입으로 저장해줘야 하므로 다시 decode를 적용하여 DB에 저장한다.

 

 

 

  3) 암호화된 비밀번호를 입력받은 값과 일치하는지 확인하는 과정은 아래와 같다.

>>> bcrypt.checkpw('12345678'.encode('utf-8'), hashed_password.encode('utf-8'))
True
  • bcrypt의 checkpw 함수를 이용하여 입력받은 비밀번호와 DB에 저장된 암호화된 비밀번호가 일치하는지 확인할 수 있다.
  • 일치하면 True, 일치하지 않으면 False를 반환한다.
  • DB에 저장된 암호화된 비밀번호는 byte 타입이 아니므로 checkpw함수를 쓸 때도 encode을 반드시 해줘야 한다.
  • 참고로, hashed_password에는 이미 salt값이 저장되어 있기 때문에 salt값을 가져오는 과정은 추가로 실행할 필요가 없다.

 

** 나의 궁금증 : 왜 해시된 패스워드를 DB에 string 타입으로 저장해야할까??

나는 회원가입 과정에서 암호화된 비밀번호를 decode하지 않고, 로그인 과정에서 비밀번호 일치를 확인하는 부분에서 DB에 저장 된 값을 가져올 때 encode 해주지 않으면 똑같이 모두 byte타입이기 때문에 문제가 되지 않을 거라고 생각했다.

 

즉 회원가입 시에는

# 사용자에게 입력받은 값(password)를 암호화 하여 DB에 저장하는 과정
password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

이렇게 DB에 저장을 하고,

로그인 시에는

# 사용자에게 입력받은 값(password)와, DB에 저장되어 있는 값(user.password) 일치 확인
if bcrypt.checkpw(password.encode('utf-8'), user.password):
	(생략..)

이렇게 일치 여부를 확인을 하면 똑같은 거 아닌가?라고 생각을 했다.

 

하지만 이렇게 설정하고 테스트한 결과 bcrypt.checkpw에서 문제가 발생함을 알 수 있었다.

암호화된 비밀번호를 decode하지 않고 DB에 저장하면

b'$2b$12$XXdgXRMbmhCcXOwfZsc3XuAEGb6OC2ZrmanIrLWf9on6Z1iGHpWAm'

이런 식으로 저장이 되는데, DB에서는 이 자체를 그냥 String 타입으로 인식하고 저장해버린다.

 

여기까지는 문제가 없지만, bcrypt.checkpw() 메소드에 들어가는 두 인자값은 모두 byte 타입으로 들어가야 한다.

여기서 user.password가 b.~~~이긴 하지만 실제로는 이 자체로는 string이기 때문에 에러가 나는 것이다.

만일에 여기서 

if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8'):

을 한다면, TypeError는 안나지만 b.~~~~로 저장되있는 값을 encode 하기 때문에 b.b.~~~가 되어버린다.

이렇게되면 비밀번호 일치 여부를 확인할 수 없게 된다.

 

따라서 비밀번호 암호화 하여 DB에 저장해야하고, checkpw를 할 때도 encode를 해줘야 정상적인 처리를 할 수 있다. 

 

 

- 한 줄 요약

import bcrypt

# 평문
password = '12345678'

# 암호화하여 저장
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')

# 일치 여부 확인
bcrypt.checkpw('12345678'.encode('utf-8'), hashed_password.encode('utf-8'))