再次理解Flask中登录与用户角色

再次理解 Flask 中登录与用户角色

登录

登录由以下几个部分构成:

  • 填写表单
  • 验证用户信息是否正确
  • 保存登录状态

填写表单

填写表单这个应该没什么好说的,但是可以让表单更高级——加入验证码。

在这里我是用的 GeeTest。

接入方法:

  • 在其官网下载对应语言的 SDK,其中 Python 的 SDK 包括一个 geetest 的包以及一个用来注入到 HTML 中的 js 文件
  • 在 HTML 中给验证码留出相应的位置,第一个控标签除就是验证码所在标签

    1
    2
    3
    4
    5
    <div class="col s12" id="embed-captcha"></div>
    <div class="col s12">
    <p id="wait" class="show" style="color: #ee6e73">正在加载验证码......</p>
    <p id="notice" class="hide" style="color: red">请先拖动验证码到相应位置</p>
    </div>
  • 引入所给 js 文件,并且进行初始化,这部分只用复制下面的结构然后稍微改动即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    <script src="{{ url_for('static', filename='js/gt.js') }}"></script>
    <script>
    var handlerEmbed = function (captchaObj) {
    $("#embed-submit").click(function (e) {
    var validate = captchaObj.getValidate();
    if (!validate) {
    $("#notice")[0].className = "show";
    setTimeout(function () {
    $("#notice")[0].className = "hide";
    }, 2000);
    e.preventDefault();
    }
    });
    // 将验证码加到id为captcha的元素里,同时会有三个input的值:geetest_challenge, geetest_validate, geetest_seccode
    captchaObj.appendTo("#embed-captcha");
    captchaObj.onReady(function () {
    $("#wait")[0].className = "hide";
    });
    // 更多接口参考:http://www.geetest.com/install/sections/idx-client-sdk.html
    };
    $.ajax({
    // 获取id,challenge,success(是否启用failback)
    url: "/pc-geetest/register?t=" + (new Date()).getTime(), // 加随机数防止缓存
    type: "get",
    dataType: "json",
    success: function (data) {
    // 使用initGeetest接口
    // 参数1:配置参数
    // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件
    initGeetest({
    gt: data.gt,
    challenge: data.challenge,
    product: "embed", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效
    offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
    width: '25%'
    // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config
    }, handlerEmbed);
    }
    });
    </script>
  • 验证验证码状态(其实这部分也不用添加,不过添加了能更进一步提升可靠性,具体参阅官方文档)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # 这个视图函数必须添加
    @main_blueprint.route('/pc-geetest/register', methods=['GET'])
    def get_pc_captcha():
    gt = GeetestLib(Config.pc_id, Config.pc_key)
    status = gt.pre_process()
    session[gt.GT_STATUS_SESSION_KEY] = status
    response_str = gt.get_response_str()
    return response_str


    @main_blueprint.route('/login', methods=['GET', 'POST'])
    def login():
    form = LoginForm()
    if form.validate_on_submit():
    # 这下面添加了能提升可靠性,也可以不添加
    gt = GeetestLib(Config.pc_id, Config.pc_key)
    challenge = request.form[gt.FN_CHALLENGE]
    validate = request.form[gt.FN_VALIDATE]
    seccode = request.form[gt.FN_SECCODE]
    status = session[gt.GT_STATUS_SESSION_KEY]
    if status:
    result = gt.success_validate(challenge, validate, seccode)
    else:
    result = gt.failback_validate(challenge, validate, seccode)
    ...

验证用户信息

平常我们都是在视图函数中来检验密码是否是正确,但实际上和验证表单内容是否合法一样,这个工作也可以在表单类中完成。

我们在视图函数中通过 form.validate_on_submit 来检查表单是否被成功提交,实际上在返回结果之前我们要先调用 form.validate 函数,也正是在这里面,我们完成对表单内容合法性和密码正确性的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LoginForm(FlaskForm):
username = StringField('用户名', [DataRequired(), Length(max=255)])
password = PasswordField('密码', [DataRequired()])
remember_me = BooleanField('记住登录状态')

