Flask - 인증인가 절차로 회원가입&로그인 엔드포인트 구현하기 with bcrypt, JWT

황인용·2020년 1월 31일
3

Flask

목록 보기
9/13

이전 블로그 중 인증(Authentication) & 인가(Authorization)에서 인증절차와 인가에 대해서 알아보았다.

특히 인증을 위해서는 bcrypt알고리즘을 사용하여 회원의 패스워드를 단방향 암호화하였다.

이번에는 Flask에서 인증부분을 회원가입 엔드포인트에 연결 및 구현하고자 한다.

회원가입 API with bcrypt

회원가입 API 구현

기존에 있던 회원가입 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을 붙여서 암호화한다.

회원가입 API 테스트

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> 

로그인 API with bcrypt

이전에 Minitter를 구현해보면서 구현하지 않은 API가 있다. 바로 로그인 API이다.
로그인 API는 사전에 회원가입 한 유저들이 요청하는 응답하는 엔드포인트로써, 회원가입시 보낸던 유저의 email과 password를 받고 Database에 저장되어 있는지 그리고 저장된 값과 일치하는 지 확인 한다. 그리고 이후 인가되는 부분에서 활용할 JWT(Json Web Token)을 응답하여 준다.

  • 회원가입 해당한 유저의 정보를 POST로 요청(Request)받음
    - email, password 등등
  • 받은 정보가 데이터베이스에 있는지 확인
    - 없으면 401 Unauthorized Error
    • 있으면 받은 정보가 일치하는지 확인
  • 받은 정보가 데이터베이스에 있고 일치하면 JWT 발급 및 응답(Response)로 보냄

로그인 API 설계

  • 로그인 API 주소 : /login
  • 로그인 엔드포인트 method : POST
  • 로그인 엔드포인트 호출할 때 전송되는 JSON데이터 형식
# 받은 정보가 DB에 있고 일치할 때 JWT발급
{
	"access_token" : "dcnaklsdcnkla.aksdcnklsdcn.alksdcnklsnadlc"
}
#  받은 정보가 DB없을 때
{
	401 Unauthorized
}

로그인 API 구현

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의 유효기간 부분인 exppayload에 넣어준다.

7) 6)에서 생성한 payload와 config.py에서 위 내용과 미리 설정한 JWT_SECRET_KEY를 가져오고 HS256으로 JWT 암호화한다.

8) 7)에서 생성한 JWT를 HTTP 응답으로 전송한다.

9) 만일 5)에서 사용자가 존재하지 않거나 사용자의 비밀번호가 틀리면 Unauthorized 401 status의 HTTP 응답을 보낸다.

로그인 API 테스트

# 로그인 성공
$ 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

profile
dev_pang의 pang.log

0개의 댓글