Flask-Login/Logout สร้างระบบ Authentication Systems — [ Ep.2] — UserMixin, check_password_hash, flask_wtf, SECRET_KEY

สำหรับใน Ep.2 นี้จะเป็นการเขียนฟังก์ชัน login เพื่อรับค่าจากฟอร์มที่เราเขียนไว้ในฝั่ง Frontend กันครับ และทำการเช็ค password โดยใช้ check_password_hash รวมไปถึงการใช้ UserMixin คลาสเข้ามาช่วยเพื่อสืบทอดเมธอดต่าง ๆ ที่จำเป็นในระบบ Login-Logout ในคลาส User ของเรา การใช้งาน flask_wtf รวมไปถึงการทำความรู้จักกับ SECRET_KEY พร้อมการใช้งานกันครับ

จุดประสงค์

  • เข้าใจและสามารถใช้งาน UserMixin คลาส เพื่อสืบทอดและเรียกใช้งานเมธอดเกี่ยวกับการ Login-Logout ได้
  • เข้าใจและสามารถใช้งาน flask-wtf ได้
  • เข้าใจ flash message
  • เข้าใจ SECRET_KEY และสามารถสร้าง SECRET_KEY เพื่อใช้งานได้
  • ฯลฯ

หลังจากใน Ep.1 ในส่วนของ flask-login ได้ทำการอิมพอร์ต LoginManager, login_required มาเรียบร้อย และใน Ep.2 นี้ให้ทำการอิมพอร์ตเข้ามาเพิ่มอีก 4 ตัวคือ

  • UserMixin
  • login_user
  • logout_user
  • current_user

pending

from flask_login import LoginManager, login_required ,UserMixin, login_user, logout_user, current_user

Flask-WTF

flask_wtf เป็นไลบรารี่ของ Flask ที่เอาไว้ใช้สำหรับจัดการเกี่ยวกับฟอร์มไม่ว่าจะเป็นการ Validate, การสร้างฟีลด์ ฯลฯ

ติดตั้ง flask_wtf

pip install flask_wtf

ทำการอิมพอร์ตเข้ามาใช้งานได้เลย โดยสร้างไฟล์ขึ้นมาใหม่อีก 1 ไฟล์คือ form.py

โดยทำการอิมพอร์ตเข้ามาดังนี้

# form.pyfrom flask_wtf import FlaskFormfrom wtforms import StringField, PasswordField, SubmitFieldfrom wtforms.validators import DataRequired

ทำการสร้างคลาสที่มีชื่อว่า LoginForm และทำการสืบทอด คลาสแม่ (Parent Class) เข้ามาใช้ในคลาส เพื่อเรียกใช้งาน properties และ methods ต่าง ๆ ในคลาสนี้ เช่น validate_on_submit ที่จะใช้ในขั้นตอนการซับมิตหรือล็อกอินเข้ามาแล้วทำการตรวจเช็คข้อมูล เป็นต้น

# form.py...# Login Form Classclass LoginForm(FlaskForm):    username = StringField("Username", validators=[DataRequired()])    password = PasswordField("Password", validators=[DataRequired()])    submit = SubmitField('Submit')

จากคลาส LoginForm ทำการกำหนด field ขึ้นมา 3 fields คือ

  • username → กำหนดเป็นสตริงฟีลด์ โดยฟีลด์นี้ต้องการ 2 อากิวเมนต์ คือ 1. label กำหนดเป็น “Username” และ 2 คือ validator ที่ใช้ในการ validate ฟอร์ม โดยในที่นี้กำหนดให้มีการ validate
  • password → กำหนดเป็นพาสเวิร์ดฟีลด์ กำหนดเป็นสตริงฟีลด์ โดยฟีลด์นี้ต้องการ 2 อากิวเมนต์ คือ 1. label กำหนดเป็น “Password” และ 2 คือ validator ที่ใช้ในการ validate ฟอร์ม โดยในที่นี้กำหนดให้มีการ validate
  • submit → กำหนดเป็นซับมิตฟีลด์

form.py สมบูรณ์

# form.pyfrom flask_wtf import FlaskFormfrom wtforms import StringField, PasswordField, SubmitFieldfrom wtforms.validators import DataRequired
# Login Form Class
class LoginForm(FlaskForm): username = StringField("Username", validators=[DataRequired()]) password = PasswordField("Password", validators=[DataRequired()]) submit = SubmitField('Submit')

UserMixin

UserMixin คือคลาสที่ได้รวบรวมเมธอดต่าง ๆ ที่จำเป็น สำหรับการใช้งานการ login โดยเมธอดเช่น

is_authenticated

is_active

is_anonymous

get_id()

pending

โดยปกติใคลาส User เราต้องทำการกำหนดเมธอดเหล่านี้ขึ้นมาเอง แต่…ไม่ต้องครับข่าวดีคือ เราสามารถที่จะทำการสืบทอดจากคลาส UserMixin ได้เลย โดยไม่ต้องเสียเวลาสร้างขึ้นมาใหม่เอง (Don’t reinvent the wheel) ทำได้โดย

# Inherit all necessary methods from UserMixin classclass User(UserMixin, db.Model):

ทำการกำหนดตัวเมธอดพิเศษที่ชื่อ __init__( ) โดยเป็นรูปแบบปกติและทั่วไปในการสร้าง class/object ของภาษาไพธอน เพื่อกำหนด properties (ตัวแปรที่ใช้เก็บข้อมูล) ให้กับคลาส User โดยแน่นอนว่ามีจำนวนทั้งสิ้ง 3 ตัว คือ username, password และ email

def __init__(self, username, password, email):    self.username = username    # self.password = generate_password_hash(password)    # Hash the password when immediately registered    # Hash the password when immediately registered    self.password = password    self.email = email

ตัวแปร password จะถูกส่งเข้าไปในฟังก์ชัน check_password_hash( ) ต่อไป

โดยในคลาส User จะมีการสร้างเมธอดขึ้นมาอีกตัวชื่อว่า verify_password ( ) ซึ่งสามารถกำหนดชื่อได้ตามต้องการ โดยเมธอดนี้จะทำการรีเทิร์นฟังก์ชัน check_password_hash( ) ออกไปใช้งานในฟังก์ชัน login เพื่อทำการเช็คพาสเวิร์ดของผู้ใช้ที่ได้ทำการกดซับมิตมา โดยจะทำการส่งตัวแปร password เข้าไปเป็นอากิวเมนต์ตัวแรกในฟังก์ชันนี้เพื่อให้ทำการเช็คพาสเวิร์ดที่ถูก hash

def verify_password(self, pwd):    return check_password_hash(self.password, pwd)

จะได้โค้ดทั้งหมดในคลาส User

# app.py...# Inherit all necessary methods from UserMixin classclass User(UserMixin, db.Model):    """Create columns to store our data"""    id = db.Column(db.Integer, primary_key=True)    username = db.Column(db.String(60), unique=True, nullable=False)    password = db.Column(db.String(20), unique=True, nullable=False)    email = db.Column(db.String(60), unique=True, nullable=False)    def __init__(self, username, password, email):        self.username = username        self.password = password        self.email = email    def __repr__(self):        return '<User %r>' % self.username    def verify_password(self, pwd):        return check_password_hash(self.password, pwd)...

pending

Login Function

หลังจากที่เราได้เซ็ตอะไรไว้เสร็จสิ้นเรียบร้อยและก็มาถึงอีกหนึ่งไฮไลต์สำคัญของบทความนี้ครับ นั่นก็คือการเขียนฟังก์ชัน login

ซึ่งในฟังก์ชัน login นี้เราต้องทำการอิมพอร์ตคลาสและฟังก์ชันเข้ามาใช้งานอีกหลายตัว ถ้าไม่อิมพอร์ตเข้ามาก็จะเจอเออเร่อในรูปแบบเดียวกันเลยคือ NameError: name ‘ฟังก์ชัน, เมธอด, คลาส หรือโมดูล’ is not defined ในกรณีนี้ซึ่งก็คือเราไม่ได้อิมพอร์ตฟังก์ชัน check_password_hash เข้ามาใช้งาน

ทำการอิมพอร์ตคลาส LoginForm ที่ได้สร้างไว้ในไฟล์ form.py

from form import LoginForm

ทำการอิมพอร์ตฟังก์ชัน check_password_hash เข้ามาใช้ด้วยอีกหนึ่งฟังก์ชัน เพราะว่าในเมธอด verify_password( )ที่เขียนไว้ในคลาส User มีการ return ฟังก์ชันนี้ออกมาใช้งาน

from werkzeug.security import generate_password_hash, check_password_hash # New

อิมพอร์ต flash และ url_for เข้ามาเพิ่มเติมอีก 2 ฟังก์ชัน

from flask import Flask, render_template, request, redirect, url_for, flash, url_for # New

โดยจะได้โค้ดตามด้านล่างนี้ในส่วนของฟังก์ชัน login

ผมได้คอมเมนต์อธิบายไว้ให้ก่อนในโค้ดแต่ละบรรทัดว่าทำงานอย่างไร เดี๋ยวจะมาเขียนอธิบายเพิ่มเติมในภายหลังครับ

# app.py...@app.route("/login", methods=["GET", "POST"])def login():    # Create an object called "form" to use LoginForm class    form = LoginForm()    username = form.username.data    password = form.password.data
# Validate a form submitted by a user if form.validate_on_submit():

# Query a user's username from the database
user = User.query.filter_by(username=username).first()
# Check and compare a user's password # in a database, if True, log a user in if user and user.verify_password(password):
# Log a user in after completing verifying a password # then flash a message "Successful Login" login_user(user) flash("Successful Login")
# Redirect to homepage return redirect(url_for('home'))
else: # Show flash message "Invalid Login" if login gets False flash("Invalid Login") else: # You can print or return something such as an error message # In this case, do nothing. But you can do it later pass
return render_template('login.html', form=form)...
ต้องกำหนด SECRET_kEY

SECRET_KEY

จาก Error ข้างบนจะเห็นว่าต้องมีการกำหนด SECRET_KEY ขึ้นมาก่อนก่อนที่จะใช้งานตัว CSRF (Cross Site Request Forgery) และเพื่อให้สามารถที่จะใช้งาน Session ได้ และมีความปลอดภัย

โดยทำการเพิ่มเข้ามาใน app.py

# app.py...# Hard coded secret key for development onlyapp.config['SECRET_KEY'] = "your-secret-key-here" ...

เราสามารถที่จะ hard-code หรือเขียนค่าของ SECRET_KEY เข้าไปได้แบบตรง ๆ แต่ต้องเป็นข้อความและอักษรที่เดาได้ยากมาก ๆ แต่ระหว่างการพัฒนานั้น สามารถใช้ข้อความง่าย ๆ ได้ไม่มีปัญหา

ในระหว่างการพัฒนา (Development Stage) เราสามารถที่จะใช้เช่นข้อความหรืออักษรง่าย ๆ เช่น“my-secret-key-for-development” แต่ถ้าโปรเจคท์ของเราขึ้นสู่โปรดักชั่นแล้ว ห้ามใช้คีย์นี้เด็ดขาด ต้องทำการ Generate มาใหม่โดยใช้ฟังก์ชันเฉพาะ จะเขียนเพิ่มให้ครับ

การ Genarate ตัว SECRET_KEY มาใช้งาน

pending

และในส่วนของ login.html ใน Ep.1 ที่ผ่านมาต้องทำการเพิ่ม

{{ form.csrf_token }}

<!-- login.html -->
...
<form action="{{ url_for('login') }}" method="POST"> {{ form.csrf_token }} ...
</form>
...

HTML

ใช้ Conditional Statement ใน HTML ได้โดยตรงผ่าน Jinja2 Template

{% if current_user.is_authenticated %}

โดยมี 2 เงื่อนไขคือ

  • เงื่อนไข 1 (if) ถ้าเป็นจริง ๆ คือ ถ้า User มีการล็อกอินเข้ามา และได้เป็นผู้ใช้ที่ยืนยันตัวตนเรียบร้อยแล้ว (Authenticated User) ให้แสดงชื่อผู้ใช้ เช่น ถ้าผู้ใช้ที่ล็อกอินเข้ามาชื่อ Sonny ก็จะปรากฏข้อความ Hi Sonny
Hi {{ current_user.username }}!
  • เงื่อนไข 2 (else) ถ้าไม่มีการล็อกอินเข้ามาให้แสดงข้อความ “Not authenticated now

จะได้โค้ดดังด้านล่างนี้

<!-- base.html --><!--Create link to show, user is authenticated or not--><li class="nav-item">    <a class="nav-link" href="#" style="color: aqua; font-weight: bold;">        {% if current_user.is_authenticated %}     Hi {{ current_user.username }}!        {% else %}     Not authenticated now        {% endif %}    </a></li>
เพิ่มแสดงผลสถานะของผู้ใช้ว่าได้าล็อกอินเข้ามาหรือยัง
ยังไม่ได้ล็อกอิน
User มีการล็อกอินเข้าใช้งานเรียบร้อยแล้ว

ทบทวน

และนี่คือฟังก์ชันต่าง ๆ ที่เราได้ทำการอิมพอร์ตเข้ามาในโปรเจคท์เราในวันนี้

ฟังก์ชันที่ได้อิมพอร์ตเข้ามาในโปรเจคท์เพิ่มในวันนี้

และนี่ก็คือไฟล์ app.py และ login.html

สำหรับ Flask-Login Ep.2 ก็ขอจบลงเพียงเท่านี้ครับ ถ้ามีคำถามหรือข้อสงสัยหรือไม่เข้าใจตรงไหนก็คอมเมนต์ได้ที่ด้านล่างบทความกันได้เลยครับ หรือถ้าอยากติชมหรือมีอะไรเสนอแนะก็คอมเมนต์เข้ามาได้เช่นกันครับ

พบกับ Ep ถัดไป Ep.3 ซึ่งเป็น Ep สุดท้าย (เป็นโบนัส) กันต่อได้เลยครับ

Sonny STACKPYTHON

ท่านสามารถติดตามพวกเราได้ที่ stackpython ตามช่องทางด้านล่างนี้ได้เลยครับ

Instagram: stackpython

Facebook: stackpython

Website: stackpython.co

YouTube: stackpython

References

flask-login documentation

flask-wtf

Full Stack Python Developers