一、添加用戶註冊表單html
app/auth/forms.py 用戶註冊表單正則表達式
表單使用WTForms提供的Regexp驗證函數,確保username字段只包含字母、數字、下劃線和點號,這個驗證函數中正則表達式後面的兩個參數分別是正則表達式的旗標和驗證失敗時顯示的錯誤消息shell
from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, validators, ValidationError
from wtforms.validators import Length, Email, Required, Regexp, EqualTo
from ..models import User
class RegistrationFrom(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, 'Usernames, dots or underscores')]) password = PasswordField('Password', validators=[Required(), EqualTo('password2', message='Passwords must match.')]) password2 = PasswordField('Confirm password', validators=[Required()]) submit = SubmitField('Register') def validate_mail(selfself, field): if User.query.filter_by(email=field.data).first(): raise ValidationError('Eamil already registered') def validate_username(self, field): if User.query.filter_by(username=field.data).frist(): raise ValidationError('Username already in use.')
二、顯示登陸表單數據庫
app/templates/auth/register.htmlflask
{% extends "base.htmml" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky - Register {% endblock %} {% block page_content %} <div class="page-header"> <h1>Register</h1> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> {% endblock %}
三、連接到註冊頁面bootstrap
app/templates/auth/login.html安全
登陸頁面要顯示一個指向註冊頁面的連接,讓沒有帳戶的用戶能輕易找到註冊頁面session
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky - Login{% endblock %} {% block page_content %} <div class="page-header"> <h1>Login</h1> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p> </div> {% endblock %}
註冊新用戶app
一、用戶註冊路由函數
app/auth/views.py
提交註冊表單,經過驗證後,系統就使用用戶填寫的信息在數據庫中添加一個用戶
from flask import render_template, redirect, request, url_for, flash from flask_login import login_user, login_required, logout_user from . import auth from ..models import User,db from .forms import LoginForm,RegistrationFrom #藍本中的路由和視圖函數 @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.remeber_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) #登出用戶 @auth.route('/logout') @login_required def logout(): logout_user() flash('You have been logged out.') return redirect(url_for('main.index')) #用戶註冊路由 @auth.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationFrom() if form.validate_on_submit(): user = User(email=form.email.data, username=form.username.data, password=form.password.data) db.session.add(user) flash('You can now login.') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form)
確認帳戶
爲確認註冊用戶時提供的信息是否正確,要求能經過電子郵件與用戶取得聯繫,爲驗證電子郵件會,經過郵件發出一份確認郵件,帳戶確認中每每要求用戶點擊一個包含確認令牌的特殊URL連接
確認郵件最簡單的確認連接是http://www.example.com/auth/confirm/<id>這種形式的URL,其中id是數據庫分配給用戶的數字id。用戶點擊這個連接後,處理這個路由的視圖函數就將接收到的用戶id做爲參數進行確認,而後將用戶狀態更新爲已確認
這種方式不安全,透露出了後臺的信息,解決方法是把URL中的id換成將相同安全加密後的到的令牌
示例:在shell中使用itsdangerous包生成包含用戶id的安全令牌
itsdangerous提供不少生成令牌的方式,其中,TimedJSONWebSignatureSerializer 類生成具備過時時間的 JSON Web 簽名 (JSON Web Signatures, JWS )。這個類的構造函數接受的參數是有個密鑰,在Flask程序中使用SECRET_KEY設置
dumps()方法爲指定的數據生成一個加密簽名,而後再對數據和簽名進行序列化,生成令牌字符串。expires_in參數設置令牌過時時間,單位爲秒
loads()方法能夠解碼令牌,其惟一的參數是令牌支付串,這個方法會效驗簽名和過時時間,若是經過,返回原始數據,若是不正確,拋出異常
一、將生成和效驗令牌的功能添加到User中
app/models.py 確認用戶帳戶
from . import db from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin from . import login_manager from flask import current_app from itsdangerous import TimedJSONWebSignatureSerializer as Serializer #定義數據庫模型 class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) users = db.relationship('User', backref='role') def __repr__(self): return '<Role %r>' %self.name #加載用戶的回調函數 @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) password_hash = db.Column(db.String(128)) email = db.Column(db.String(64), unique=True, index=True) #確認用戶帳戶 confirmed = db.Column(db.Boolean, default=False) def generate_confirmation_token(self, expiration=3600): s = Serializer(current_app.config['SECRET_KEY'], expiration) 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: return False self.confirmed = True db.session.add(self) return True #在User模型中加入密碼散列 @property def password(self): raise AttributeError('password is not a readable attribute') @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) def __repr__(self): return '<User %r>' %self.username
二、發送確認郵件
當前/register路由把新用戶添加到數據庫中後,會重定向到/index。在重定向以前,這個路由須要發送確認郵件
app/auth/views.py 能發送確認郵件的註冊路由
經過配置,程序已經能夠在請求末尾自動化提交數據庫變化,這裏要添加db.session.commit()調用。問題在於,提交數據庫以後才能賦予新用戶id值,而確認令牌須要用到id,因此不能延後提交
一個郵件須要兩個模板,分別渲染純文本文件和富文本文件
from ..email import send_email #用戶註冊路由 @auth.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationFrom() if form.validate_on_submit(): user = User(email=form.email.data, username=form.username.data, password=form.password.data) db.session.add(user) db.session.commit() token = user.generate_confirmation_token() send_mail(user.email, 'Confirm Your Accout', '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
默認狀況下,url_for()生成相對URL,例如 url_for('auth.confirm', token='abc') 返回的字符串是 '/auth/confirm/abc'。這顯然不是可以在電子郵件中發送的正確 URL。相對 URL 在網頁的上下文中能夠正常使用,由於經過添加當前頁面的主機名和端口號,瀏
覽器會將其轉換成絕對 URL。 但經過電子郵件發送 URL 時,並無這種上下文。添加到url_for() 函數中的 _external=True 參數要求程序生成完整的 URL,其中包含協議(http://或 https://)、主機名和端口
Dear {{ user.username }},
Welcome to Flasky!
To confirm your accout 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
四、確認用戶的帳戶
app/auth/views.py
Flask-Login提供的login_required修飾器會保護這個路由,所以,當用戶點擊確認郵件中的連接後,要先登陸,而後才能執行這個視圖函數
函數中,先檢查當前登陸用戶是佛已經確認過,確認過,重定向到首頁。
from flask_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'))
五、使用鉤子在before_app_request處理程序中過濾未確認的帳戶
app/auth/views.py
同時知足如下3個條件時,before_app_request處理程序會攔截請求
(1) 用戶已登陸(current_user.is_authenticated() 必須返回 True)。
(2) 用戶的帳戶還未確認。
(3) 請求的端點(使用 request.endpoint 獲取)不在認證藍本中。訪問認證路由要獲取權限,由於這些路由的做用是讓用戶確認帳戶或執行其餘帳戶管理操做。
若是知足以上所有條件,則會被重定向到/auth/unconfirmed路由,顯示一個確認帳戶相關信息的頁面
#過濾未確認用戶 @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_annoymous() or current_user.confirmed: return redirect(url_for('main.index')) return render_template('auth/unconfirmed.html')
六、從新發送帳戶確認郵件
app/auth/views.py
顯示給未確認用戶的頁面只渲染一個模板,其中有如何確認帳戶的說明,此外還提供了一個連接, 用於請求發送新的確認郵件,以防以前的郵件丟失
這個路由也有login_required保護,確認訪問時程序知道請求再次發送郵件的是哪一個用戶
#給未確認用戶,顯示頁面,是否須要重發確認郵件 @auth.route('/confirm') @login_required def resend_confirmation(): token = current_user.generate_confirmation_token() send_mail(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'))