如何覆盖Flask-Security默认消息?
Flask-Security 是一个可以帮助你在用 Python Flask 开发网页应用时,简化用户身份验证和权限管理的工具。不过,我遇到了一些问题。
在登录页面上,当用户输入不正确的信息时,会显示一些提示信息。比如说: - 指定的用户不存在 - 密码不正确 - 账户已被禁用
这些提示信息其实不太符合安全最佳实践。因为你不应该告诉用户为什么他的登录尝试被拒绝。上面的信息会让黑客更容易找到有效的用户名。
我想把这些默认的 Flask-Security 提示信息替换成类似“用户名或密码无效”的内容。但是,我还没找到一个方便的方法来做到这一点。
这些提示信息存储在 site-packages/flask_security/core.py 文件中的 _default_messages 里。我可以修改这个文件,但这不是个好办法:如果我重新安装或更新 Flask-Security,这个修改就会失效。
我知道我可以 自定义 Flask-Security 的默认视图。但是这些视图里有一些很有用的代码,比如
{{ render_field_with_errors(login_user_form.email) }}
它隐藏了登录表单的实现细节。我不想为了改变几个提示信息就丢掉这些有用的代码,还要重新写大部分内容。
有没有人知道更好的方法来定制 Flask-Security 的登录提示信息呢?
3 个回答
一开始我用了Rachel的回答,但因为错误信息还是和表单字段关联在一起,最终用户能看到他们的邮箱是有效的,但密码不对……所以,由于我几乎所有的反馈都用闪现消息,我发现直接修改flask_security是对我来说最简单的方法。
如果你打开这个文件:
site-packages\flask_security\forms.py
你会看到以下代码:
if self.user is None:
self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
return False
if not self.user.password:
self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
return False
if not verify_and_update_password(self.password.data, self.user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False
if not self.user.is_active:
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False
你会发现它把错误信息附加到相关的字段(邮箱或密码)上。你可以把这些注释掉,这样就不会在失败时提供任何反馈——我个人是像下面这样显示了一个错误信息:
if self.user is None:
#self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
flash('Error: There was an issue logging you in')
return False
if not self.user.password:
#self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
flash('Error: There was an issue logging you in')
return False
if not verify_and_update_password(self.password.data, self.user):
#self.password.errors.append(get_message('INVALID_PASSWORD')[0])
flash('Error: There was an issue logging you in')
return False
if requires_confirmation(self.user):
#self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
flash('Error: You have not confirmed your email')
return False
if not self.user.is_active:
#self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
flash('Error: There was an issue logging you in')
return False
我不确定有没有比直接修改这个包更好的方法(我知道这样做可能不太好),但对我来说这样有效,而且我也理解这个过程。
Rachel说得有点对(顺便提一下,所有的消息都在flask-security的core.py文件里)。通过改变默认的安全消息,你可以让错误提示变得更宽泛。不过,如果你使用的是标准的字段渲染方式,错误消息仍然会和导致问题的表单元素关联在一起。所以,大家很容易就能明白问题出在用户名还是密码上。
我做了以下几件事:
在你的配置文件中,把消息改成更宽泛的内容。我修改了以下这些消息:
SECURITY_MSG_INVALID_PASSWORD = ("Bad username or password", "error") SECURITY_MSG_PASSWORD_NOT_PROVIDED = ("Bad username or password", "error") SECURITY_MSG_USER_DOES_NOT_EXIST = ("Bad username or password", "error")
使用了一个宏来渲染字段,但不显示错误消息(
render_field
)。如果你在用flask-bootstrap,那就没有这样的宏,所以我自己创建了一个(非常简单,只需去掉错误块和给表单元素上色的类)。下面的代码就是从flask-bootstrap复制过来的,只是去掉了字段错误的代码:{% macro bootstrap_form_field_no_errors(field, form_type="basic", horizontal_columns=('lg', 2, 10), button_map={}) %} {% if field.widget.input_type == 'checkbox' %} {% call _hz_form_wrap(horizontal_columns, form_type, True) %} <div class="checkbox"> <label> {{field()|safe}} {{field.label.text|safe}} </label> </div> {% endcall %} {%- elif field.type == 'RadioField' -%} {# note: A cleaner solution would be rendering depending on the widget, this is just a hack for now, until I can think of something better #} {% call _hz_form_wrap(horizontal_columns, form_type, True) %} {% for item in field -%} <div class="radio"> <label> {{item|safe}} {{item.label.text|safe}} </label> </div> {% endfor %} {% endcall %} {%- elif field.type == 'SubmitField' -%} {# note: same issue as above - should check widget, not field type #} {% call _hz_form_wrap(horizontal_columns, form_type, True) %} {{field(class='btn btn-%s' % button_map.get(field.name, 'default'))}} {% endcall %} {%- elif field.type == 'FormField' -%} {# note: FormFields are tricky to get right and complex setups requiring these are probably beyond the scope of what this macro tries to do. the code below ensures that things don't break horribly if we run into one, but does not try too hard to get things pretty. #} <fieldset> <legend>{{field.label}}</legend> {%- for subfield in field %} {% if not bootstrap_is_hidden_field(subfield) -%} {{ form_field(subfield, form_type=form_type, horizontal_columns=horizontal_columns, button_map=button_map) }} {%- endif %} {%- endfor %} </fieldset> {% else -%} <div class="form-group"> {%- if form_type == "inline" %} {{field.label(class="sr-only")|safe}} {{field(class="form-control", placeholder=field.description, **kwargs)|safe}} {% elif form_type == "horizontal" %} {{field.label(class="control-label " + ( " col-%s-%s" % horizontal_columns[0:2] ))|safe}} <div class=" col-{{horizontal_columns[0]}}-{{horizontal_columns[2]}}"> {{field(class="form-control", **kwargs)|safe}} </div> {%- if field.description -%} {% call _hz_form_wrap(horizontal_columns, form_type) %} <p class="help-block">{{field.description|safe}}</p> {% endcall %} {%- endif %} {%- else -%} {{field.label(class="control-label")|safe}} {{field(class="form-control", **kwargs)|safe}} {%- if field.errors %} {%- for error in field.errors %} <p class="help-block">{{error}}</p> {%- endfor %} {%- elif field.description -%} <p class="help-block">{{field.description|safe}}</p> {%- endif %} {%- endif %} </div> {% endif %} {% endmacro %}
创建了一个新的宏,用来渲染所有字段的错误,并把它放在表单的顶部。我没有看到同时生成多个错误,但你其实并不知道是哪个字段导致了错误。同样,我的代码风格和flask-bootstrap一致,但你可以轻松去掉特定于bootstrap的元素。
{% macro fields_errors() %} {% for field in varargs %} {% if field.errors %} {% for error in field.errors %} {% call _hz_form_wrap(horizontal_columns, form_type) %} <div class="alert alert-danger">{{error}}</div> {% endcall %} {% endfor %} {% endif %} {% endfor %} {% endmacro %}
在表单中,你用所有的表单字段来调用这个宏:
{{ fields_errors(login_user_form.email, login_user_form.password, login_user_form.remember) }}`
从代码来看,它似乎是在给所有消息设置一些默认配置:
_default_messages = {
'INVALID_PASSWORD': ('Invalid password', 'error'),
}
... later on in the init method ....
for key, value in _default_messages.items():
app.config.setdefault('SECURITY_MSG_' + key, value)
如果你想改变消息内容,似乎只需要在你的 app.config 文件中设置这个:
SECURITY_MSG_INVALID_PASSWORD = ('Your username and password do not match our records', 'error'),
如果这样做不行,看起来改造这个模块去使用 babel 或者类似的东西也不会太难。作为一个优秀的开源软件公民,这样做会是个很不错的选择。