Flask Tutorial Part 3: User Authentication and Basic Form in Flask

Tiếp tục với bài tech blog lần trước, lần này ta sẽ tiếp tục tìm hiểu về framework Flask. Bài lần này sẽ đề cập đến 2 vấn đề sau:

  • Tạo Form dể người dùng đăng ký vào hệ thống
  • Sử dụng plugin flask-login để authentication cho app

Tạo Form để người dùng đăng ký vào hệ thống

Khi làm việc với web framwork thì việc thao tác với form là việc chắc chắn sẽ xảy ra, bởi vậy các thao tác trên Form như nhận input, validator các input nhận được từ user là vô cùng quan trọng. Ta sẽ sử dụng hai thư viện WTFormsFlask-WTF để tạo form và validate form. Ta cần thêm 2 thư viện vào file requirements.txt.

flask-wtf==0.9.5
WTForms-Alchemy==0.13.0

và chạy:

pip install -r requirements.tx

để cài đặt.

Sau khi đã cài đặt xong, ta sẽ thêm một file app/forms.py để lưu các forms sẽ dùng trong app và tạo một signup form đơn giản như sau:

from flask_wtf import Form
from wtforms import TextField, SubmitField, validators, PasswordField, HiddenField, BooleanField, IntegerField, FormField
from models import User

