在了解使用Flask来实现用户认证之前,我们首先要明白用户认证的原理。假设现在我们自己去实现用户认证,需要做哪些事情呢?
- 首先,登录。用户能够输入用户名和密码进行登录,所以需要网页和表单,实现用户输入和提交的过程。
- 接着,校验登录是否成功。用户提交了用户名和密码,后台需要比对用户名密码是否正确,而要想比对,首先系统中就要有存储用户名密码的地方,大多数后台系统会通过数据库来存储,也可以存储到文件当中。存储用户名密码需要加密存储尤其是密码,如果只是简单的用明文存储,很容易被“有心人”盗取,从而造成用户信息泄露
- 登录之后,我们需要维持用户登录状态,以便用户在访问特定网页的时候来判断用户是否已经登录,以及是否有权限访问改网页。这需要维护一个会话来保存用户的登录状态和用户信息。
- 从第三步我们也可以看出,如果我们的网页需要权限保护,那么当请求到来的时候,我们首先要检查用户的信息,比如是否已经登录,是否有权限等,如果检查通过,那么在response的时候就会将相应网页回复给请求的用户,但是如果检查不通过,那么就需要返回错误信息。
- 用户登出
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, Blueprintapp = 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_requiredapp = 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 1def b():return 2c = 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 Truedef is_actice(self):return Truedef is_anonymous(self):return Falsedef get_id(self):return "1"
这里给所有的函数都返回了默认值,默认对应的情况是这个用户已经登录,并且是有效的。
然后在 login 的 view 里面 login_user, logout的view里面logout_user,这样整个登录过程就连接起来了,最后的代码是这样的:
#!/usr/bin/env python
# encoding: utf-8from flask import Flask,Blueprint
from flask.ext.login import LoginManager,login_required,login_user,logout_user,UserMixinapp = Flask(__name__)# user models
class User(UserMixin):def is_authenticated(self):return Truedef is_actice(self):return Truedef is_anonymous(self):return Falsedef 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 userauth = 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 stuffreturn 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.pyfrom 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 = usernameself.id = self.get_id()@propertydef password(self):raise AttributeError('password is not a readable attribute')@password.setterdef 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 Falsereturn 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 inthe 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 Noneexcept ValueError:return Nonereturn Nonedef get_id(self):"""get user id from profile file, if not exist, it willgenerate 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:passexcept ValueError:passreturn unicode(uuid.uuid4())@staticmethoddef 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 Nonetry: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 Nonereturn 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