本教程詳細介紹在使用者註冊過程中如何去驗證他們的email地址。
工作流程上來講,在使用者註冊一個新賬戶後會寄送一個確認信。直到使用者按指示完成了郵件中的“驗證”,否則他們的賬戶會一直處於“未驗證”狀態。這是大多數網路應用會採用的工作流程。
這當中很重要的一件事就是,未驗證的使用者有什麼許可權?或者說,對於你的應用,他們是有全部許可權呢,還是被限制的許可權呢,還是根本沒有許可權?對於本教程中的應用,未驗證使用者會在登入後進到一個頁面,會提醒他們只有驗證了賬戶才可以進入應用。
開始前說明一下,很多我們要增加的功能是Flask-使用者和Flask-安全的擴充部分——問題來了,為什麼不直接用這兩個擴充呢?嗯,首先,這是個學習機會。同時,這倆擴充都有局限性,比如支援的資料庫。要是你想用RethinkDB怎麼辦呢?
我們開始吧
Flask基本註冊
我們將會要開始一Flask範例,這包括了使用者基本註冊。從這個github倉庫擷取程式碼程式庫。一旦你建立和啟用了virtualenv,運行下面的命令來快速開始:
$ pip install -r requirements.txt$ export APP_SETTINGS="project.config.DevelopmentConfig"$ python manage.py create_db$ python manage.py db init$ python manage.py db migrate$ python manage.py create_admin$ python manage.py runserver
在應用啟動並執行狀態下,訪問http://localhost:5000/register頁面,註冊一個新使用者。注意,註冊之後應用會自動登陸,引導你進入首頁面。大概看一下,然後運行代碼——尤其是user這個藍圖(Blueprint是flask的一個概念)。
完成時停止伺服器。
更新當前應用
模型
首先,我們來在我們project/models.py中的User模型裡添加上confirmed欄位:
class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String, unique=True, nullable=False) password = db.Column(db.String, nullable=False) registered_on = db.Column(db.DateTime, nullable=False) admin = db.Column(db.Boolean, nullable=False, default=False) confirmed = db.Column(db.Boolean, nullable=False, default=False) confirmed_on = db.Column(db.DateTime, nullable=True) def __init__(self, email, password, confirmed, paid=False, admin=False, confirmed_on=None): self.email = email self.password = bcrypt.generate_password_hash(password) self.registered_on = datetime.datetime.now() self.admin = admin self.confirmed = confirmed self.confirmed_on = confirmed_on
注意此地區是怎樣預設成“False”的。還要添加個confirmed_on欄位,那是個datetime。為了用隊列分析來分析registered_on和confirmed_on日期的不同,所以我想包含這個datetime。
讓我們完全從頭開始建立資料庫並遷移吧!所以,先刪除資料庫dev.sqlite,以及“遷移”檔案夾。
控制命令
接下來,在manage.py中,更新create_admin命令,使新的資料庫欄位生效:
@manager.commanddef create_admin(): """Creates the admin user.""" db.session.add(User( email="ad@min.com", password="admin", admin=True, confirmed=True, confirmed_on=datetime.datetime.now()) ) db.session.commit()
要確保匯入datetime。現在,先再一次運行下面的指令:
$ python manage.py create_db$ python manage.py db init$ python manage.py db migrate$ python manage.py create_admin
register()視圖函數
最後,在我們再次註冊使用者之前,我們需要改一下project/user/views.py中的register()視圖函數……
user = User( email=form.email.data, password=form.password.data)
改成下面的:
user = User( email=form.email.data, password=form.password.data, confirmed=False)
明白了嗎?思考下為什麼要把confirmed預設成False。
嗯好。再運行一遍應用。轉入到http://localhost:5000/register,再註冊一個新使用者。如果你在SQLite瀏覽器中開啟了你的SQLite資料庫,你會看到:
那麼,這個我新註冊的使用者,michael@realpython.com沒有被驗證。讓我們驗證它。
添加email驗證
產生驗證令牌
郵件驗證應包含一個特殊URL,使得使用者只需簡單地點擊它,即可驗證他/她的賬戶。理想情況下,這個URL應該看起來像這樣–http://yourapp.com/confirm/。這裡的關鍵是它id。在這個id中用itsdangerous包,編碼使用者的郵件(包含時間戳記)。
建立一個叫project/token.py的檔案,添加下面的代碼:
# project/token.py from itsdangerous import URLSafeTimedSerializer from project import app def generate_confirmation_token(email): serializer = URLSafeTimedSerializer(app.config['SECRET_KEY']) return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT']) def confirm_token(token, expiration=3600): serializer = URLSafeTimedSerializer(app.config['SECRET_KEY']) try: email = serializer.loads( token, salt=app.config['SECURITY_PASSWORD_SALT'], max_age=expiration ) except: return False return email
所以在generate_confirmation_token()函數中,通過URLSafeTimedSerializer用在使用者註冊時得到的email地址產生一個令牌。那個_真實的_email在令牌中被編了碼。確認令牌之後,在confirm_token()函數中,我們可以用loads()方法,它接管令牌和其到期時間——一個小時(3600秒)內有效——作為參數。只要令牌沒到期,那它就會返回一個email。
在應用的配置中(BaseConfig()),確保添加SECURITY_PASSWORD_SALT:
SECURITY_PASSWORD_SALT = 'my_precious_two'
更新register()視圖函數
現在再從project/user/views.py更新下register()視圖函數:
@user_blueprint.route('/register', methods=['GET', 'POST'])def register(): form = RegisterForm(request.form) if form.validate_on_submit(): user = User( email=form.email.data, password=form.password.data, confirmed=False ) db.session.add(user) db.session.commit() token = generate_confirmation_token(user.email)
也是,要確保更新了這些匯入模組:
from project.token import generate_confirmation_token, confirm_token
處理Email驗證
接下來,添加個新的視圖來解決email驗證:
@user_blueprint.route('/confirm/')@login_requireddef confirm_email(token): try: email = confirm_token(token) except: flash('The confirmation link is invalid or has expired.', 'danger') user = User.query.filter_by(email=email).first_or_404() if user.confirmed: flash('Account already confirmed. Please login.', 'success') else: user.confirmed = True user.confirmed_on = datetime.datetime.now() db.session.add(user) db.session.commit() flash('You have confirmed your account. Thanks!', 'success') return redirect(url_for('main.home'))
把這個添加到project/user/views.py。同樣,確保更新了這些匯入:
import datetime
現在我們通過令牌調用confirm_token()函數。如果成功,我們更新使用者,把email_confirmed屬性改成True, 設定datetime為驗證發生的時間。還有,要是使用者已經進行過一遍驗證過程了——而且已經驗證了——我們要提醒使用者這點。
建立email模板
接著,添加一個基礎email模板:
Welcome! Thanks for signing up. Please follow this link to activate your account:
{{ confirm_url }}
Cheers!
把這個在“project/templates/user”中存為activate.html。這用了一個簡單叫confirm_url的變數,它會在register()視圖函數中被建立。
發送郵件
通過已經安裝了而且設定在project/__init__.py中的Flask-Mail的一點兒協助來建立一個基本函數來發送郵件。
建立一個叫email.py的檔案:
# project/email.py from flask.ext.mail import Message from project import app, mail def send_email(to, subject, template): msg = Message( subject, recipients=[to], html=template, sender=app.config['MAIL_DEFAULT_SENDER'] ) mail.send(msg)
在“project”檔案夾中存一下。
所以,我們只要簡單地去處理收件者清單,主題,模板即可。我們會一點一點處理郵件配置的設定。
在project/user/views.py中(再次!)更新register()視圖函數
@user_blueprint.route('/register', methods=['GET', 'POST'])def register(): form = RegisterForm(request.form) if form.validate_on_submit(): user = User( email=form.email.data, password=form.password.data, confirmed=False ) db.session.add(user) db.session.commit() token = generate_confirmation_token(user.email) confirm_url = url_for('user.confirm_email', token=token, _external=True) html = render_template('user/activate.html', confirm_url=confirm_url) subject = "Please confirm your email" send_email(user.email, subject, html) login_user(user) flash('A confirmation email has been sent via email.', 'success') return redirect(url_for("main.home")) return render_template('user/register.html', form=form)
還要添加下面的匯入模組:
from project.email import send_email
我們在這裡將所有的東西整合到一起。這個函數準系統是作為控制器(直接或間接):
- 處理最初註冊,
- 產生令牌和確認URL,
- 寄送確認email,
- 快速驗證,
- 使用者登入,
- 更改使用者。
你注意到_external=True參數了嗎?這增加了包含了hostname和port(在我們情況中,http://localhost:5000)的完整URL。
在我們測試這個之前,我們要去設定郵件設定。
郵件
在project/config.py中更新BaseConfig():
class BaseConfig(object): """Base configuration.""" # main config SECRET_KEY = 'my_precious' SECURITY_PASSWORD_SALT = 'my_precious_two' DEBUG = False BCRYPT_LOG_ROUNDS = 13 WTF_CSRF_ENABLED = True DEBUG_TB_ENABLED = False DEBUG_TB_INTERCEPT_REDIRECTS = False # mail settings MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 465 MAIL_USE_TLS = False MAIL_USE_SSL = True # gmail authentication MAIL_USERNAME = os.environ['APP_MAIL_USERNAME'] MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD'] # mail accounts MAIL_DEFAULT_SENDER = 'from@example.com'
查看 official Flask-Mail documentation 以得到更多資訊
如果你已經有了GMAIL帳號,那你可以用它或者註冊一個測試用GMAIL賬戶。然後把環境變數暫時設定在當前shell中:
$ export APP_MAIL_USERNAME="foo"$ export APP_MAIL_PASSWORD="bar"
如果你GMAIL帳號有兩步授權, Google會屏蔽掉它。
現在開始測試!
第一個測試
開啟應用,轉入到 http://localhost:5000/register。然後用你能登陸的email地址註冊。順利的話,你應該會收到封email,看起來像這樣:
點擊URL,你會轉到 http://localhost:5000/。保證使用者在資料庫裡,那‘confirmed'欄位是True,有個datetime和confirmed_on欄位綁定在一起。
好!
處理許可
如果你記得,在教程的開始部分,我們決定了“未驗證使用者可以登入但是他們會立刻被轉入一個頁面——我們稱之為/unconfirmed路徑——提醒使用者需要驗證賬戶才能使用應用”。
所以,我們要——
- 添加/unconfirmed路徑
- 添加unconfirmed.html模板
- 更新register()視圖函數
- 建立裝飾器
- 更新navigation.html模板
- 添加/unconfirmed路徑
添加下面的路徑project/user/views.py:
@user_blueprint.route('/unconfirmed')@login_requireddef unconfirmed(): if current_user.confirmed: return redirect('main.home') flash('Please confirm your account!', 'warning') return render_template('user/unconfirmed.html')
你看過類似的代碼,所以我們繼續。
添加unconfirmed.html模板
{% extends "_base.html" %} {% block content %} Welcome!
You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.
Didn't get the email? Resend.
{% endblock %}
在“project/templates/user”中,把這個存為 unconfirmed.html 。這次應該直截了當。現在,為了重新寄送驗證email,就只加了一個假的URL。我們接下來會解決它的。
更新register()視圖函數
現在簡單地把:
return redirect(url_for("main.home"))
變成:
return redirect(url_for("user.unconfirmed"))
所以,送完驗證email後,使用者會進入/unconfirmed路徑。
建立裝飾器
# project/decorators.py from functools import wraps from flask import flash, redirect, url_forfrom flask.ext.login import current_user def check_confirmed(func): @wraps(func) def decorated_function(*args, **kwargs): if current_user.confirmed is False: flash('Please confirm your account!', 'warning') return redirect(url_for('user.unconfirmed')) return func(*args, **kwargs) return decorated_function
這裡我們用一個基本函數去檢查使用者是否驗證。如果未驗證,使用者會進入/unconfirmed路徑。在“project”目錄中,把這個存為decorators.py。
現在裝飾 profile() 視圖函數:
@user_blueprint.route('/profile', methods=['GET', 'POST'])@login_required@check_confirmeddef profile(): ... snip ...
確保匯入裝飾器:
from project.decorators import check_confirmed
更新 navigation.html 模板
最後,更新navigation.html模板接下來的部分——
把:
{% if current_user.is_authenticated() %}
- Profile
{% endif %}
變成:
{% if current_user.confirmed and current_user.is_authenticated() %}
- Profile
{% elif current_user.is_authenticated() %}
- Confirm
{% endif %}
又該測試了!
第二次測試
開啟應用,再次用能登陸的email地址註冊。(可以隨便從資料庫刪除你之前註冊的老使用者,這樣可以再用一遍)現在,註冊完會轉入http://localhost:5000/unconfirmed。
確保測試了http://localhost:5000/profile路徑。這會使你轉到http://localhost:5000/unconfirmed。
驗證email,你就會有完整頁面的許可權了。快去把!
重寄送email
最後,來做重新寄送的連結。增加下面的視圖函數到project/user/views.py:
@user_blueprint.route('/resend')@login_requireddef resend_confirmation(): token = generate_confirmation_token(current_user.email) confirm_url = url_for('user.confirm_email', token=token, _external=True) html = render_template('user/activate.html', confirm_url=confirm_url) subject = "Please confirm your email" send_email(current_user.email, subject, html) flash('A new confirmation email has been sent.', 'success') return redirect(url_for('user.unconfirmed'))
現在更新unconfirmed.html 模板:
{% extends "_base.html" %} {% block content %} Welcome!
You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.
Didn't get the email? Resend.
{% endblock %}
第三次測試
你知道這次的演練內容了,這次保證去重新寄送一個新的確認email,測試連結。應該沒問題。
最後,如果你給自己發好幾個驗證信,會發生什嗎? 每封都有效嗎?測試一下。註冊個新使用者,寄送一些新的驗證信。驗證第一封試試。它可以嗎?它應該可以。這樣行嗎?你是否認為要是新的寄出,其他email應該失效呢?
對這種事做些調查。測試下你用的其他網頁應用。它們是怎麼處理這種行為的?
更新測試套件
好的。這是為了主要功能。我們更新當前測試套件怎麼樣?因為它,嗯,有問題。
運行測試:
$ python manage.py test
你會看到下面的錯誤:
TypeError: __init__() takes at least 4 arguments (3 given)
要去改正它,只需要在project/util.py中更新setUp()方法:
def setUp(self): db.create_all() user = User(email="ad@min.com", password="admin_user", confirmed=False) db.session.add(user) db.session.commit()
測試之前,在tests/test_models.py中,注釋一下test_user_registration()測試,因為我們不想為了這個測試真 的發送一封郵件。
現在再測試一下。應該全能通過!
結論
還可以做更多的:
- 富文本 vs. 純文字email——應該都寄出。
- 重設定密碼email——當使用者忘記密碼時,這些應該被寄出。
- 使用者管理——應該允許使用者更新email和密碼,當email變了的話,應該重新驗證一遍。
- 測試——需要寫更多測試來涉及更多新功能,包含用mock/patch更新的test_user_registration(),來防止實際email被發送。