用户模型
1.用户登录后认证状态需要被记录,这样浏览不同的页面才能记住这个状态,flask-login专门用来管理用户认证系统中的认证状态,且不依赖特定的认证机制
2.用户模型必须实现is_authenticated/is_active/is_anonymous/get_id四个方法才可以集成扩展,flask-login为我们提供了UserMixin类,包含了这些方法的默认实现,只需用户模型继承此类即可
| is_authenticated() |
如果用户已登录,必须返回True,否则返回False |
| is_active() |
如果允许用户登录,必须返回True,否则返回false,如果禁用账户,可返回False |
| is_anonymous() |
对普通用户必须返回False |
| get_id() |
必须返回用户对象的唯一标识符,常为主键id字段,使用Unicode编码字符串 |
FlaskWeb/app/models.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from . import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class Role(db.Model):
__tablename__ == 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False, index=True)
users = db.relationship('User', backref='role', lazy='dynamic')
class User(UserMixin, db.Model):
__tablename__ == 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, nullable=False, index=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
@property
def password(self):
raise AttributeError(u'password 不允许读取.')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
说明:User模型继承UserMixin,于是用户对象将拥有is_authenticated/is_active/is_anonymous/get_id属性方法,特别是基于上下文的全局变量current_user,可以通过这四个属性判断用户当前状态
FlaskWeb/app/__init__.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from flask import Flask
from config import config
from flask_mail import Mail
from flask_moment import Moment
from flask_login import LoginManager
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
mail = Mail()
db = SQLAlchemy()
moment = Moment()
bootstrap = Bootstrap()
loginmanager = LoginManager()
loginmanager.login_view = 'auth.login'
loginmanager.session_protection = 'strong'
loginmanager.login_message = u'你需要先登录才能继续本操作'
def create_app(env='default'):
env_config = config.get(env)
app = Flask(__name__)
app.config.from_object(env_config)
db.init_app(app)
mail.init_app(app)
moment.init_app(app)
bootstrap.init_app(app)
loginmanager.init_app(app)
from .main import main as main_blueprint
from .auth import auth as auth_blueprint
app.register_blueprint(main_blueprint)
app.register_blueprint(auth_blueprint, url_prefix='/auth')
return app
说明:集成flask-login时可设置loginmanager.session_protection为None/basic/strong,防止用户会话被篡改,此设置会记录客户端的IP地址和浏览器的用户代理信息,如果发现异动则强制登出用户,loginmanager.login_view设置登录视图端点,需要验证登录才可以访问其它路由时非常有用,loginmanager.login_message为默认需要验证登录时flash的默认消息.
FlaskWeb/app/models.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from . import db, loginmanager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
@loginmanager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class Role(db.Model):
__tablename__ == 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False, index=True)
users = db.relationship('User', backref='role', lazy='dynamic')
class User(UserMixin, db.Model):
__tablename__ == 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, nullable=False, index=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
@property
def password(self):
raise AttributeError(u'password 不允许读取.')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
说明:需要在modles.py数据库模型文件中给loginmanager定义一个回调函数,当用户登录时会向session['user_id']插入当前用户对象的主键id,当访问其它页面时,如果session['user_id']依然存在,则尝试通过回调函数和用户id来还原全局用户对象current_user,否则尝试从本地cookie加载之前用户登录信息重新尝试登录,此回调函数要求用户对象存在就返回用户对象,否则返回None
执行:python manager.py db drop,python manager.py db init,重置下数据库中的数据,并在视图模块儿中导入数据模型中User对象,这样回调函数才会被设置,否则可能会由于你本地cookie存在session['user_id']值会出现No user_loader has been installed for this错误,其实是因为回调函数没有被设置,却给回调函数传递给一个session['id']让程序还原current_user对象,这不是痴人说梦吗?
保护路由
1.为了保护路由只让认证用户访问,flask-login提供了login_required和fresh_login_required装饰器,前者包含了cookie自动登录,后者必须手动登录
FlaskWeb/app/auth/views.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from . import auth
from ..models import User
from flask import render_template
from flask_login import login_required, fresh_login_required
@auth.route('/')
@fresh_login_required
def index():
pass
@auth.route('/login', methods=['GET', 'POST'])
def login():
return render_template('auth/login.html')
说明:如果未认证的用户访问这个路由,flask-login会拦截请求,把用户发往登录页面
添加登录表单
1.登录表单包含一个用于电子邮件地址的文本字段,一个密码字段,一个"记住密码"复选框,表单使用flask-wtf生成,使用flask-bootstrap的wtf宏前端展现
FlaskWeb/app/auth/forms.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from ..models import User
from flask_wtf import Form
from wtforms import ValidationError
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Length, DataRequired, Email, Regexp
class LoginForm(Form):
email = StringField(u'邮箱', validators=[
DataRequired(u'请填写此字段'),
Length(6, 64, u'长度必须在6-64之间'),
Email(u'邮箱地址格式有误'),
])
password = PasswordField(u'密码', validators=[
DataRequired(u'请填写此字段'),
Length(6, 128, u'长度必须在6-64之间'),
Regexp(r'^[a-zA-Z0-9_][a-zA-Z0-9_]*$', 0, u'密码只能包含字母数字下划线')
])
remeber_me = BooleanField(u'是否记住密码?', default=True)
submit = SubmitField(u'登录')
def validate_email(self, field):
user = User.query.filter_by(email=field.data).first()
if not user:
raise ValidationError(u'邮箱地址还未注册')
说明:针对每个表单字段我们都设置了对应的验证器,需要说明的是在表单类中以validate_开头的,以表单字段结尾的,如上validate_email会自动被加载作为验证器,它接受一个字段类作为参数
FlaskWeb/app/templates/base.html
{%- extends 'bootstrap/base.html' -%}
{%- import 'bootstrap/wtf.html' as wtf -%}
{%- import 'bootstrap/utils.html' as utils -%}
{%- import 'bootstrap/fixes.html' as fixes -%}
{%- block html_attribs -%}
{{ super() }}
lang="zh-cn"
{%- endblock -%}
{%- block meta -%}
{{ super() }}
charset="utf-8"
{%- endblock -%}
{%- block title -%}
{{ title|default('Flasky', true) }}
{%- endblock -%}
{%- block head -%}
{{ super() }}
{{ fixes.ie8() }}
<link rel="shortcut icon"
type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="icon"
type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
{%- endblock -%}
{%- block navbar -%}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
{# 说明: 先不支持响应式 #}
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div>
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home</a></li>
</ul>
</div>
<div class="navbar-right">
<ul class="nav navbar-nav">
{%- if current_user.is_authenticated -%}
<li><a href="#current_user_profile">{{ current_user.username }}</a></li>
<li><a href="#auth_logout">登出</a></li>
{%- else -%}
<li><a href="{{ url_for('auth.login') }}">登录</a></li>
<li><a href="#auth_register">注册</a></li>
{%- endif -%}
</ul>
</div>
</div>
</div>
{%- endblock -%}
{%- block content -%}
<div class="container">
{%- block page_content -%}{%- endblock -%}
</div>
{%- endblock -%}
{%- block styles -%}
{{ super() }}
{%- endblock -%}
{%- block scripts -%}
<script type="text/javascript"
src="{{ url_for('main.static',filename='js/fixes/respond.min.js') }}"></script>
<script type="text/javascript"
src="{{ url_for('main.static',filename='js/fixes/html5shiv.min.js') }}"></script>
{{ super() }}
{{ moment.include_moment(local_js=url_for('main.static', filename='js/moment-with-locales.min.js')) }}
{%- endblock -%}
说明:之前未使用独立模版目录和静态资源目录由于可以公用base.html基础模版,flask-login为上下文注入了一个全局对象current_user,可以在模版中任意引用,可以利用此特性添加一个动态的注册/登录/登录/用户信息导航栏,用来验证登录,更新首页
FlaskWeb/app/templates/auth/login.html
{%- extends 'base.html' -%}
{%- block page_content -%}
<div class="page-header">
{{ utils.flashed_messages(container=false, dismissible=true) }}
</div>
<div class="page-content">
{{ wtf.quick_form(form, method='POST', role='form') }}
</div>
{%- endblock -%}
说明:为了方便,直接使用flask-bootstrap的utils和wtf宏实现消息闪现和快捷表单创建,如果想个性化表单可单独设置每个表单字段,设置其id/class等额外属性控制其样式和行为.
登入用户
FlaskWeb/app/auth/views.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from . import auth
from ..models import User
from .forms import LoginForm
from flask import render_template, flash, url_for, redirect
from flask_login import login_required, fresh_login_required, login_user
@auth.route('/')
@fresh_login_required
def index():
pass
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.verify_password(form.password.data):
flash(u'已成功登录', 'success')
login_user(user, form.remeber_me.data)
return redirect(url_for('main.index'))
flash(u'用户名或密码错误', 'danger')
return redirect(url_for('auth.login'))
return render_template('auth/login.html', form=form)
说明:当GET请求时,视图函数直接渲染表单,POST请求时,form.validate_on_submit()验证表单,表单验证通过,通过输入的emial从数据库加载用户对象,如果存在并且密码通过验证则调用flask-login的login_user(user, form.remeber_me.data),第一个参数为要登录的用户对象,第二个参数为是否记住密码的Bool值,如果为False,关闭浏览器后用户会话就过期,下次用户访问时要重新登录,否则会在用户浏览器中写一个长期有效的cookie(session['user_id']),使用这个cookie可以复现用户会话
登出用户
1.flask-login提供现成的logout_user()函数,它会删除并重设用户会话,至此此用户将改变状态为未授权访问状态,加上fresh_login_required的修饰,如果没有手动重定向到其它页面会重新调用login_view至登录界面
Flasky/app/main/views.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
#
# Authors: limanman
# OsChina: http://my.oschina.net/pydevops/
# Purpose:
#
"""
from . import auth
from ..models import User
from .forms import LoginForm
from flask import render_template, redirect, request, flash, url_for
from flask_login import login_user, fresh_login_required, login_required, logout_user
@auth.route('/', methods=['GET', 'POST'])
@login_required
def refuse():
pass
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.verify_password(form.password.data):
login_user(user, form.remeber_me.data)
return redirect(url_for('main.index'))
flash(u'用户名或密码错误', 'danger')
return redirect(url_for('auth.login'))
return render_template('auth/login.html', form=form)
@auth.route('/logout', methods=['GET', 'POST'])
@fresh_login_required
def logout():
logout_user()
flash(u'您已经成功登出', 'success')
return redirect(url_for('main.index'))
说明:由于所有的视图函数必须返回response对象,fresh_login_required尝试从cookie中获取session['user_id']如果本地缓存被你强制清除则自动重写响应跳转到loginmanager.login_view定义的默认登录视图,地址栏中地址将变为http://127.0.0.1/auth/login?next=/auth/logout
来源:oschina
链接:https://my.oschina.net/u/2612057/blog/700941