User Authentication — User Login

George Chang
origino
Published in
12 min readJul 27, 2017

研續上一篇使用者驗證User Authentication — HTTP Basic Auth,這篇文章要介紹的是如何實作最常見的使用者登入。

我們一樣使用restful-flask來改寫。

因為僅是展示使用,我直接使用Flask內建的Sessions object來實作,這邊要提一下,預設的session是將資料用secret_key加密後存在cookie,你也可以將session儲存在:

  1. Memory caching system(memcached or redis)
  2. Database

所以我們先用python生成一個secret key

$ python
>>> import secrets
>>> secrets.token_hex(64)
ce1b2d429a34d5371776785aac26002f654d7fe08bf387e082607571fc6f770bebd113cc5c821d216af5d5abfc01ae9f0c79c8db324c10aed9bfaad7fc7223fa

接著將secret key加入app/__init__.py

# app/__init__.py
...
app.secret_key = 'ce1b2d429a34d5371776785aac26002f654d7fe08bf387e082607571fc6f770bebd113cc5c821d216af5d5abfc01ae9f0c79c8db324c10aed9bfaad7fc7223fa'

然後我們修改原本的User Model,加上password_hash與對應的方法

# app/modes/user.py
from .. import db
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80))
email = db.Column(db.String(120), unique=True)
password_hash = db.Column(db.Text)
def __init__(self, email, password, username):
self.email = email
self.set_password(password)
self.username = username
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<User %r>' % self.username
@classmethod
def login(cls, email, password):
user = cls.query.filter_by(email = email).first()
if user != None:
if user.check_password(password):
return user
else:
return None
return None

會看到我加入了一個password_hash欄位,然後在建立user時去呼叫set_password這個方法,並在登入時去呼叫check_password去檢查密碼是否正確。

p.s. 這裡用到Werkzeug(一個WSGI工具包,Flask就是基於這個實作出來)的generate_password_hash(生成密碼hash)與 check_password_hash(檢查密碼)

接著登入flask script console

$ python manage.py shell>>> from app import db
>>> db.drop_all() # 將原先的table移除
>>> db.create_all() #建立新的table
>>> from app.models.user import User
>>> robin = User('robin@myserver.com', 'test', 'Robin')
>>> db.session.add(robin)
>>> db.session.commit()

我們修改資料表欄位後,重新建立一個資料表,並建立一個user

修改完Model,接著來修改Controller

# app/views/users.py
from ..models.user import User
import flask
from flask import Blueprint, request, Response, jsonify, render_template, abort, redirect, url_for, session
from functools import wraps
from jinja2 import TemplateNotFound
from .. import db
users = Blueprint('users', __name__, template_folder='templates')def current_user():
if session.get('user_id'):
return User.query.filter_by(id=session['user_id']).first()
else:
return None

def authenticate():
return redirect(url_for('users.login'))
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if current_user() == None:
return authenticate()
return f(*args, **kwargs)
return decorated
@users.errorhandler(404)
def page_not_found():
return abort(404)
@users.route('/', methods=['GET'])
@requires_auth
def index():
users = User.query.all()
return render_template('users/index.html', users = users)
@users.route('/new', methods=['GET'])
def new():
user = User()
return render_template('users/new.html', users = user)
@users.route('/create', methods=['POST'])
def create():
username = request.form['username']
email = request.form['email']
user = User(username, email)
db.session.add(user)
db.session.commit()
return redirect(url_for('users.index'))
@users.route('/<int:id>', methods=['GET'])
def show(id):
try:
user = User.query.filter_by(id=id).first()
return render_template('users/show.html', user = user)
except TemplateNotFound:
abort(404)
@users.route('/<int:id>/edit', methods=['GET'])
def edit(id):
try:
user = User.query.filter_by(id=id).first()
return render_template('users/edit.html', user = user)
except TemplateNotFound:
abort(404)
@users.route('/<int:id>', methods=['POST'])
def update(id):
user = User.query.filter_by(id=id).first()
email = request.form['email']
user.email = email
return redirect(url_for('users.index'))
@users.route('/<int:id>/delete', methods=['POST'])
def destroy(id):
user = User.query.filter_by(id=id).first()
db.session.delete(user)
db.session.commit()
return redirect(url_for('users.index'))
@users.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = str(request.form['email'])
password = str(request.form['password'])
user = User.login(email, password)
if user != None:
session['user_id'] = user.id
return redirect(url_for('users.index'))
else:
return render_template('users/login.html')
return render_template('users/login.html')
@users.route('/logout', methods=['POST'])
def logout():
session.pop('user_id', None)
return redirect(url_for('users.index'))

我新加了login & logout方法,並修改原本的裝飾器(decorator)來檢查使用者是否有登入,如果沒有登入便會轉到/login頁面

於是在User View(templates)加入一個login.html

# app/templates/users/login.html
{% extends "users/layout.html" %}
{% block body %}
<form action="{{ url_for('users.login') }}" method="POST">
<label for="email">email</label>
<input type="text" name="email"><br>
<label for="password">password</label>
<input type="password" name="password"><br>
<input type="submit" value="submit">
</form>
{% endblock %}

在這裡建立一個表單供使用者登入

並在users index加入一個登出按鈕

# app/templates/users/index.html
{% extends "users/layout.html" %}
{% block body %}
<ul>
{% for user in users %}
<li><h2><a href="{{ url_for('users.show', id = user.id) }}">{{ user.username }}</a></h2>{{ user.email }}
<form action="{{ url_for('users.destroy', id=user.id) }}" method="POST">
<input type="hidden" name="_method" value="DELETE" />
<input type="submit" value="Delete user" />
</form>
</li>
{% endfor %}
</ul>
<a href="{{url_for('users.new') }}">new user</a>
<form action="{{ url_for('users.logout') }}" method="POST">
<input type="submit" value="logout" />
</form>
{% endblock %}

最後執行看看

$ python manage.py runserver

進到/users頁面,會跳轉到/users/login要求登入。

輸入剛剛建立的email與密碼登入後。

其實在Flask要實作登入可以有更方便的選擇:Flask-Login

附上github連結:

--

--