class SignupForm(Form):
    username = TextField('Username',  [
        validators.Required('Please enter your username.'),
        validators.Length(max=30, message='Username is at most 30 characters.'),
    ])
    email = TextField('Email',  [
        validators.Required('Please enter your email address.'),
        validators.Email('Please enter your email address.')
    ])
    password = PasswordField('Password', [
        validators.Required('Please enter a password.'),
        validators.Length(min=6, message='Passwords is at least 6 characters.'),
        validators.EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')
    submit = SubmitField('Create account')

    def __init__(self, *args, **kwargs):
        Form.__init__(self, *args, **kwargs)

    def validate(self):
        if not Form.validate(self):
            return False

        user = User.query.filter_by(username=self.username.data).first()
        if user:
            self.username.errors.append('That username is already taken.')
            return False
        user_email = User.query.filter_by(email=self.email.data).first()
        if user_email:
            self.username.errors.append('That email is already taken.')
            return False

        return True

Ta sẽ giải thích qua về các class SignupForm này. Class này kế thừa từ class Form của flask_wtf và gồm có các field như sau:

  • username: Ta chọn kiểu dữ liệu là TextField và thêm validator yêu cầu người dùng phải nhập, tối đa là 30 ký tự đi kèm với message báo lỗi
  • email: Ta chọn kiểu dữ liệu là TextField và thêm validator yêu cầu người dùng phải nhập, và dữ liệu nhập vào phải theo format email
  • password: Ta chọn kiểu dữ liệu là PasswordField, tối thiểu 6 ký tự, và ở đây ta có dùng thêm validator EqualTo với trường confirm để đảm bảo người dùng nhập đúng.
  • submit: một nút để submit

Các kiểu trường như TextField, SubmitField, validators, PasswordField, HiddenField, BooleanField, IntegerField, FormField đều đã được wtforms định nghĩa sẵn, kèm với đó là các validator, bạn có thể đọc thêm tại đây và tại đây

Đối với form thì phần ko thể thiếu đó chính là validate. Ngoài các validate đã được định nghĩa ở các trường, ta có thể customize hàm validate của Form bằng cách định nghĩa hàm validate, hàm này return True hoặc False kèm với message cho ta biết các input nhập vào có hợp lệ hay không. Như trong SignupForm, ta có thực hiện kiểm tra xem username và email đã nhập vào đã tồn tại tron DB hay chưa, nếu đã tồn tại return False, chưa thì True.

Sau khi đã có form, ta sẽ thêm router để xử lý sự kiện người dùng sign-up như ta sửa lại trong file app/views.py như sau:

from app import app
from flask import render_template, flash, redirect, url_for
from forms import SignupForm
from models import User

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
def index():
    return render_template(
        'index.html',
    )

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    form = SignupForm()
    if form.validate_on_submit():
        new_user = User(form.username.data, form.email.data, form.password.data)
        db.session.add(new_user)
        db.session.commit()
        flash('Signed up successfully.', category='success')
        return redirect(url_for('index'))
    return render_template('signup.html', form=form)

Ta giữ lại hai route indexsignup. Trong sigup, ta khởi tạo form và kiểm tra xem khi form được submit thì các input nhập vào đã được validate hãy chưa, nếu tất cả đều hợp lệ, ta tạo user mới:

    def __init__(self, username, email, password):
        self.username = username
        self.name = username
        self.email = email.lower()
        self.set_password(password)

và sau đó sử dụng session để lưu vào DB và flash cho người dùng biết là sign-up đã thành công. Easy ? Tuy nhiên khi chạy thì sẽ có lỗi:

jinja2.exceptions.TemplateNotFound

TemplateNotFound: signup.html

Đó là do ta chưa có template. Hãy tạo một file signup.html ở trong thư mục app/templates:

{% extends "base.html" %}
{% block content %}
{% for message in form.username.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
{% for message in form.email.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
{% for message in form.password.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
{% for message in form.confirm.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
<form class="form-signup" role="form" action="{{ url_for('signup') }}" method="POST">
    <h2 class="form-signup-heading">Please sign up</h2>
    <input name="username" type="text" class="form-control" placeholder="Username" required autofocus>
    <input name="email" type="email" class="form-control" placeholder="Email address" required>
    <input name="password" type="password" class="form-control" placeholder="Password" required>
    <input name="confirm" type="password" class="form-control" placeholder="Password Confirm" required>
    {{ form.hidden_tag() }}
    <br>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
</form>
{% endblock %}

Tương tự như ở index.html, ta extend layout từ base.html, và ở phía trên, để hiện thị được các lỗi mà người dùng nhập vào, ta cầm lấy các giá trị trong form.AAAAA.errors để hiển thị.

Thêm một điểm cần lưu ý đó là do có sử dụng CRSF token, ta cần có {{ form.hidden_tag() }} để khi render ra có include token trong form:

<div style="display:none;"><input id="csrf_token" name="csrf_token" type="hidden" value="1422378558.79##f8c88444f90c2e2129deeda71b4f51d422f07940"></div>

Ta cũng cần sửa lại file base.html để có thể flash message ra.

Flash khi signup thành công:

kiểm tra trong DB:

mysql> select * from users;
+----+----------+--------------------------------------------------------------------+--------+-----------------------------+------+
| id | username | password                                                           | name   | email                       | role |
+----+----------+--------------------------------------------------------------------+--------+-----------------------------+------+
|  1 | tienna   | pbkdf2:sha1:1000$7ksIXows$ab238cec08482299feaab23a8fa777b651527480 | tienna | [email protected] |    0 |
+----+----------+--------------------------------------------------------------------+--------+-----------------------------+------+
1 row in set (0.02 sec)

Sử dụng flask-login để authentication

Việc authentication user vào hệ thống cũng là một việc rất thường gặp. Thật may vì đã có sẵn rất nhiền thư viện giúp chúng ta implement các bước để authentication, trong bài này ta sẽ sử dụng flask-login. Thêm dòng sau vào requirements.txt và cài đặt với pip.

flask-login==0.2.11

Ta cần import thư viện này và init nó trong app/init.py như sau:

from flask.ext.login import LoginManager

lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'

Hãy chú ý thiết lập lm.login_view = 'login' nghĩa là ta sẽ sử dụng route login cho người dùng đăng nhập vào hệ thống.

Ta cần tạo login form để người dùng có thể đăng nhập. Thêm class LoginForm trong app/forms.py:

class LoginForm(Form):
    username = TextField('Username',  [
        validators.Required('Please enter your username.'),
        validators.Length(max=30, message='Username is at most 30 characters.'),
    ])
    password = PasswordField('Password', [
        validators.Required('Please enter a password.'),
        validators.Length(min=6, message='Passwords is at least 6 characters.'),
    ])
    submit = SubmitField('Sign In')

    def __init__(self, *args, **kwargs):
        Form.__init__(self, *args, **kwargs)

    def validate(self):
        if not Form.validate(self):
            return False

        user = User.query.filter_by(username=self.username.data).first()
        if user and user.check_password(self.password.data):
            return True
        else:
            self.password.errors.append('Invalid username or password')
            return False

Ko có gì quá khó hiểu với form này, hai trường username vào password, cùng với đó là validate xem password nhập vào đã đúng chưa bằng helper check_password.

Flask-login yêu cầu ta định nghĩa một hàm call_back load_user để load user từ user_id chứa trong session. Đơn giản là một hàm với tham số truyền vào là id và trả về User tương ứng trong DB.

@lm.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

Thêm vào đó, ta sẽ set user hiện tại vào biến global g của Flask để tiện thao tác trong các router. current_user chính là user tương ứng với session hiện tại của flask-login.

@app.before_request
def before_request():
    g.user = current_user

Tiếp đó là route login trong app/views.py:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if g.user is not None and g.user.is_authenticated():
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        # login and validate the user...
        user = User.query.filter_by(username=form.username.data).first()
        login_user(user)
        flash('Logged in successfully.', category='success')
        return redirect(request.args.get('next') or url_for('index'))
    return render_template('login.html', form=form)

Nếu user đã login, ta redirect về trang index, ko thì ta sẽ kiểm tra login form, login user với hàm login_user và flash message.

Ta tạo thêm view cho phần login ở file app/templates/login.html

{% extends "base.html" %}
{% block content %}
{% for message in form.username.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
{% for message in form.password.errors %}
<div class="alert alert-danger">{{ message }}</div>
{% endfor %}
<form class="form-signin" role="form" action="" method="POST">
    <h2 class="form-signin-heading">Please login</h2>
    <input name="username" type="text" class="form-control" placeholder="Username" required autofocus>
    <input name="password" type="password" class="form-control" placeholder="Password" required>
    {{ form.hidden_tag() }}
    <br>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Log in</button>
</form>
{% endblock %}

Sửa lại base.html để hiển thị tên user đã đăng nhập, thực hiện kiểm tra ở biến g.user:

<ul class="nav navbar-nav navbar-right">
{% if g.user.is_authenticated() %}
<li class="dropdown">
  <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ g.user.name }} <span class="caret"></span></a>
  <ul class="dropdown-menu" role="menu">
    <li><a href="#">Profile</a></li>
    <li class="divider"></li>
    <li><a href="#">Log out</a></li>
  </ul>
</li>
{% else %}
<li><a href="{{ url_for('login') }}">Log in</a></li>
{% endif %}
</ul>

Sau khi authenticate user, ta có thể sử dụng decorate login_required đối với các view cần người dùng đăng nhập vào hệ thống. Và tất nhiên là ko thể thiếu route logout cho người dùng.

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Logged out successfully.', category='success')
    resp = make_response(redirect(url_for('index')))
    return redirect(url_for('index'))

Final

Vậy là ta đã thực hiện xong việc đăng ký người dùng cũng như authentication người dùng vào hệ thống.

Reference