Flask-Login/Logout สร้างระบบ Authentication Systems —[ Ep.1 Introduction, LoginManager, login_required, user_loader]

flask-login เป็นไลบรารี่ที่ได้รับความนิยมสูงสุดสำหรับ Flask Framework สำหรับพัฒนาระบบยืนยันตัวตน (Authentication Systems) สำหรับใน tutorial นี้จะเป็นการสอนการใช้งาน Flask Authentication Systems โดยประยุกต์ใช้งาน flask-login ไลบรารี่ เข้ามาใช้ในโปรเจคท์นี้ พร้อมทั้งทำความรู้จักกับ LoginManager คลาส login_required decorator, และ user_loader ฟังก์ชัน รวมไปถึงการสร้างแบบฟอร์มสำหรับการล็อกอินในหน้าเว็บ

สำหรับท่านใดที่ยังไม่รู้ว่า Flask คืออะไร ทำไมถึงต้องใช้ Flask รับชมได้ที่วิดีโอนี้

Flask Introduction- แนะนำการเขียนพัฒนาเว็บด้วยไพธอน(Flask)

จุดประสงค์

  • เข้าใจและสามารถใช้งาน flask-login ไลบรารี่ได้
  • เข้าใจกระบวนการทำงานในระบบยืนยันตัวตน(Authentication Systems)ของผู้ใช้งาน
  • เข้าใจการทำงานของ LoginManager
  • เข้าใจ user_loader
  • เข้าใจและสามารถใช้ login_required decorator ในการจำกัดการเข้าถึงหน้าเว็บได้
  • เข้าใจกระบวนการทำงานของฟอร์มและสามารถสร้างฟอร์มในหน้าเว็บเพื่อใช้สำหรับการล็อกอิน
  • ฯลฯ

หลังจาก Ep.11/1 ที่ผ่านมาเราได้ทำการสร้างหน้าลงทะเบียน พร้อมทั้งได้มีการ hash password

หน้า sign-up ใน Ep.11/1 ที่ผ่านมา

ถาม: เปิดบทความมาทำไมเจอ Flask-Login เลย ทำไมไม่มีระบบลงทะเบียนผู้ใช้งาน หรือระบบลงทะเบียนนั้นทำไว้ในคลิปไหนหรือบทความไหนหรือไม่

ตอบ: บทความนี้ทำมาสำหรับนักเรียนคอร์ส Python Web Development with Flask ครับ แต่สำหรับท่านที่ไม่ได้เรียนในคอร์สนี้ก็ไม่เป็นไรครับ สามารถอ่านเป็นแนวทางและทำความเข้าใจและต่อยอดได้เช่นกันครับ

Flask-Login

ก่อนอื่นมาทำความรู้จักกับพระเอกเราในวันนี้ครับ นั่นก็คือเจ้าตัวไลบรารี่ flask-login

  • เก็บ ID ของผู้ใช้ไว้ใน Session ช่วยให้สามารถทำการ Login-Logout ได้อย่างง่ายดาย
  • ช่วยจัดการฟีเจอร์ “remember me” ได้ง่าย.
  • ช่วยป้องกันการขโมย Session ของผู้ใช้จากคุ๊กกี้.
  • นำไปประยุกต์ใช้กับ 3rd Party Libraries หรือ Extension ด้าน Authorization ต่าง ๆ ได้
  • ฯลฯ

อ่านเพิ่มเติม Flask-Login Documentation

ไม่รอช้าไปเริ่มการผจญภัยสำหรับ Flask-Login Tutorial นี้กันได้เลย

1. ทำการติดตั้ง flask-login

pip install flask-login

2. อิมพอร์ต flask-login เข้ามาใช้งาน

หลังจากติดตั้งเสร็จ จากนั้นทำการอิมพอร์ตเข้ามาใช้งาน

from flask_login import LoginManager, login_required

โดย

LoginManager → คลาสที่ต้องกำหนดเข้ามาเพื่อใช้จัดการกับ Login

login_required → decorator ฟังก์ชันที่ใช้สำหรับป้องกันหน้าเว็บให้สำหรับผู้ใช้ที่ได้ยืนยันตัวตนแล้วเท่านั้นที่จะสามารถเข้าดูหน้านี้ได้

3. ทดสอบจำกัดการเข้าถึงหน้าเว็บ

โดยใช้ login_required decorator โดยทดสอบที่หน้า about.html

# app.py...
@app.route('/about')
@login_required # Newdef about(): return render_template("about.html")
...

ปล. ในโค้ดบล็อคจะมีการใช้ เพื่อบ่งบอกว่าให้โฟกัสเฉพาะโค้ดในขอบเขตนี้เท่านั้น ซึ่งจะมีโค้ดส่วนอื่นที่อยู่ทั้งด้านบนและด้านล่าง และโค้ดเหล่านี้คือโค้ดใหม่ที่ได้ทำการสร้างขึ้นมาใน Tutorial นี้

ทำการรันเซิร์ฟเวอร์

ทดสอบคลิ๊กเข้าดูหน้า about me
ยังมีเออเร่อ

