6

Flask-Security为 Python Flask Web 应用程序开发消除了身份验证和授权方面的大量繁重工作。不过,我遇到了一个障碍。

在登录页面上,消息会闪烁以响应各种无效输入。示例包括: - 指定的用户不存在 - 密码无效 - 帐户被禁用

这不符合安全最佳实践。您不应向用户透露其登录尝试被拒绝的原因的详细信息。上述消息使黑客更容易识别有效用户名。

我想覆盖这些标准的 Flask-Security 消息,将它们全部替换为“无效的用户名或密码”之类的内容。但是,我还没有找到一种方便的方法来做到这一点。

这些消息存储在 site-packages/flask_security/core.py 的 _default_messages 中。我可以修改该文件,但这不是一个好的解决方案:如果我重新安装或更新 Flask-Security,它将中断。

我知道我可以自定义 Flask-Security 的默认视图。但是视图包含有用的代码,例如

{{ render_field_with_errors(login_user_form.email) }}

隐藏登录表单的实现细节。我不想丢弃所有有用的代码并重写其中的大部分只是为了更改一些消息。

有谁知道自定义 Flask-Security 登录消息的更好方法?

4

3 回答 3

6

Rachel 部分正确(顺便说一句,所有消息都在烧瓶安全的 core.py 中)。通过更改默认安全消息,您可以使错误消息更广泛。但是,假设您使用的是标准的字段呈现,错误消息仍将附加到导致问题的表单元素。所以不难理解,问题出在用户名还是密码上。

我做了什么:

  1. 将配置文件中的消息更改为广泛消息。我已更改以下消息:

    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")
    
  2. 使用了在没有错误消息的情况下呈现字段的宏 ( 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 %}
    
  3. 创建了一个新的宏来呈现所有字段的所有错误,并将其放在表单的顶部。我没有看到同时产生多个错误,但你并不真正知道是哪个字段导致了错误。同样,我的代码与flask-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) }}`
于 2014-04-19T05:15:11.987 回答
5

通过阅读代码,看起来它为所有消息设置了配置默认值:

_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或类似的东西并不难。作为一名优秀的 FOSS 公民,这将是一件很棒的事情。

于 2014-04-18T21:03:06.350 回答
0

起初我使用了 Rachel 的答案,但由于错误仍然附加到表单字段,最终用户可以判断他们是否有有效的电子邮件和不正确的密码......所以,当我使用 Flash 消息进行几乎所有反馈时,我发现编辑 flask_security 本身对我来说是最简单的方法。

如果你打开文件

站点包\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

我不确定是否有比直接编辑包更好的方法来实现我所做的更改(我确信这是不好的做法),但它对我有用,我理解。

于 2020-06-29T11:30:49.690 回答