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

Author Avatar
patrickcty 8月 08, 2017

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

登录

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

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

填写表单

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

在这里我是用的 GeeTest。

接入方法:

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

    <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 文件,并且进行初始化,这部分只用复制下面的结构然后稍微改动即可

    <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>
  • 验证验证码状态(其实这部分也不用添加,不过添加了能更进一步提升可靠性,具体参阅官方文档)

    # 这个视图函数必须添加
    @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 函数,也正是在这里面,我们完成对表单内容合法性和密码正确性的检查。

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 中显示出来。

<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 函数等
    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 中定义这个函数

@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 的初始化

identity_changed.send(
    current_app._get_current_object(),
    identity=Identity(user.id)
)

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

初始化 Permission

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

使用 Permission 的方法:

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

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

    permission = Permission(UserNeed(post.user.id))
    
    # 发布者和管理员都有权限
    if permission.can() or admin_permission.can():
        ...