Flask Framework를 사용하여 API서버를 만들어보는 과정에서 HTTP 통신을 수행할 때, 전달되는 QueryParams 혹은 Body의 데이터를 어떻게 검증할 수 있을까
생각할 수 있었다.
서비스코드에서 제한하고있는 범위를 벗어난 사용자 정보가 들어올 경우 의도하지 않은 상황이 벌어질 수 있고, 예외처리가 안되어있을 경우 높은 확률로 서비스 장애가 발생할 수 있다.
유저가 항상 올바른 값을 줄 것이라는 생각은 배제하고 시작한다
위와 같은이유로 클라이언트로부터 전달되는 데이터의 유효성 검증을 위해 Flask-WTF 확장 패키지를 사용하게되었고 전달되는 데이터를 좀 더 안전하게 다룰 수 있기까지의 과정을 문서로 작성합니다.
파이썬 웹 개발환경에서 Form을 유연하게 생성하거나 유효성검사를 하기위한 목적으로 생성된 WTForms 패키지를 Flask에서 편하게 사용할 수 있도록 배포된 패키지이다
API를 통해 사용자 정보를 받아야하는 상황에서 아래의 요구사항이 있다.
field | type | required | description |
---|---|---|---|
name | String | True | 이름 |
age | Integer | True | 나이 |
String | True | 이메일 |
이름과 나이는 필수로 입력되어야하며, 이메일은 아무래도 상관없다.
from email.utils import parseaddr
@app.route('/user', methods=["POST"])
def get_user():
body = request.get_json()
name = str(body.get("name"))
age = body.get("age")
email = str(body.get("email"))
if (not name) or (not age):
return {"error": "name or age is missing"}, 400
try:
age = int(age)
except ValueError:
return {"error": "age is not a number"}, 400
email = parseaddr(email)[1]
# 대략 여기서 유저정보를 저장하던 뭘하던 지지고 볶는
# 서비스 로직이 들어가면 되지 않을까?
response = {
"name": name,
"age": age,
"email": email,
}
return response, 200
요구사항을보고 나름대로 예외처리 하면서 코드를 작성하고 실제 작동하는 것 까지 확인했지만 이 코드도 완벽하지 않을 수 있다.
만약 받아야하는 데이터가 많아지거나 조건이 까다로워지면 로직이 복잡해지고, 서비스코드에서 유효성검사를 위한 코드 라인 수가 늘어나버리니 정작 중요한 서비스 로직을 보기도 전에 머릿속이 복잡해질 것 같다.
Flask-WTF를 사용해보았는데 생각보다 괜찮다.
FlaskForm Class를 정의하고 View 영역 안에서 요청정보를 간편하게 검증할 수 있다.
위와 같은 상황에 따라 사용법이 조금 달라지는데, 예시로 다뤄보려고 한다.
pip install Flask-WTF
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired
class MyValidator(FlaskForm):
name = StringField("name", validators=[DataRequired()])
age = IntegerField("age", validators=[DataRequired()])
email = StringField("email")
아래에서 설명될 예시에서 유효성 검사시 공통으로 사용할 클래스를 생성해준다.
StringField
, IntegerField
, ...DataRequired
를 인자로 전달하여 필수 요소 여부 파악 가능하다.import json
from flask import request
@app.route("/user-with-wtf", methods=["GET"])
def get_user_with_wtf():
body = MyValidator(request.args, meta={"csrf": False})
if body.validate():
return json.dumps(body.data), 200
return json.dumps(body.errors), 400
import json
@app.route("/user-with-wtf", methods=["POST"])
def get_user_with_wtf():
body = MyValidator(meta={"csrf": False})
if body.validate():
return json.dumps(body.data), 200
return json.dumps(body.errors), 400
클라이언트 영역까지 생성해줘야하기 때문에, API 서버를 만들 때보다 조금 더 번거롭다.
app.py: Form 생성 부분
from flask import render_template
@app.route("/lab", methods=["GET"])
def render_wtf_lab_page():
return render_template("wtf-lab.html", form=MyValidator())
이전에 생성한 MyValidator
클래스를 form 인자로 전달하면 form을 동적으로 생성할 수 있다.
templates/wtf-lab.html
<html>
<head>
<title>wtf-lab</title>
</head>
<body>
<form method="POST" action="/wtf/user-form-with-wtf">
{{form.csrf_token}}
{{form.name.label}} {{form.name(size=20)}}
<br>
{{form.age.label}} {{form.age}}
<br>
{{form.email.label}} {{form.email(size=128)}}
<br>
<input type="submit">
</form>
</body>
</html>
페이지에 접속할 때 Pre-flight 연결이 완료되면 클라이언트의 헤더에는 CSRF 토큰이 생성되는데, Form 데이터를 전달하는 사람이 데이터를 들고올 때 미리 줬던 토큰을 들고오는지 인증하기 위함이다.
결국 전송되는 데이터의 무결성 검증을 위해 CSRF 토큰을 전달하는 것으로 봐도 무관한데.. 자세한 내용은 이글의 주제와 벗어나기 때문에 링크로 대신한다.
app.py: Form 요청 받는 부분
import json
@app.route("/user-form-with-wtf", methods=["POST"])
def get_user_form_with_wtf():
form = MyValidator(request.form)
if form.validate_on_submit():
return json.dumps(form.data), 200
return json.dumps(form.errors), 400
어찌되던간에 위에서 만든 Form에서 데이터가 들어오는 경우 미리 만들어둔 MyValidator
를 기준으로 데이터를 검증한다.
API 요청을 받을 때와 다르게 meta={"csrf": False}
는 사용하지 않는다.
Flask를 꼭 써야하는 환경이라면 Flask-WTF는 정말 좋은 선택인 것 같다.
여담이지만 FastAPI는 이러한 기능이 자체적으로 내장되어있고 API 문서 자동생성, 타입정의 및 기타 부가기능이 더 강력하기 때문에 Flask를 사용하는 개발자들이 FastAPI로 마이그레이션 하고있지 않을까 생각이 든다.
작업하면서 작성된 코드는 Git Repo에 올려두었다.
apps/wtfLab.py
flask-skeleton/ PR