이전 블로그 중 인증(Authentication) & 인가(Authorization)에서 인증절차와 인가에 대해서 알아보았다.
특히 인증을 위해서는 bcrypt
알고리즘을 사용하여 회원의 패스워드를 단방향 암호화하였다.
이번에는 Flask에서 인증부분을 회원가입 엔드포인트에 연결 및 구현하고자 한다.
기존에 있던 회원가입 API 즉 sign-up 엔드포인트 부분에서 password
부분을 bcrypt
알고리즘과 hashpw 함수를 넣어 수정한다.
import bcrypt # 1)
...
...
@app.route("/sign-up", methods=['POST'])
def sign_up():
new_user = request.json
new_user['password'] = bcrypt.hashpw( # 2)
new_user['password'].encode('UTF-8'),
bcrypt.gensalt()
)
print(new_user['password'])
new_user_id = app.database.execute(text("""
INSERT INTO users (
name,
email,
profile,
hashed_password
) VALUES (
:name,
:email,
:profile,
:password
)
"""), new_user).lastrowid
new_user_info = get_user(new_user_id)
return jsonify(new_user_info)
...
...
1) bcrypt 모듈을 import한다.
2) bcrypt 모듈을 사용하여 사용자의 비밀번호를 암호화한다.
앞서 인증(Authentication) & 인가(Authorization)에서 알아보았던, bcrypt 모듈의 hashpw 함수는 string 값이 아닌 byte의 값을 받아야 함으로 utf-8
로 encoding해주고 추가로 salting
을 붙여서 암호화한다.
Http 테스트
$ http -v POST localhost:5000/sign-up name=비크립트 email=bcrypt@gmail.com password=12345678 profile="bcrypt test"
POST /sign-up HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 115
Content-Type: application/json
Host: localhost:5000
User-Agent: HTTPie/0.9.8
{
"email": "bcrypt@gmail.com",
"name": "비크립트",
"password": "12345678",
"profile": "bcrypt test"
}
HTTP/1.0 200 OK
Content-Length: 115
Content-Type: application/json
Date: Sat, 01 Feb 2020 07:36:17 GMT
Server: Werkzeug/0.16.1 Python/3.7.6
{
"email": "bcrypt@gmail.com",
"id": 11,
"name": "비크립트",
"profile": "bcrypt test"
}
MySQL 확인
mysql> SELECT * FROM users WHERE name='비크립트';
+----+--------------+------------------+--------------------------------------------------------------+-------------+---------------------+------------+
| id | name | email | hashed_password | profile | created_at | updated_at |
+----+--------------+------------------+--------------------------------------------------------------+-------------+---------------------+------------+
| 11 | 비크립트 | bcrypt@gmail.com | $2b$12$dnUKSp29wRtk/LKkqAASjeljqAFFdHDjC8iTolVBb0wCjzBYqly9m | bcrypt test | 2020-02-01 16:36:17 | NULL |
+----+--------------+------------------+--------------------------------------------------------------+-------------+---------------------+------------+
1 row in set (0.00 sec)
mysql>
이전에 Minitter를 구현해보면서 구현하지 않은 API가 있다. 바로 로그인 API이다.
로그인 API는 사전에 회원가입 한 유저들이 요청하는 응답하는 엔드포인트로써, 회원가입시 보낸던 유저의 email과 password를 받고 Database에 저장되어 있는지 그리고 저장된 값과 일치하는 지 확인 한다. 그리고 이후 인가되는 부분에서 활용할 JWT(Json Web Token)을 응답하여 준다.
# 받은 정보가 DB에 있고 일치할 때 JWT발급
{
"access_token" : "dcnaklsdcnkla.aksdcnklsdcn.alksdcnklsnadlc"
}
# 받은 정보가 DB없을 때
{
401 Unauthorized
}
import bcrypt
import jwt # 1)
from datetime import datetime, timedelta
...
...
@app.route('/login', methods=['POST'])
def login():
data = request.json
email = data['email'] # 2)
password = data['password'] # 3)
row = database.execute(text(""" # 4)
SELECT
id,
hashed_password
FROM users
WHERE email = :email
"""), {'email' : email}).fetchone()
if row and bcrypt.checkpw(password.encode('UTF-8'),
row['hashed_password'].encode('UTF-8')): # 5)
user_id = row['id']
payload = { # 6)
'user_id' : user_id,
'exp' : datetime.utcnow() + timedelta(seconds = 60 * 60 * 24)
}
token = jwt.encode(payload, app.config['JWT_SECRET_KEY'], 'HS256') # 7)
return jsonify({
'acces_token' : token.decode('UTF-8') # 8)
})
else:
return '', 401 # 9)
...
...
1) jwt 모듈을 import한다
2) HTTP 요청(request)으로 전송된 JSON body에서 사용자의 이메일을 읽어 들인다.
3) HTTP 요청으로 전송된 JSON body에서 사용자의 비밀번호를 읽어 들인다.
4) 2)에서 읽어 들인 사용자의 이메일을 사용하여 데이터베이스에서 해당 사용자의 암호화된 비밀번호를 읽어 들인다.
5) 4)에서 읽어 들인 사용자의 암호화된 이메일과 2)에서 읽어 들인 사용자의 비밀번호가 일치하는 지 확인한다.
먼저, row가 None이면, 해당 사용자가 DB에 존재하지 않는 다는 뜻이므로 인증 허가를 안해주면된다.
만일 사용자가 존재한다면, 해당 사용자가의 암호화된 비밀번호와 사용자가 전송한 비밀번호를 bcrypt
모듈의 checkpw
함수를 사용해서 사용자가 전송한 비밀번호를 동일하게 암호화하여 동일한지 확인한다.
6) 사용자의 비밀번호가 확인 되었으면, DB상의 id에 해당하는 user_id
, 그리고 해당 JWT의 유효기간 부분인 exp
를 payload
에 넣어준다.
7) 6)에서 생성한 payload
와 config.py에서 위 내용과 미리 설정한 JWT_SECRET_KEY
를 가져오고 HS256
으로 JWT 암호화한다.
8) 7)에서 생성한 JWT를 HTTP 응답으로 전송한다.
9) 만일 5)에서 사용자가 존재하지 않거나 사용자의 비밀번호가 틀리면 Unauthorized
401 status의 HTTP 응답을 보낸다.
# 로그인 성공
$ http -v POST localhost:5000/login email=bcrypt@gmail.com password=12345678
POST /login HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 53
Content-Type: application/json
Host: localhost:5000
User-Agent: HTTPie/0.9.8
{
"email": "bcrypt@gmail.com",
"password": "12345678"
}
HTTP/1.0 200 OK
Content-Length: 147
Content-Type: application/json
Date: Sat, 01 Feb 2020 09:42:37 GMT
Server: Werkzeug/0.16.1 Python/3.7.6
{
"acces_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMSwiZXhwIjoxNTgwNjM2NTU3fQ.bKyaSjsLGdZRPJDypDYHF1kPox_vWQzE583uW4R9PhM"
}
## 로그인 실패
$ http -v POST localhost:5000/login email=bcrypt@gmail.com password=1234567
POST /login HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 52
Content-Type: application/json
Host: localhost:5000
User-Agent: HTTPie/0.9.8
{
"email": "bcrypt@gmail.com",
"password": "1234567"
}
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sat, 01 Feb 2020 09:42:41 GMT
Server: Werkzeug/0.16.1 Python/3.7.6