AttributeError: ‘Flask’ object has no attribute ‘login_manager’

ในตอนนี้โปรแกรมยัง error เพราะว่ายังไม่มีตัวแอตทริบิวต์ login_manager

4. ทำการสร้างออปเจ็คท์สำหรับเรียกใช้งานคลาส LoginManager

โดยทำการสร้างออปเจคท์ กำหนดชื่อตัวแปรเป็น login_manager เพื่อที่จะสามารถใช้งาน Properties และ Methods ต่าง ๆ ในคลาส LoginManager ได้ ซึ่งก็จะทำให้เราสามารถใช้งาน Flask ร่วมกันกับไลบรารี่ flask-login ได้นั่นเอง

# app.py
...
from flask_login import LoginManager, login_required
app = Flask(__name__)app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'db = SQLAlchemy(app)login_manager = LoginManager() # Login manager for flask-login # Newlogin_manager.init_app(app) # New...
ยังไม่ได้เรียกใช้งาน user_loader method

Exception: Missing user_loader or request_loader. Refer to http://flask-login.readthedocs.io/#how-it-works for more info.

ตอนี้จะเห็นว่ายังมีเออเร่ออยู่ ก็เพราะว่าเราต้องทำการโหลด user เข้ามาทุกครั้ง โดยใช้เมธอด user_loader เพื่อโหลดผู้ใช้จาก ID ใน session

ตอนนี้เราได้ทำการ initialize ตัว login_manager เพื่อใช้งานคลาส LoginManager เรียบร้อย สังเกตได้ในโค้ด login_manager.init_app(app) แต่ว่ายังไม่ได้เรียกใช้งาน user_loader

# login_manager.pyclass LoginManager(object):    ...    def user_loader(self, callback):  # เมธอด user_loader    '''    This sets the callback for reloading a user from the session. The    function you set should take a user ID (a ``unicode``) and return a    user object, or ``None`` if the user does not exist.    :param callback: The callback for retrieving a user object.    :type callback: callable    '''    self._user_callback = callback    return callback
...

ปล. โค้ดด้านบนเป็นแค่ตัวอย่างเพื่อแสดงให้ทราบแหล่งที่มาของ class และ method ที่เรากำลังเรียกใช้งาน ซึ่งเป็นโค้ดที่มากับแพ็คเกจ ดังนั้นทำความรู้จักไว้ก็จะดี แต่ยังไม่ต้องโฟกัสตรงส่วนนี้มากนัก เพราะอยู่นอกเหนือขอบเขตของบทความนี้

5. เรียกใช้งาน user_loader

เรียกใช้งาน user_loader โดยต้องผ่านตัว login_manager ก่อน

# app.py
...
class Course(db.Model):

"""Create this course table to store course details"""
...# New
# สร้างฟังก์ชันload_user สำหรับโหลดผู้ใช้จาก ID
@login_manager.user_loaderdef load_user(user_id): return User.query.get(int(user_id))
...

ทำการรันเซิร์ฟเวอร์หรือรีเฟรชหน้าเว็บ

ไม่สามารถเข้าดูหน้านี้ได้ เพราะว่าหน้าเว็บได้ถูกป้องกันการเข้าถึงเรียบร้อย

Unauthorized: The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn’t understand how to supply the credentials required.

ตอนนี้จะเห็นได้ว่าโปรแกรมทำงานถูกต้อง เพียงแต่แสดงผลหน้า 401 Unauthorized นั่นก็เพราะว่าเฉพาะผู้ใช้ที่ได้ยืนยันตัวตน (Authenticated User) เท่านั้นถึงจะสามารถเข้าใช้งานได้ แต่ในตอนนี้เรายังไม่ได้ทำการเขียนโค้ดฟังก์ชั่นนั้น ซึ่งจะอยู่ในหัวข้อถัด ๆ ไปที่กำลังจะกล่าวถึง

HTML Login Form

จากคลิปคอร์ส Ep.11/1 ในวิดีโอที่ผ่านมาเรายังไม่ได้สร้างแบบฟอร์มขึ้นมา มีเพียงแค่ลิ้งค์ใน Navigation Bar

ตอนนี้มีเพียงข้อความ Log in ใน Navbar แต่ว่ายังไม่ได้สร้างลิ้งค์ไปยังหน้า login

แต่ยังไม่ได้สร้างหน้า login.html เพื่อทำการลิ้งค์ไปที่หน้าล็อกอินฟอร์ม พอคลิ๊กก็ยังไม่มีอะไรเกิดขึ้น

6. สร้างหน้า Login Form

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

  • ส่งข้อมูลยิงเข้าไปที่ endpoint ก็คือฟังก์ชัน login ที่ได้กำหนด url(Route) ไว้เมื่อมีการ submit หน้าฟอร์ม โดยกำหนดเป็น POST method
<form action="{{url_for('login')}}" method="POST">
  • แท็ก input ของ username กำหนด type เป็นชนิดข้อความ ตัวอักษร (text) และตัวแอตทริบิวต์ที่ส่งเข้าไปที่ฝั่งเซิร์ฟเวอร์คือ username ซึ่งในฟังก์ชัน login ของ Flask ก็จะเขียนรอรับค่าแอตทริบิวต์ตัวนี้เพื่อประมวลผลต่อไป