def validate(self): # 在 validate_on_submit 的时候会检查
check_validate = super(LoginForm, self).validate()

if not check_validate:
return False

user = User.query.filter_by(username=self.username.data).first()
if not user:
self.username.errors.append('用户名或密码错误')
return False

if not user.verify_password(self.password.data):
self.username.errors.append('用户名或密码错误')
return False

return True

如果出现了错误则只用把它添加到相应表单的 errors 中去,然后就可以在 HTML 中显示出来。

1
2
3
4
5
6
7
8
9
<div class="input-field col s12">
{{ form.password.label }}
{{ form.password(class_='validate') }}
{% if form.password.errors %}
{% for e in form.password.errors %}
<p class="help-block alert-danger">{{ e }}</p>
{% endfor %}
{% endif %}
</div>

保存登录状态

最基本的方法当然是使用 session 来保存,然后通过判断 session 是否有相应的信息来检查是否登录处于状态。

比较常用也是进阶的就是使用 flask-login 了。

使用 flask-login 需要根据模块来进行一定的配置:

  • 对 User 类实现特定的方法,这里可以通过继承 UserMixin 来简化,但是要注意如果用户 id 的格式和默认的不同则还是要重写 get_id 方法
  • 定义登陆的视图,load_user 函数等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    login_manger = LoginManager()
    login_manger.login_view = 'main.login'
    login_manger.session_protection = 'strong'
    login_manger.login_message = '请登录以访问该页面'
    login_manger.login_message_category = 'info'


    @login_manger.user_loader
    def load_user(userid):
    from .models import User
    return User.query.get(userid)

用户角色

要使用用户权限,则要进行用户角色的相应配置,在这里可以使用 flask-principal 模块来对用户权限进行管理。

当然我们要先建立一张表来控制用户和用户角色的多对多关系,在建立数据库之后不要忘记初始化角色表。

flask-principal 的关键名词主要有 Identity,Permission,Need。其中 Identity 和 Permission 都是通过 Need 来实现功能。Need 则是一些 namedtuple(相当于 C 语言中的结构体),包括 method 和 value 两个属性,定义了每种身份可以干什么。

例如 UserNeed 的 method 默认则是 id,key 则应该传入对应用户的 id 值,而 RoleNeed 的 method 的默认值则是 role,key 的默认值应该是 role 的名称。其中 UserNeed 和 RoleNeed 是通过 partical 固定了 Need 的一个参数。点击查看 nametuplepartical 用法。

Identity 是通过 user.id 来进行初始化的,然后此时会自动调用自己定义的初始化函数把需要添加的 Need 添加进这个 Identity 中。

在 __init__.py 中定义这个函数

1
2
3
4
5
6
7
8
9
10
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
identity.user = current_user

if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))

if hasattr(current_user, 'roles'):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))

而当用户登录登出时 Identity 应该发生改变,此时应该调用 identity_changed 方法来发送信号,此时就会调用 on_identity_loaded 函数来进行新 Identity 的初始化

1
2
3
4
identity_changed.send(
current_app._get_current_object(),
identity=Identity(user.id)
)

而 Permission 则是通过 Need 来进行初始化,这些 Need 就表示当前权限所需的角色,只有满足了相应的角色才能达到相应权限。

初始化 Permission

1
2
3
admin_permission = Permission(RoleNeed('admin'))
poster_permission = Permission(RoleNeed('poster'))
default_permission = Permission(RoleNeed('default'))

使用 Permission 的方法:

  • 使用初始化权限的装饰器

    1
    2
    3
    4
    @login_required
    @poster_permission.require(http_exception=403)
    def edit(id):
    ...
  • 使用 Permission.can() 来判断是否符合权限

    1
    2
    3
    4
    5
    permission = Permission(UserNeed(post.user.id))

    # 发布者和管理员都有权限
    if permission.can() or admin_permission.can():
    ...