Flask-WTF 在 csrf_enabled 为真时抛出错误(SECRET_KEY 已设置)
Flask-WTF throws error when csrf_enabled is True (SECRET_KEY is set)
我 运行 遇到了一个关于 Flask-WTF 的 csrf 保护的问题。
当表单像这样实例化时:
uform = UserForm(csrf_enabled=False)
一切都按预期工作,并且表单正确显示。然而:
uform = UserForm()
导致类型错误:
Traceback (most recent call last):
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2463, in __call__
return self.wsgi_app(environ, start_response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2449, in wsgi_app
response = self.handle_exception(e)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1952, in full_dispatch_request
return self.finalize_request(rv)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1969, in finalize_request
response = self.process_response(response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2268, in process_response
self.session_interface.save_session(self, ctx.session, response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/wrappers/base_response.py", line 481, in set_cookie
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/http.py", line 1163, in dump_cookie
buf = [key + b"=" + _cookie_quote(value)]
TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
A SECRET_KEY 已设置(WTF_CSRF_SECRET_KEY 也是如此,只是为了确保 Flask-WTF 没有预料到它,即使根据文档它是可选的)。
这是(相关)代码的其余部分:
admin/routes.py
from flask import current_app as app
from .. import db
from ..models import User
from .forms import UserForm
# Set up a Blueprint
admin_bp = Blueprint('admin_bp', __name__, template_folder='templates', static_folder='static')
@admin_bp.route("users/add/", methods=["GET", "POST"])
def add_user():
#Add user to the DB
add_user = True
#uform = UserForm(csrf_enabled=True)
uform = UserForm()
if uform.validate_on_submit():
user = User(username=uform.username.data, email=uform.email.data, admin=uform.admin.data, address=uform.address.data, delivery=uform.delivery.data)
user.pwhash(uform.password.data)
try:
db.session.add(user)
db.session.commit()
flash("Benutzer '", user.username, "' erfolgreich hinzugef gt.")
except:
flash("Fehler: Benutzer konnte nicht hinzugef gt werden. Existiert Benutzer bereits?")
return redirect(url_for("admin_bp.users"))
return render_template("/user.html", action="Hinzuf gen", add_user=add_user, uform=uform, title="Benutzer hinzuf gen")
admin/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField
from wtforms.validators import DataRequired, Email
class UserForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
email = StringField("Email", validators=[DataRequired(), Email()])
password = StringField("Neues Passwort")
address = StringField("Adresse")
admin = BooleanField("Admin?")
delivery = SelectField("Delivery", choices=[("1", "test1"), ("2", "test2")])
submit = SubmitField("OK")
__init__.py:
from flask_sqlalchemy import SQLAlchemy
from flask_static_compress import FlaskStaticCompress
from flask_bootstrap import Bootstrap
from flask_wtf.csrf import CSRFProtect
# Globally accessible libraries
db = SQLAlchemy()
bs = Bootstrap()
csrf = CSRFProtect()
def create_app():
app = Flask(__name__, instance_relative_config=False, static_folder="static", template_folder="templates")
from config import Config
app.config.from_object('config.DevConfig')
db.init_app(app)
bs.init_app(app)
csrf.init_app(app)
with app.app_context():
from .models import Image, Gallery, ImageGalleryMap, Delivery, ImageDeliveryMap, Blogpost, User, Log
db.create_all()
compress = FlaskStaticCompress(app)
# Register Blueprints
from website.admin.routes import admin_bp
from website.delivery.routes import delivery_bp
from website.landing.routes import landing_bp
from website.public.routes import public_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(delivery_bp, url_prefix='/delivery')
app.register_blueprint(landing_bp, url_prefix='/landing')
app.register_blueprint(public_bp, url_prefix='/')
return app
有人知道如何解决这个问题吗?
出于(非常明显的)原因,我不太热衷于设置 WTF_CSRF_ENABLED=False...
我可以复制您在示例中给出的 Traceback 的唯一方法是 app.secret_key
有效,但 app.session_cookie_name
设置为 None
。
请看下面我用来测试的脚本。 运行 此脚本和访问 http://127.0.0.1:5000
会引发 TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
,这正是您在上面发布的异常。如果未设置密钥,当我们尝试实例化表单时,在 flask 尝试服务它提出的请求之前,我们会得到一个不同的错误 KeyError: 'A secret key is required to use CSRF.'
。这是脚本:
from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
app = Flask(__name__)
# comment out below and a KeyError is raised, not a TypeError.
# So the fact that the code raises the TypeError from trying to generate
# a cookie means that the secret key is set, the form is instantiating and
# flask is trying to serve the request. So we can rule out this being a
# problem with secret key not set.
app.secret_key = "lskdjflksdj"
# comment out below and the app runs
app.session_cookie_name = None
class MyForm(FlaskForm):
a_str = StringField()
submit = SubmitField()
template = """
<form action="" method="POST">
{{form.a_str()}}
{{form.submit()}}
</form>
"""
@app.route("/", methods=["GET", "POST"])
def route():
# setting csrf_enabled=False makes the problem go away.
form = MyForm(csrf_enabled=True)
return render_template_string(template, form=form)
if __name__ == "__main__":
app.run(debug=True)
那么为什么在表单上设置csrf_enabled=False
会停止错误呢?发生错误时 flask 生成的令牌是 csrf 令牌。当我们在表单上禁用 csrf 检查时,不再需要生成该令牌并且我们不会 运行 进入错误。
事实上,在表单上切换 csrf 保护与是否引发错误有直接关系,这确实使它看起来像是密钥配置的问题,但真正的问题是为什么 key
参数传入dump_cookie()
NoneType
?
如果您遵循 Traceback,您会看到 flask 库中的最后一次调用在这里:File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
。这是 the source:
response.set_cookie(
name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)
您可以 see for yourself 上面传递给 set_cookie
的第一个参数被传递给 werkzeug.wrappers.base_response.set_cookie
中的 key
参数,它本身被传递给 key
werkzeug.http.dump_cookie
中的参数(即错误消息中的 None
)。
上面 save_session
片段中 name
的值是 defined earlier in the function as name = self.get_cookie_name(app)
, and the body of the get_cookie_name
method 就是 return app.session_cookie_name
.
app.session_cookie_name
的默认值是 "session"
,但在您的配置中它被 None
覆盖了。
我 运行 遇到了一个关于 Flask-WTF 的 csrf 保护的问题。
当表单像这样实例化时:
uform = UserForm(csrf_enabled=False)
一切都按预期工作,并且表单正确显示。然而:
uform = UserForm()
导致类型错误:
Traceback (most recent call last):
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2463, in __call__
return self.wsgi_app(environ, start_response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2449, in wsgi_app
response = self.handle_exception(e)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1952, in full_dispatch_request
return self.finalize_request(rv)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1969, in finalize_request
response = self.process_response(response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2268, in process_response
self.session_interface.save_session(self, ctx.session, response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/wrappers/base_response.py", line 481, in set_cookie
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/http.py", line 1163, in dump_cookie
buf = [key + b"=" + _cookie_quote(value)]
TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
A SECRET_KEY 已设置(WTF_CSRF_SECRET_KEY 也是如此,只是为了确保 Flask-WTF 没有预料到它,即使根据文档它是可选的)。
这是(相关)代码的其余部分:
admin/routes.py
from flask import current_app as app
from .. import db
from ..models import User
from .forms import UserForm
# Set up a Blueprint
admin_bp = Blueprint('admin_bp', __name__, template_folder='templates', static_folder='static')
@admin_bp.route("users/add/", methods=["GET", "POST"])
def add_user():
#Add user to the DB
add_user = True
#uform = UserForm(csrf_enabled=True)
uform = UserForm()
if uform.validate_on_submit():
user = User(username=uform.username.data, email=uform.email.data, admin=uform.admin.data, address=uform.address.data, delivery=uform.delivery.data)
user.pwhash(uform.password.data)
try:
db.session.add(user)
db.session.commit()
flash("Benutzer '", user.username, "' erfolgreich hinzugef gt.")
except:
flash("Fehler: Benutzer konnte nicht hinzugef gt werden. Existiert Benutzer bereits?")
return redirect(url_for("admin_bp.users"))
return render_template("/user.html", action="Hinzuf gen", add_user=add_user, uform=uform, title="Benutzer hinzuf gen")
admin/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField
from wtforms.validators import DataRequired, Email
class UserForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
email = StringField("Email", validators=[DataRequired(), Email()])
password = StringField("Neues Passwort")
address = StringField("Adresse")
admin = BooleanField("Admin?")
delivery = SelectField("Delivery", choices=[("1", "test1"), ("2", "test2")])
submit = SubmitField("OK")
__init__.py:
from flask_sqlalchemy import SQLAlchemy
from flask_static_compress import FlaskStaticCompress
from flask_bootstrap import Bootstrap
from flask_wtf.csrf import CSRFProtect
# Globally accessible libraries
db = SQLAlchemy()
bs = Bootstrap()
csrf = CSRFProtect()
def create_app():
app = Flask(__name__, instance_relative_config=False, static_folder="static", template_folder="templates")
from config import Config
app.config.from_object('config.DevConfig')
db.init_app(app)
bs.init_app(app)
csrf.init_app(app)
with app.app_context():
from .models import Image, Gallery, ImageGalleryMap, Delivery, ImageDeliveryMap, Blogpost, User, Log
db.create_all()
compress = FlaskStaticCompress(app)
# Register Blueprints
from website.admin.routes import admin_bp
from website.delivery.routes import delivery_bp
from website.landing.routes import landing_bp
from website.public.routes import public_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(delivery_bp, url_prefix='/delivery')
app.register_blueprint(landing_bp, url_prefix='/landing')
app.register_blueprint(public_bp, url_prefix='/')
return app
有人知道如何解决这个问题吗? 出于(非常明显的)原因,我不太热衷于设置 WTF_CSRF_ENABLED=False...
我可以复制您在示例中给出的 Traceback 的唯一方法是 app.secret_key
有效,但 app.session_cookie_name
设置为 None
。
请看下面我用来测试的脚本。 运行 此脚本和访问 http://127.0.0.1:5000
会引发 TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
,这正是您在上面发布的异常。如果未设置密钥,当我们尝试实例化表单时,在 flask 尝试服务它提出的请求之前,我们会得到一个不同的错误 KeyError: 'A secret key is required to use CSRF.'
。这是脚本:
from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
app = Flask(__name__)
# comment out below and a KeyError is raised, not a TypeError.
# So the fact that the code raises the TypeError from trying to generate
# a cookie means that the secret key is set, the form is instantiating and
# flask is trying to serve the request. So we can rule out this being a
# problem with secret key not set.
app.secret_key = "lskdjflksdj"
# comment out below and the app runs
app.session_cookie_name = None
class MyForm(FlaskForm):
a_str = StringField()
submit = SubmitField()
template = """
<form action="" method="POST">
{{form.a_str()}}
{{form.submit()}}
</form>
"""
@app.route("/", methods=["GET", "POST"])
def route():
# setting csrf_enabled=False makes the problem go away.
form = MyForm(csrf_enabled=True)
return render_template_string(template, form=form)
if __name__ == "__main__":
app.run(debug=True)
那么为什么在表单上设置csrf_enabled=False
会停止错误呢?发生错误时 flask 生成的令牌是 csrf 令牌。当我们在表单上禁用 csrf 检查时,不再需要生成该令牌并且我们不会 运行 进入错误。
事实上,在表单上切换 csrf 保护与是否引发错误有直接关系,这确实使它看起来像是密钥配置的问题,但真正的问题是为什么 key
参数传入dump_cookie()
NoneType
?
如果您遵循 Traceback,您会看到 flask 库中的最后一次调用在这里:File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
。这是 the source:
response.set_cookie(
name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)
您可以 see for yourself 上面传递给 set_cookie
的第一个参数被传递给 werkzeug.wrappers.base_response.set_cookie
中的 key
参数,它本身被传递给 key
werkzeug.http.dump_cookie
中的参数(即错误消息中的 None
)。
上面 save_session
片段中 name
的值是 defined earlier in the function as name = self.get_cookie_name(app)
, and the body of the get_cookie_name
method 就是 return app.session_cookie_name
.
app.session_cookie_name
的默认值是 "session"
,但在您的配置中它被 None
覆盖了。