INDEX



이번 포스트는 macOS Mojave 10.14.5 에서 수행된 작업입니다.

개발 환경

  • MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
  • Python 3.7
  • vscode
  • Docker-Compose version 1.23.2, build 1110ad01



Authorization 붙이기 (PyJwt), Password 암호화

먼저, 클라이언트 측에서 회원가입을 할 때, PasswordDatabase에 저장 때는 암호화 해서 넣어야 합니다
(개발자가 사용자 비밀번호 까지 알아서 뭐 할려고?)

그러기 위해서는 Bcrypt를 사용해 암호화를 진행 할 것이며, 암호화 한 것을 Database에 저장하고, 사용자가 로그인 할 때, 서버에서 발급해주는 Token을 이용해 API를 사용할 것 입니다 Token이 없다면 API의 요청은 거절 하도록 만들 것 입니다

해봅시다!

PyJwt 종속성을 설치합니다

1
> pipenv install PyJwt



데코레이터를 이용해서 구현한 API에 붙여 주도록 하곘습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# ./app/api/auth_type.py

import jwt
from flask import request, Response
from functools import wraps

SECERET_KEY = "Secret Hellow"
ACCESS_TOKEN = {
'Access Token': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization'
}
}
BASIC_AUTH = {
'Basic Auth': {
'type': 'basic',
'in': 'header',
'name': 'Authorization'
},
}

def confirm_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
access_token = request.headers['Authorization']
if access_token is not None:
try:
payload = jwt.decode(access_token, SECERET_KEY, "HS256")
except jwt.InvalidTokenError:
payload = None
if payload is None:
return Response(status=401)
user_id = payload["user_id"]
# 원하는 작업
else:
return Response(status=401)

return f(*args, **kwargs)
return decorated_function



REST API DocumentsAuthorization을 보여주기 위해 위와 같은 코드를 작성하고,
Token 발급을 위해 다음과 같이 코드를 작성하겠습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ./app/api/__init__.py

"""
API config file
"""

from flask_restplus import Api
from app.users.views import API as users_api
from app.posts.views import API as posts_api
from app.api.auth_type import ACCESS_TOKEN, BASIC_AUTH

REST_API = Api(authorizations={**ACCESS_TOKEN, **BASIC_AUTH})

REST_API.add_namespace(users_api, '/user')
REST_API.add_namespace(posts_api, '/post')



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# ./app/users/views.py

"""
User views file
"""
import jwt
import bcrypt
from http import HTTPStatus
from flask import jsonify, make_response
from sqlalchemy.exc import SQLAlchemyError
from flask_restplus import Namespace, Resource, reqparse, fields
from app.users.models import Users, UsersSchema
from app.api.database import DB
from app.api.auth_type import SECERET_KEY

API = Namespace('Users', description="User's RESTPlus - API")
USERS_SCHEMA = UsersSchema()

@API.route('s')
class UsersAuth(Resource):
parser = reqparse.RequestParser()
parser.add_argument('user_id', required=True, type=str,
help="User's ID", location='json')
parser.add_argument('user_password', required=True,
type=str, help="User's PW", location='json')
parser.add_argument('user_email', required=True, type=str,
help="User's Email", location='json')

users_field = API.model('userRegister', {
'user_id': fields.String,
'user_password': fields.String,
'user_email': fields.String
})

@API.doc('post')
@API.expect(users_field)
def post(self):
args_ = self.parser.parse_args()
password = args_['user_password']
hash_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
user = Users(args_['user_id'], hash_pw, args_['user_email'])
return user.add(user, USERS_SCHEMA)

@API.route('/auth')
class UserAuth(Resource):
parser = reqparse.RequestParser()
parser.add_argument('user_id', required=True, type=str,
help="User's ID", location='json')
parser.add_argument('user_password', required=True,
type=str, help="User's PW", location='json')

user_login_field = API.model('userLogin', {
'user_id': fields.String,
'user_password': fields.String
})

