1. 程式人生 > >[python3.6 flask web學習]Flask使用者認證框架

[python3.6 flask web學習]Flask使用者認證框架

現在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')