Flask 第三方组件之 login

14 篇文章 1 订阅

在了解使用Flask来实现用户认证之前,我们首先要明白用户认证的原理。假设现在我们自己去实现用户认证,需要做哪些事情呢?

  1. 首先,登录。用户能够输入用户名和密码进行登录,所以需要网页和表单,实现用户输入和提交的过程。
  2. 接着,校验登录是否成功。用户提交了用户名和密码,后台需要比对用户名密码是否正确,而要想比对,首先系统中就要有存储用户名密码的地方,大多数后台系统会通过数据库来存储,也可以存储到文件当中。存储用户名密码需要加密存储尤其是密码,如果只是简单的用明文存储,很容易被“有心人”盗取,从而造成用户信息泄露
  3. 登录之后,我们需要维持用户登录状态,以便用户在访问特定网页的时候来判断用户是否已经登录,以及是否有权限访问改网页。这需要维护一个会话来保存用户的登录状态和用户信息。
  4. 从第三步我们也可以看出,如果我们的网页需要权限保护,那么当请求到来的时候,我们首先要检查用户的信息,比如是否已经登录,是否有权限等,如果检查通过,那么在response的时候就会将相应网页回复给请求的用户,但是如果检查不通过,那么就需要返回错误信息。
  5. 用户登出

flask通常是使用Flask-Login模块来实现上述流程控制。下面介绍使用Flask-Login登录注销,以及帮助大家解答一些可能比较常见的问题。

代码实现

首先,先概述下例子,有三个url,分别是:

/auth/login     用于登录
/auth/logout    用于注销
/test           用于测试,需要登录才能访问

安装必要的库

pip install Flask==0.10.1
pip install Flask-Login==0.3.2
pip install Flask-WTF==0.12
pip install WTForms==2.1

编写web框架。在开始登录之前,我们先把整个 web 的框架搭建出来,也就是,我们要能够先在不登录的情况下访问到上面提到的三个url,我就直接放在一个叫做 app.py 的文件中。

#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprint


app = Flask(__name__)

# url redirect
auth = Blueprint('auth', __name__)


@auth.route('/login', methods=['GET', 'POST'])
def login():
    return "login page"


@auth.route('/logout', methods=['GET', 'POST'])
def logout():
    return "logout page"    


# test method
@app.route('/test')
def test():
    return "yes , you are allowed"


app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)

现在,我们可以尝试一下运行一下这个框架,使用 python app.py 运行即可,然后打开浏览器,分别访问一下,看一下是否都正常

http://localhost:5000/test
http://localhost:5000/auth/login
http://localhost:5000/auth/logout

设置登录才能查看。现在框架已经设置完毕,我们可以将 test 和 auth/logout 这两个 page 设置成登录之后才能查看。因为这个功能已经和 login 有关系了,所以这时我们就需要使用到 Flask-Login了。代码如下

#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprint
from flask.ext.login import LoginManager, login_required


app = Flask(__name__)

#################### 以下这段是新增加的 #################### 
app.secret_key = 's3cr3t'
login_manager = LoginManager()

# 设置不同的安全等级防止用户会话遭篡改,属性可以设为None、basic或strong
# 设为 strong 时,Flask-Login 会记录客户端 IP 地址和浏览器的用户代理信息,如果发现异动就登出用户
login_manager.session_protection = 'strong' 

# 如果未登录,返回的页面
login_manager.login_view = 'auth.login'
login_manager.init_app(app)

# Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。加载用户的回调函数接收以 Unicode 字符串形式表示的用户标识符。如果能找到用户,这个函数必须返回用户对象;否则应该返回 None,这里因为设置框架所以就默认返回 None。
@login_manager.user_loader
def load_user(user_id):
    return None
#################### 以上这段是新增加的 #################### 

auth = Blueprint('auth', __name__)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    return "login page"


# 通过Flask-Login提供的login_required装饰器来增加路由保护,如果未认证用户访问这个路由,Flask-Login会将这个请求发往登录页面
@auth.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():
    return "logout page"


# test method
@app.route('/test')
@login_required
def test():
    return "yes , you are allowed"


app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)

其实我们就增加了两项代码,一项是初始化 LoginManager 的, 另外一项就是给 test 和 auth.logout 添加了 login_required 的装饰器,表示要登录了才能访问。注意 login_required 必须放在 auth.route 后面

#################### 部分源码 ####################
@app.route('/test', methods=['GET', 'POST'])
@csrf.exempt
@login_required
def test():
    pass
# test= app.route('/test', methods=['GET', 'POST'])(test)
# test= login_required(test)


# login_required 源码
def login_required(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        if current_app.login_manager._login_disabled:
            return func(*args, **kwargs)
        elif not current_user.is_authenticated:
            return current_app.login_manager.unauthorized()
        return func(*args, **kwargs)
    return decorated_view


# app.route 实际最后执行代码
def app.route() 
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError('View function mapping is overwriting an existing endpoint function: %s' % endpoint)
        self.view_functions[endpoint] = view_func


#################### 分析 ####################
# 原因,正常情况下装饰器需要将函数地址传入并返回一个新的函数地址,但是 app.route 创建了一个新的结构并将传入的函数地址直接保存到结构中,导致其他的装饰器对这个函数地址修改影响不到 app.route 创建的结构,而在路由分发的时候,直接调用的是结构中保存的地址,所以其他装饰器不起作用,所以必须将装饰器放在 app.route 下面


#################### 简化代码 ####################
def a():
    return 1


def b():
    return 2


c = a
a = b
print(c())

用户授权。到此,我们发现 test 是不能访问的,会被重定向到 login 的那个 page。看一下现在的代码, login_required 有了, 那么就差login了,接下来写login,看Flask-Login的文档发现一个叫做login_user的函数,看看它的原型:

flask.ext.login.login_user(user, remember=False, force=False, fresh=True)

这里需要一个user的对象,所以先创建一个Model,其实这个Model还是有一点讲究的,最好是继承自Flask-Login的UserMixin,然后需要实现几个方法,Model 为:

# user models
class User(UserMixin):
    def is_authenticated(self):
        return True

    def is_actice(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return "1"

这里给所有的函数都返回了默认值,默认对应的情况是这个用户已经登录,并且是有效的。

然后在 login 的 view 里面 login_user, logout的view里面logout_user,这样整个登录过程就连接起来了,最后的代码是这样的:

#!/usr/bin/env python
# encoding: utf-8

from flask import Flask,Blueprint
from flask.ext.login import LoginManager,login_required,login_user,logout_user,UserMixin


app = Flask(__name__)


# user models
class User(UserMixin):
    def is_authenticated(self):
        return True

    def is_actice(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return "1"


# flask-login
app.secret_key = 's3cr3t'
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
login_manager.init_app(app)


@login_manager.user_loader
def load_user(user_id):
    user = User()
    return user


auth = Blueprint('auth', __name__)


@auth.route('/login', methods=['GET', 'POST'])
def login():
    user = User()
    login_user(user)
    return "login page"


@auth.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():
    logout_user()
    return "logout page"


@app.route('/test')
@login_required
def test():
    return "yes , you are allowed"


app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)

总结

到此,这就是一个比较精简的Flask-Login 教程了,通过这个框架大家可以自行扩展达到更丰富的功能,诸如发送确认邮件,密码重置,权限分级管理等,这些功能都可以通过flask及其插件来完成,这个大家可以自己探索下。

问题

1、未登录访问鉴权页面如何处理

如果未登录访问了一个做了 login_required 限制的 view,那么 flask-login 会默认 flash 一条消息,并且将重定向到 login view, 如果你没有指定 login view, 那么 flask-login 将会抛出一个401错误。指定 login view 只需要直接设置login_manager即可:

login_manager.login_view = "auth.login"

2、自定义flash消息

login_manager.login_message = u"请登录!"       # 自定义 flash 的消息
login_manager.login_message_category = "info"  #  flash 消息的级别,一般设置成 info 或者 error

 

3、自定义未登录处理函数

如果你不想使用默认的规则,那么你也可以自定义未登录情况的处理函数,只需要使用 login_manager 的 unauthorized_handler 装饰器即可。

@login_manager.unauthorized_handler
def unauthorized():
    # do stuff
    return render_template("some template")

4、匿名用户是怎么处理的?有哪些属性?

在 flask-login 中,如果一个匿名用户访问站点,那么 current_user 对象会被设置成一个 AnonymousUserMixin 的对象,AnonymousUserMixin 对象有以下方法和属性:

  • is_active and is_authenticated are False
  • is_anonymous is True
  • get_id() returns None

5、自定义匿名用户Model:

如果你有需求自定义匿名用户的 Model,那么你可以通过设置 login_manager 的 anonymous_user 属性来实现,而赋值的对象只需是可调用对象(class 和 function都行)即可。

login_manager.anonymous_user = MyAnonymousUser

6、Flask-Login如何加载用户的:

当一个请求过来的时候,如果 ctx.user 没有值,那么 flask-login 就会使用 session 中 session['user_id'] 作为参数,调用 login_manager 中使用 user_loader 装饰器设置的 callback 函数加载用户,需要注意的是,如果指定的 user_id 无效,不应该抛出异常,而是应该返回 None。

登录成功后,就可以使用 current_use r对象了,current_user 保存的就是当前用户的信息,实质上是一个 User 对象,所以我们直接调用其属性, 例如这里我们要给模板传一个 username 的参数,就可以直接用 current_user.username

@login_manager.user_loader
def load_user(user_id):
    return User.get(user_id)

session['user_id'] 其实是在调用 login_in 函数之后自动设置的。

7、Flask-Login设置session过期时间:

在 Flask-Login 中,如果你不特殊处理的话,session 是在你关闭浏览器之后就失效的。也就是说每次重新打开页面都是需要重新登录的。如果你需要自己控制 session 的过期时间的话:

  • 首先需要设置 login_manager 的 session类型为永久的,
  • 然后再设置 session 的过期时间
#################### 配置文件 ####################
class Config:
  ...
   PERMANENT_SESSION_LIFETIME = datetime.timedelta(minutes=5)


#################### 登录 ####################
def login():
    login_user(user)
    session.permanent = True  # 设置session永久有效  注意这个要设置在request里边 即请求内部

同时,还需要注意的是 cookie 的默认有效期其实是 一年 的,所以,我们最好也设置一下:

login_manager.remember_cookie_duration=timedelta(days=1)

8、如何在同域名下的多个系统共享登录状态

这个需求可能在公司里面会比较常见,也就是说我们一个公司域名下面会有好多个子系统,但是这些子系统都是不同部门开发的,那么,我们如何在这不同系统间共享登录状态?也就是说,只要在某一个系统登录了,在使用其他系统的时候也共享着登录的状态,不需要再次登录,除非登录失效。

Server-side Sessions with Redis

这个说明尝试,也差不多是类似的解决方法。

9、使用Flask自带的函数加密存储密码

# models.py
 
from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import json
import uuid
 
# define profile.json constant, the file is used to
# save user name and password_hash
PROFILE_FILE = "profiles.json"
 
class User(UserMixin):
    def __init__(self, username):
        self.username = username
        self.id = self.get_id()
 
    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')
 
    @password.setter
    def password(self, password):
        """save user name, id and password hash to json file"""
        self.password_hash = generate_password_hash(password)
        with open(PROFILE_FILE, 'w+') as f:
            try:
                profiles = json.load(f)
            except ValueError:
                profiles = {}
            profiles[self.username] = [self.password_hash,
                                       self.id]
            f.write(json.dumps(profiles))
 
    def verify_password(self, password):
        password_hash = self.get_password_hash()
        if password_hash is None:
            return False
        return check_password_hash(self.password_hash, password)
 
    def get_password_hash(self):
        """try to get password hash from file.
        :return password_hash: if the there is corresponding user in
                the file, return password hash.
                None: if there is no corresponding user, return None.
        """
        try:
            with open(PROFILE_FILE) as f:
                user_profiles = json.load(f)
                user_info = user_profiles.get(self.username, None)
                if user_info is not None:
                    return user_info[0]
        except IOError:
            return None
        except ValueError:
            return None
        return None
 
    def get_id(self):
        """get user id from profile file, if not exist, it will
        generate a uuid for the user.
        """
        if self.username is not None:
            try:
                with open(PROFILE_FILE) as f:
                    user_profiles = json.load(f)
                    if self.username in user_profiles:
                        return user_profiles[self.username][1]
            except IOError:
                pass
            except ValueError:
                pass
        return unicode(uuid.uuid4())
 
    @staticmethod
    def get(user_id):
        """try to return user_id corresponding User object.
        This method is used by load_user callback function
        """
        if not user_id:
            return None
        try:
            with open(PROFILE_FILE) as f:
                user_profiles = json.load(f)
                for user_name, profile in user_profiles.iteritems():
                    if profile[1] == user_id:
                        return User(user_name)
        except:
            return None
        return Non
  • User类需要继承flask-login中的UserMixin类,用于实现相应的用户会话管理。

  • 这里我们是直接存储用户信息到一个json文件"profiles.json"

  • 我们并不直接存储密码,而是存储加密后的hash值,在这里我们使用了werkzeug.security包中的generate_password_hash函数来进行加密,由于此函数默认使用了sha1算法,并添加了长度为8的盐值,所以还是相当安全的。一般用途的话也就够用了。

  • 验证password的时候,我们需要使用werkzeug.security包中的check_password_hash函数来验证密码

  • get_id是UserMixin类中就有的method,在这我们需要overwrite这个method。在json文件中没有对应的user id时,可以使用uuid.uuid4()生成一个用户唯一id

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在UniApp中使用第三方登录,如快手登录,可以通过uni-login插件来实现。首先,你需要安装uni-login插件,可以在HBuilderX的插件市场中搜索并安装。 安装完成后,你需要在快手开放平台注册应用,获取到AppKey和AppSecret。然后,在你的UniApp项目中,按照以下步骤进行配置: 1. 在`manifest.json`中添加uni-login插件的权限配置: ```json "permissions": { "scope.userInfo": { "desc": "用于快手登录" } } ``` 2. 在`pages.json`中添加uni-login插件的页面配置: ```json "pages": [ { "path": "pages/login/index", "style": { "navigationBarTitleText": "登录" } }, { "path": "uni_modules/uni-login/pages/uni-login/uni-login", "style": { "navigationBarTitleText": "第三方登录" } } ] ``` 3. 在登录页的`<template>`中引入uni-login组件: ```html <template> <view class="container"> <button @tap="loginByKuaishou">快手登录</button> </view> <uni-login></uni-login> </template> ``` 4. 在登录页的`<script>`中引入uni-login组件的相关方法,并在点击快手登录按钮时调用相应方法: ```javascript import { loginByPlatform } from '@/uni_modules/uni-login/js_sdk/login.js' export default { methods: { async loginByKuaishou() { const result = await loginByPlatform('kuaishou') // 处理登录结果 } } } ``` 这样就可以在UniApp中使用快手登录了。当用户点击快手登录按钮后,会弹出快手授权登录页面,用户登录授权后,你可以获取到相关的用户信息进行处理。具体的登录结果处理,请参考uni-login插件的文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值