@API.doc('post')
@API.expect(user_login_field)
def post(self):
args_ = self.parser.parse_args()
try:
user = Users.query.filter(Users.user_id == args_['user_id']).first()
if bcrypt.checkpw(args_['user_password'].encode('utf-8'), user.user_password.encode('utf-8')):
# token 발급
payload = {
'user_id' : user.user_id
}
token = jwt.encode(payload, SECERET_KEY, "HS256")
body = jsonify({'access_token': token.decode('utf-8'),'user': user.id})
if user:
code = HTTPStatus.OK
else:
code = HTTPStatus.NOT_FOUND
except SQLAlchemyError as err:
body = jsonify({'message': str(err)})
code = HTTPStatus.INTERNAL_SERVER_ERROR
return make_response(body, code.value)

발급한 Token을 클라이언트 측에서 저장을 한 뒤 발급받은 토큰을 서버로 Header 부분에 담아 데이터를 전송하여 맞으면 API요청을 받는 코드를 작성하도록 하겠습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# /app/posts/views.py

"""
Posts view file
"""

from flask_restplus import Namespace, Resource, reqparse, fields
from flask import jsonify, make_response
from http import HTTPStatus
from sqlalchemy.exc import SQLAlchemyError
from app.posts.models import Posts, PostsSchema
from app.users.models import Users, UsersSchema
from app.api.database import DB
from app.api.auth_type import confirm_token, ACCESS_TOKEN, BASIC_AUTH

API = Namespace('Posts', description="Post's REST API")
POSTS_SCHEMA = PostsSchema()

@API.route('s')
class Post(Resource):
parser = reqparse.RequestParser()
parser.add_argument('author_id', required=True, type=str, help="Post's author ID", location='json')
parser.add_argument('title', required=True, type=str, help="Post's title", location='json')
parser.add_argument('body', required=True, type=str, help="Post's body", location='json')

post_field = API.model('Post', {
'author_id': fields.String,
'title': fields.String,
'body': fields.String
})

@API.doc('get')
def get(self):
try:
posts = Posts.query.all()
body = jsonify(POSTS_SCHEMA.dump(posts, many=True).data)
if posts:
code = HTTPStatus.OK
else:
code = HTTPStatus.NOT_FOUND
except SQLAlchemyError as err:
body = jsonify({'message' : str(err)})
code = HTTPStatus.INTERNAL_SERVER_ERROR
return make_response(body, code.value)

@API.expect(post_field)
@confirm_token
@API.doc('post', security=ACCESS_TOKEN)
def post(self):
args_ = self.parser.parse_args()
post = Posts(author_id=args_['author_id'], title=args_['title'], body=args_['body'])
return post.add(post, POSTS_SCHEMA)

@API.route('/<int:reqno>')
class PostItem(Resource):
@confirm_token
@API.doc('get', security=ACCESS_TOKEN)
def get(self, reqno):
try:
post = DB.session.query(Posts).outerjoin(
Users, Users.user_id == Posts.author_id).filter(Posts.id==reqno).first()
body = POSTS_SCHEMA.dump(post).data
if post:
code = HTTPStatus.OK
else:
code = HTTPStatus.NOT_FOUND
except SQLAlchemyError as err:
body = jsonify({'message' : str(err)})
code = HTTPStatus.INTERNAL_SERVER_ERROR
return make_response(body, code.value)

Post Classpost함수를 보게 되면 데코레이터confirm_token 을 붙인 것을 볼 수 있습니다 이 API로 요청이 왔을 때 먼저 토큰이 있는지 검사하고 통과 되면 Post함수를 실행하게 되는 구조입니다


실행을 해봅시다

1
$ python manage.py run




아래와 같은 화면을 볼 수 있으며 각각 실행을 해보세요!



최종적인 폴더 구조는 다음과 같습니다





고생하셨습니다! 지금까지 따라와주셔서 정말 감사드리며 여기까지 하게 되면 Flask 프레임워크의 기본을 할 수 있게 된 것 입니다!



INDEX