앞서 인증인가 절차로 회원가입&로그인 엔드포인트 구현하기 with bcrypt, JWT에서 인증 엔드포인트를 통해 access_token
을 응답 받았다.
Minitter 많은 API중 인증절차가 필요한 부분들이 있다.
바로 tweet과 follow, unfollow API들이다.
기존에는 어떤 아이디가 tweet를 올리는지, follow 하는지를 id값을 지정하여 전송하였다.
하지만 먼저 어떤 아이디인지를 확인하는 절차를 사전에 거친다면 id값을 지정할 필요없이 바로 tweet이나 follow, unfollow 절차를 밟을 수 있을 것이다. 또한 이 절차는 자주 사용되며 재사용성이 크다.
따라서 이 부분은 파이썬의 decorator
를 활용한다.
decorator
를 사용 시 우리가 앞에서 로그인하고 응답받은 access_token
을 활용하여 인증절차가 필요한 엔드포인트에서 사용하고자 한다.
decorator
를 사용하여 구현한다.decorator
에서는 로그인에서 응답받은 access_token
를 활용하여 구현한다.Decorator
는 어떠한 함수를 다른 함수가 실행되기 전에 자동으로 먼저 실행될 수 있도록 해주는 문법decorator
사용하는 함수는 함수의 이름앞에 @
를 붙여서 적용하고자 하는 함수 정의부분 웨에 지정한다.functools
모듈의 wraps
decorator함수를 활용할 수 있다.from functools import wraps # 1)
def test_decorator(): # 2)
@wraps(f) # 3)
def decoratored_function(*args, **kwargs): # 4)
print("Decorator Function") # 5)
return f(*args, **kwargs) # 6)
return decoratored_function # 6)
@test_decorator # 7)
def func(): # 8)
print("Calling func function!")
1) functools
모듈의 wraps
decorator 함수를 import한다.
2) decorator 함수를 정의한다.
decorator함수는 함수를 인자로 받아서 함수를 리턴하는 함수이다. 여기서 f
라고 이름 지어진 함수를 인자로 받는다. 그리고 이 f
함수는 해당 decorator함수가 적용되는 함수가 된다.
3) wraps
decorator 함수를 적용한다. 꼭 적용해야할 함수는 아니지만, 적용하면 부차적으로 생기는 이슈들을 해결해 준다. 자세한 내용은 파이썬 공식문서 참고...
4) 실질적인 decorator 함수이다. decorator 함수를 리턴해 줘야 함으로 nested 함수로 지정해준다.
함수의 인자가 *args
, `kwargs로
*args는 tuple형태의 인자를,
kwargs`는 dictionary형태의 인자를 받을 수 있다.
5) f
함수를 실행시켜 리턴해준다.
즉, 해당 decorator함수가 실행되고 난 후에 decorator가 적용된 함수를 호출해 주는 것이다.
6) decorator 함수를 리턴한다.
7) 2)에서 만든 decorator함수를 8)의 함수에 적용해 준다.
8) 2)에서 만든 decorator함수가 적용되는 함수다.
위 와 같이 작성된 파일을 파이썬 shell에서 불러오면 다음과 같은 결과를 확인 할 수 있다.
>>> func()
Decorator Function
Calling func function
이제 인증 절차를 위한 decorator함수를 만들어보자.
인증 엔드포인트를 통해 생성된 JWT access_token
을 프론트엔드는 HTTP요청에서 Authorization
헤더에 포함해서 보내게 된다.
decorator함수는 Authorization
헤더에서 값을 읽어서 JWT access_token
이 있는지 확인한다.
access_token
이 있으면 그 값을 복호화하여서 사용자의 id를 읽어들이고 해당 사용자의 로그인 여부를 확인한다.
import jwt
from functools import wraps
from flask import request, Response, g
def login_required(f): # 1)
@wraps(f) # 2)
def decorated_function(*args, **kwargs):
access_token = request.headers.get('Authorization') # 3)
if access_token is not None: # 4)
try:
payload = jwt.decode(access_token, current_app.config['JWT_SECRET_KEY'], 'HS256') # 5)
except jwt.InvalidTokenError:
payload = None # 6)
if payload is None: return Response(status=401) # 7)
user_id = payload['user_id'] # 8)
g.user_id = user_id
g.user = get_user(user_id) if user_id else None
else:
return Response(status = 401) # 9)
return f(*args, **kwargs)
return decorated_function
1) login_required
decorator 함수를 지정한다. 이제 login_required
decorator 함수가 적용된 함수는 해당 사용자가 로그인을 이미 한 상태에서만 실행될 수 있다.
2) functools
라이브러리의 wraps
decorator 함수를 적용해서 decorator 함수를 구현한다.
3) 전송된 HTTP 요청에서 Authorization
헤더 값을 읽어 들여 access_token
을 얻는다.
4) authorization 헤더가 전송되었다면 access_token
을 복호화하여 payload
JSON을 읽어 들이도록 한다.
만일 access_token이 None 이라면 authorization 헤더가 전송되지 않았다는 뜻으로 인증허가를 하지 않아도 된다.
5) access_token
을 복호화 하여 payload JSON을 읽어 들인다. jwt 모듈의 decode 함수는 JWT access_token
을 복호화할 뿐만 아니라 토큰이 해당 백엔드 API서버에서 생선된 token인지를 확인하는 절차를 밟는다.
그래서 'JWT_SECRET_KEY'에서 필드 값을 읽어 들여 secret key를 읽고 'HS256'으로 동일하게 복호화해서 결과가 같은지를 확인한다.
6) 만약 5)에서 exception이 일어나면, JWT 복호화에 문제가 있는 것으로 Unauthorized
401 응답을 보낸다.
7) payload
가 None
이라면 이 또한 문제가 있다는 뜻으로 Unauthorized
401 응답을 보낸다.
8) JWT에서 복호화한 payload
JSON에서 user_id
를 읽어 들인다.
그리고 해당 사용자 id를 사용해서 데이터베이스에서 사용자 정보도 읽어 들이도록 한다.
get_user_info
함수는 사용자 id를 바탕으로 데이터베이스에서 사용자 정보를 읽어 들이는 함수이다.
인증 decorator login_required 함수를 tweet, follow, unfollow 엔드포인트에 적용한다.
@app.route('/tweet', methods=['POST'])
@login_required
def tweet():
user_tweet = request.json
user_tweet['id'] = g.user_id
tweet = user_tweet['tweet']
if len(tweet) > 300:
return '300자를 초과했습니다', 400
insert_tweet(user_tweet)
return '', 200
@app.route('/follow', methods=['POST'])
@login_required
def follow():
payload = request.json
payload['id'] = g.user_id
insert_follow(payload)
return '', 200
@app.route('/unfollow', methods=['POST'])
@login_required
def unfollow():
payload = request.json
payload['id'] = g.user_id
insert_unfollow(payload)
return '', 200
...
...
route
decorator를 먼저 적용해야한다.route
와 login_required
decorator를 순서에 맞게 적용한다.tweet with access_token
$ http -v POST localhost:5000/tweet tweet="My first Decorator Login" "Authorization:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMSwiZXhwIjoxNTgwNjQ4ODE0fQ.tP7kVVyObey0_yexaCjmhwZ_y7xc_HOL1MtwnusuKRE"
POST /tweet HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMSwiZXhwIjoxNTgwNjQ4ODE0fQ.tP7kVVyObey0_yexaCjmhwZ_y7xc_HOL1MtwnusuKRE
Connection: keep-alive
Content-Length: 37
Content-Type: application/json
Host: localhost:5000
User-Agent: HTTPie/0.9.8
{
"tweet": "My first Decorator Login"
}
HTTP/1.0 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sat, 01 Feb 2020 13:08:12 GMT
Server: Werkzeug/0.16.1 Python/3.7.6
HTTP 통신은 stateless이다. 따라서 이 전 통신은 어떠한 절차를 밟는 통신인지 알수없다. 그러므로 HTTP 통신에 인증 여부 정보를 보내야되는데, 이때 사용하는 방법이 access_token
이다.
access_token
은 JWT(JSON Web Token)을 사용해서 생성될 수 있다.
JWT 생성할 때 보안상 조심해야할 부분은 사용자의 개인정보는 포함시키지 말아야 한다.
인증 절차를 엔드포인트들에 적용시키기 위해 decorator 함수를 사용할 수 있다.