<input type="text" class="form-control" name="username">
  • แท็ก input ของ password กำหนด type เป็นชนิดรหัสผ่าน สังเกตได้อย่างง่าย ๆ เวลาที่เราทำการกรอกแบบฟอร์ม ถ้าฟอร์มที่เป็นรหัสผ่าน (password) จะมีเครื่องหมาย “.(Dot) หรือไม่ก็ “*(Asterisk) ซ่อนรหัสผ่านไว้ และตัวแอตทริบิวต์ที่ส่งเข้าไปที่ฝั่งเซิร์ฟเวอร์คือ password ซึ่งในฟังก์ชัน login ของ Flask ก็จะเขียนรอรับค่าแอตทริบิวต์ตัวนี้เพื่อประมวลผลต่อไป
<input type="password" class="form-control" name="password">

จะได้โค้ดฉบับเต็มดังนี้

<!-- login.html -->{% extends 'base.html' %}{% block title %}<title>Login</title>{% endblock title %}{% block content %}<br><div class="container" style="align-items: center;">    <div class="row">        <div class="col-lg-5"><form action="{{url_for('login')}}" method="POST">                <!-- Header Text -->                <div>                    <h5 style="text-align: center;">You have to login before reading this post</h5>                    <p>Didn't have an account ? <a href="     {{url_for('sign_up')}}">Sign Up</a></p>                 </div>
<!-- Username --> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" name="username"> </div>
<!-- Password --> <div class="form-group"> <label>Password</label> <input type="password" class="form-control" name="password"> </div>
<!-- Button for submitting form --> <div style="text-align: center;"> <button type="submit" class="btn btn-primary">Login</button> </div> </form><br><br> </div> </div></div>{% endblock content %}

7. สร้างฟังก์ชัน login ใน app.py เพื่อ render HTML ไปแสดงผล

เขียนฟังก์ชัน login ต่อจาก sign_up ใน ep.11/1 ที่ผ่านมา

# app.py
...
# New@app.route('/login')def login():
# Do nothing, only render HTML now return render_template("login.html")....

จากนั้นทำการกำหนด url ใน Navbar link ซึ่งตอนนี้ยังเป็น # ในแท็ก href

<a class="dropdown-item" href="#">Log in</a>

โดยทำการเปลี่ยนเป็น url_for ของฟังก์ชันทั้ง Login เข้าไปในแท็ก href

<!-- base.html -->...<div class="dropdown-menu" aria-labelledby="navbarDropdown">    <a class="dropdown-item" href="{{url_for('login')}}">Log in</a>    <a class="dropdown-item" href="#">Log out</a>    <div class="dropdown-divider"></div>        <a class="dropdown-item" href="{{url_for('sign_up')}}">Sign up</a></div>...

รีเฟรชหน้าเว็บจะได้หน้าแบบฟอร์มสำหรับล็อกอินแสดงผลขึ้นมา

ได้หน้าฟอร์มสำหรับการล็อกอินเรียบร้อย แต่ว่ายังไม่สมบูรณ์ ต้องเขียนฟังก์ชันในฝั่ง backend เพิ่ม

ทดสอบลงทะเบียนผู้ใช้งานเพื่อรับ username และ password เพื่อทดสอบใช้งานการล็อกอิน จากนั้นคลิ๊ก Login

Method Not Allowed

ซึ่ง Method Not Allowed สาเหตุมาจากตอนนี้เราได้ทำการส่งแบบฟอร์มในรูปแบบของ POST แต่จำได้ใช่ไหมครับว่าในฟังก์ชัน login ที่เขียนไว้ใน app.py เรายังไม่ได้ทำการกำหนด method เป็น POST เพราะในตัว default ของ app.route นั้นจะเป็น GET method

จากนั้นให้ทำการเพิ่ม “POST” method

# app.py
...
# @app.route('/login') # Default is "GET" method
@app.route('/login', methods=['GET', 'POST']) New --> add "POST"
...

จากนั้นให้ทำการ Login ดูใหม่อีกครั้งจะเห็นว่าไม่มี Error ใด ๆ เกิดขึ้นแล้ว เพียงแต่ในตอนนี้จะยังไม่สามารถล็อกอินได้เพราะเรายังไม่ได้เขียนโค้ดให้ฟังก์ชัน Login ทำงานได้อย่างสมบูรณ์ มีเพียงแค่การ render แบบฟอร์มหน้าล็อกอินออกไปแสดงผลเพียงเท่านั้น

โค้ดทั้งหมดของ app.py ดูเพิ่มเติมได้ ที่นี่

Demo Website

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

พบกับ Ep ถัดไป Ep.2 กันต่อได้เลยครับ

Sonny STACKPYTHON

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

Instagram: stackpython

Facebook: stackpython

Website: stackpython.co

YouTube: stackpython

Reference

flask-login documentation

Full Stack Python Developers