您的位置:首页 > 编程语言 > Python开发

[python3.6 flask web学习]Flask用户认证框架

2017-08-15 09:51 726 查看
现在web系统基本都会有用户功能,一个良好的用户认证框架可以很轻松的实现一个轻巧、安全、可扩展的用户认证功能。Flask按照一般的用户认证流程,主要使用三个扩展模块进行用户的认证管理。

Flask-Login:管理已经认证的用户信息

Werkzeug:计算密码的散列值以及用户认证处理

itsdangerous:生成和核对加密token,主要用来实现用户注册邮件确认,密码找回,密码重置

1.用户登录处理

对于用户的密码字段,保存原始密码是一个最忌讳的。因为一旦后台服务器被攻破,很容易导致用户的信息遭到泄露。而且很多用户多个网站通常喜欢使用同一个密码,因此风险就更加大了。所以常用的方法是保存免得的散列值。

Flask使用Werkzeug计算和核对密码的散列值。Werkzeug提供两个方法实现这一功能。

generate_password_hash(password, method = pbkdf2:sha1, salt_length = 8):该方法是散列原始密码的,接受三个参数,第一个为待散列的原始密码,必传项,第二个为采用的散列方法,第三个为散列时候的加盐字符串,这两个为非必传项,通常采用默认的就足够了。返回值为密码的散列值。

check_password_hash(hash, password):该方法是校验数据库存储的散列值和原始密码是否一致的,用来校验用户密码是否正确的。该方法接受两个参数,第一个为数据库取出来的hash值,第二个为带校验的密码。如果返回True,则表明密码正确。

为了是User数据库模型支持密码的校验,修改User如下:

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
#...
password_hash = db.Column(db.String(128))

@property #该注释可以使字段直接通过方法名访问
def password(self):
raise AttributeError('password is not a readable attribute')

@password.setter #字段password,直接赋值然后调用该方法
def password(self, password):
self.password_hash = genrate_password_hash(password)

def verify_password(self, password): //校验密码
return check_password_hash(self.password_hash, password)


2.用户认证蓝本

把蓝本放在不同的包中方便,可以保证项目结构清晰。创建单独的用户认证蓝本,在app中创建auth包, 编辑初始文件创建蓝本app/auth/__init__.py

from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views


定义好认证的蓝本后,就在认证相关的视图函数中定义路由,app/auth/views.py

from flask import render_template
from . import auth

@auth.route('/login')
def login():
return render_template('auth/login.html')

3.使用Flask_Login记录用户认证信息

Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且不依赖特定的认证机制。首先安装flask-login

(venv) F:\python\flasky>pip install flask-login


要想使用Flask-Login扩展模块,用户数据库模型User必须实现一下几个方法:

is_authenticated() 如果用户已经登录,必须返回True,否则返回False
is_active() 如果允许用户登录,必须返回True,否则返回False。如果要禁用账户,可以返回False

is_anonymous() 对普通用户必须返回False
get_id() 必须返回用户的唯一标识符,使用Unicode 编码字符串

当然Flask-Login提供了一个更简单的方法,直接继承UserMixin即可,UserMixin中已经实现了这些默认方法。修改app/modes.py

from flask.ext.login import UserMixin

class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64), unique = True, index = True) #值唯一且建立索引
username = db.Column(db.String(64), unique = True, index = True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))


同时,Flask-Login还提供了一个login_required装饰器,修饰了某个视图函数之后,如果未认证的用户访问,将会跳转到登录页面。

修改玩User模型之后,新增登录表单app/auth/forms.py

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email

class LoginForm(Form):
email = StringField('Email', validators = [Required(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[Required()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')


定义好表单类之后就可以渲染模板了,由于登录状态是一个全局的,因此修改app/templates/base.html模板

<ul class = "nav navbar-nav navbar -right">
{% if current_user.is_authenticated() %} //current_user 有框架Flask-Login 定义
<li><a href="{{ url_for('auth.logout') }}">退出</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">登录</a></li>
{% endif %}
</ul>


完成了这些之后就可以处理登录视图函数了

from flask import render_template, redirect, request, url_for, flash
from flask.ext.login import login_user
from . import auth
from ..models import User
from .forms import LoginForm

@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 is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password.')
return render_template('auth/login.html', form=form)


渲染登陆表单 app/templates/login.html

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}登录{% endblock %}

{%block page_content %}
<div class = "page-header">
<h1>登录</h1>
</div>
<div class = "col-md-4">
{{ wtf.quick_form(form) }}
</div>


退出登录

from flask.ext.login import logout_user, login_required

@auth.route('/logout')
@login_required
def logout():
logout_user()
flask('已经退出登录')
return redirect(url_for('main.index'))

4.注册

注册和登录过程基本一样,只是注册之后需要给用户发送一封包含了确认链接的邮件让用户确认注册。

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Requried, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User

class RegistrationForm(Form):
email = StringField('Email', validators = [Required(), Length(1,64), Email()])
username = StringField('Username', validators = [Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, '用户名必须以字母开头,','并且只能包含字母数字下划线')])
password = PasswordField('Password', validators = [Required(), EqualTo('password2', message = '两次密码必须一样')])
password2 = PasswordField('Confirm password', validators = [Required()])
submit = SubmitField('Register')

#这种已validate_+字段名为名字的方法,跟validators里面的验证函数作用一样
def validate_email(self, field):
if User.query.filter_by(email=field.data).first()
raise ValidationError('邮箱已经被注册')

def validate_username(self, field):
if User.query.filter_by(username=field.data).first()
raise ValidationError('用户名已经存在')


路由定义 app/auth/views.py

@auth.route('register', method=['GET','POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email = form.email.data, username = form.username.data, password = form.password.data)
db.session.add(user)
flask('注册成功')
return redirect(url_for('auth.login'))
return render_template(url_for('auth/register.html', form = form))


上面紧紧完成了用户数据写入到数据库,要完成整个注册过程,还需要验证用户邮箱的有效性。使用itdangerous的dumps方法和用户id生成一个确认令牌发到用户邮箱里面,用户点击链接之后使用itdangerous的loads方法从令牌中获取id,然后校验是否和当前的session中用户id一样实现校验。

修改User模型,让其具备生成和校验令牌的功能

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db
class User(UserMixin, db.Model):
# ...
confirmed = db.Column(db.Boolean, default=False)

def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration) #生成Serilizer方法实例
return s.dumps({'confirm': self.id}) #生成令牌

def confirm(self, token): #确认令牌
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id://令牌中id和当前session中的id是否一直,防止恶意认证
return False
self.confirmed = True
db.session.add(self)
return True


发送邮件的路由

from ..email import send_email
@auth.route('/register', methods = ['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# ...
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm Your Account',
'auth/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)


app/templates/auth/email/confirm.txt:确认邮件的纯文本正文

Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.


确认用户账户的路由

from flask.ext.login import current_user
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmation link is invalid or has expired.')
return redirect(url_for('main.index'))


app/auth/views.py:重新发送账户确认邮件

@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm Your Account',
'auth/email/confirm', user=current_user, token=token)
flash('A new confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))


在完成确认之前是用户是不应该有权限访问确认页面之外的其他页面,对蓝本来说,before_request 钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对程序全局请求的钩子,必须使用before_app_request 修饰器。

@auth.before_app_request
def before_request():
if current_user.is_authenticated() \
and not current_user.confirmed \
and request.endpoint[:5] != 'auth.':
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))

@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous() or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: