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 Classclass 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
จาก 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>
ทบทวน
และนี่คือฟังก์ชันต่าง ๆ ที่เราได้ทำการอิมพอร์ตเข้ามาในโปรเจคท์เราในวันนี้
และนี่ก็คือไฟล์ 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