การพัฒนาระบบ OTP (One Time Password) และ Session Server ด้วย Redis และ Flask สำหรับ Microservice

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

บทความนี้ผู้อ่านจะได้คลายข้อสงสัยเกี่ยวกับการทำเว็บไซต์ ในส่วนของการเข้าสู่ระบบ ซึ่งมีการพิสูจน์ตัวตนด้วย OTP ที่ส่งไปยังผู้ใช้ ทาง Email รวมทั้งเทคนิคการทำให้ Session ไม่หลุด เมื่อมีการทำ Load Balance และเมื่อมีการเพิ่มจำนวน Service ให้รองรับโหลดได้มากขึ้น (Scale Out) แบบไม่มี Downtime ด้วยการจัดเก็บ OTP และ Session ลง In Memory Data Structure Store ครับ

หมายเหตุ สำหรับผู้อ่านใหม่ที่ต้องการจะ Implement ตาม สามารถอ่านบทความ 3 บทความก่อนหน้านี้ได้

Redis

Redis เป็นระบบเก็บข้อมูลแบบ Key/Value บนหน่วยความจำ หรืออาจมองว่ามันเป็น NoSQL Database ตัวหนึ่ง ที่ทำงานบน RAM ซึ่งทำให้มันทำงานได้เร็วมากๆ

Redis ยังมีความสามารถในการ Set Expire ให้กับ Key เราจึงนำมันมา Implement ระบบ OTP (One Time Password) สำหรับยืนยันตัวตนในงานทะเบียนนักศึกษา โดยสมมติว่าเมื่อมีการประกาศรายชื่อผู้ผ่านการสอบสัมภาษณ์ ว่าที่นักศึกษาจะต้องเข้าระบบ กรอก Email ของตนเองเพื่อขอ OTP ซึ่งมีการกำหนดอายุไว้ 3 นาที หาก OTP หมดอายุ ผู้ใช้จะต้องขอ OTP ใหม่

Microservice Architecture

เพื่อให้เห็นภาพมากขึ้น เราจะสร้าง Microservice จำลองระบบงานทะเบียนนักศึกษา เพิ่มอีก 4 Service คือ

  • OTP เป็น Service ที่เก็บรายการ Email และ OTP สำหรับผู้ผ่านการสอบสัมภาษณ์ ในรูปของ Key/Value บน Redis Data Structure Store โดยจะถูกเรียกใช้ผ่าน RPC
  • Send Mail OTP เป็น Service ที่ส่ง OTP ไปยัง Email ของผู้ผ่านการสอบสัมภาษณ์ โดยจะถูกเรียกใช้ผ่าน RPC เช่นเดียวกัน
  • OTP Gateway เป็น Service ที่ควบคุม Flow ของงาน ซึ่งจะถูกเรียกใช้ผ่าน RESTful API
  • Register UI เป็นหน้า UI สำหรับกรอกข้อมูลการลงทะเบียน

เมื่อผู้ผ่านการสอบสัมภาษณ์ เข้าระบบ และกรอก Email ของตนเอง ผ่าน Register UI  Service จะมี Flow เกิดขึ้น ดังนี้

  • สร้าง OTP ใหม่ อายุ 3 นาที ที่ OTP Service
  • ส่ง OTP ไปยัง Email ของผู้ผ่านการสอบสัมภาษณ์ จาก Send Mail OTP Service
  • เมื่อผู้ผ่านการสอบสัมภาษณ์กรอก Email และ OTP ของตนเองได้ถูกต้อง จะมีการสร้างและจัดเก็บ Session ที่ Session Server
  • เมื่อผู้ผ่านการสอบสัมภาษณ์กรอก ชื่อ นามสกุล ที่ Register UI จะมีการเรียกใช้ Register Gateway Service เพื่อขึ้นทะเบียนเป็นนักศึกษา

OTP Service

เราจะสร้าง OTP Service ที่เก็บรายการ Email ของผู้ผ่านการสอบสัมภาษณ์ รวมทั้งการพิสูจน์ตัวตนด้วย OTP ตามขั้นตอนดังนี้

  • Remote Login ไปยัง Cloud Server โดยใช้ ssh
ssh nc-user@labxx.cpsudevops.com
  • สร้าง Project ชื่อ otp_dock ซึ่งภายใน Folder จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ python/
   |__ Dockerfile
   |__ requirements.txt
   |__ rpc.py
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  otp:
    container_name: otp
    build: python/
    restart: always
    
    depends_on:
      - otp_redis 
      
    networks:
      - microservice
      - default
      
  otp_redis:
    container_name: otp_redis
    image: "redis:alpine"
    
networks:
  default:
    external:
      name: otp_network
  microservice:
    external:
      name: microservice_network
  • แก้ไข Dockerfile ตามตัวอย่าง
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@rabbitmq:5672
  • แก้ไขไฟล์ requirements.txt
redis
nameko==2.10.0
validate_email
  • แก้ไขไฟล์ rpc.py
from nameko.rpc import rpc
import math, random 
from datetime import timedelta
from validate_email import validate_email
import redis

email_list_key = 'register_email'

def connect():
    redisConnect = redis.Redis(host='otp_redis', port=6379)
    return redisConnect

redisConnect = connect()

def generate_otp(email):
    digits = "0123456789"
    otp = ""

    for i in range(6) :
        otp += digits[math.floor(random.random() * 10)]

    return otp

def exist_key(email):
    data = redisConnect.exists(email)
    if data == 1:
        exist = True
    else:
        exist = False

    return exist
    
def exist_email(email):
    set_email = redisConnect.smembers(email_list_key)
    email = bytes(email, 'utf-8')
    if email in set_email:
        exist = True
    else:
        exist = False

    return exist

def val_email(email):
    is_valid = validate_email(email)
    return is_valid

class OTPService:
    name = "otp"

    @rpc
    def create(self, email):
        otp = 0
        if exist_email(email):
            otp = generate_otp(email)
            redisConnect.setex(email, timedelta(minutes=3), str(otp))
        return otp

    @rpc
    def delete(self, email):
        success = 0
        if exist_email(email):
            redisConnect.srem(email_list_key, email)
            success = 1

        return success

    @rpc
    def create_email_list(self, emails):
        val = {}
        setExpire = False
        for email in emails:
            if val_email(email) and not exist_email(email): 
                redisConnect.sadd(email_list_key, email)
                val[email] = 'done'
                setExpire = True
            else:
                val[email] = 'error'

        if setExpire:
           redisConnect.expire(email_list_key, 60*60*24*30)

        return val

    @rpc
    def authen(self, email, otp):
        success = 0
        if exist_key(email):
            test = redisConnect.get(email)
            otp = bytes(otp, 'utf-8')
            if test == otp:
               success = 1
               
        return success
  • สร้าง Bridge network โดยตั้งชื่อเป็น otp_network
docker network create otp_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ของ otp_dock ที่กำลังรันทั้งหมด ตามที่ docker-compose.yml ดูแล ด้วย Command line

Send Mail OTP Service

เมื่อผู้ผ่านการสอบสัมภาษณ์กรอก Email ได้ถูกต้อง Send Mail OTP Service จะส่ง OTP สำหรับการพิสูจน์ตัวตนกลับทาง Email โดยเราจะ Config docker-compose และไฟล์อื่นๆ ตามขั้นตอนต่อไปนี้

  • แต่ก่อนที่จะติดต่อกับ Relay Host สำหรับการรับส่ง Email โดยใช้ Email Account เช่น ของ Google เราจะต้องมีการลงชื่อเข้าใช้ด้วยรหัสผ่านสำหรับแอป (app password) ตามคำแนะนำต่อไปนี้ >> การลงชื่อเข้าใช้ด้วยรหัสผ่านสำหรับแอป
  • สร้าง Project ใหม่ ภายใน Folder send_email_otp_dock ประกอบด้วย ไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ python/
   |__ Dockerfile
   |__ requirements.txt
   |__ rpc.py
  • แก้ไข docker-compose.yml โดยเปลี่ยน RELAY_HOST, RELAY_USERNAME และ RELAY_PASSWORD ด้วยข้อมูล Remote SMTP Server ของตัวเอง
version: '3'

services:
  send_email_otp:
    container_name: send_email_otp
    build: python/
    restart: always

    networks:
      - microservice
      - default

  smtp_otp:
    container_name: smtp_otp
    image: bytemark/smtp
    restart: always
    environment:
      RELAY_HOST: smtp.gmail.com
      RELAY_PORT: 587
      RELAY_USERNAME: perceptiondesignthailand@gmail.com
      RELAY_PASSWORD: xxx //Password จากการลงชื่อเข้าใช้ด้วย google app password

networks:
  default:
    external:
      name: email_otp_network
  microservice:
    external:
      name: microservice_network
  • แก้ไข Dockerfile ตามตัวอย่าง
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@rabbitmq:5672
  • แก้ไขไฟล์ requirements.txt
nameko==2.10.0
  • แก้ไขไฟล์ rpc.py
import smtplib
from email.message import EmailMessage
from nameko.rpc import rpc

def send_email(otp, email):
    msg = EmailMessage()
    text = "OTP สำหรับการลงทะเบียนนักศึกษาใหม่ของท่าน คือ " + str(otp) + ", OTP ของท่านจะหมดอายุใน 2 นาที"
    msg.set_content(text)

    msg['Subject'] = 'Register OTP'
    msg['To'] = email

    s = smtplib.SMTP("smtp_otp",25)
    s.ehlo()
    s.sendmail(from_addr = 'nuttachot@hotmail.com', to_addrs = email, msg = msg.as_string())
    s.quit()


class Email:
    name = "email_otp"

    @rpc
    def send(self, otp, email):
        send_email(otp, email)
  • สร้าง Bridge network โดยตั้งชื่อเป็น email_otp_network
docker network create email_otp_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

OTP Gateway Service

OTP Gateway เป็น Service ที่ทำหน้าที่ประสานงานกับ OTP Service และ Send Mail OTP Service รวมทั้งการรับ Request จากผู้ใช้ผ่าน RESTful API 3 ตัว ได้แก่

getotp ทำหน้าที่สร้าง OPT ส่งกลับทาง Email ตามบัญชีผู้ผ่านการสอบสัมภาษณ์
create_email_list ทำหน้าที่สร้างบัญชี Email ของผู้ผ่านการสอบสัมภาษณ์ โดยจะกำหนดอายุของบัญชีไว้ 30 วัน
authen ทำหน้าที่ยืนยันตัวตนในงานทะเบียนนักศึกษาด้วย Email และ OTP

โดยเราจะ Config docker-compose และไฟล์อื่นๆ ตามขั้นตอนต่อไปนี้

  • สร้าง Project ใหม่ ภายใน Folder otp_gateway_dock ประกอบด้วย ไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ python/
   |__ Dockerfile
   |__ requirements.txt
   |__ api.py
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  register:
    container_name: otp_gateway
    build: python/
    restart: always
    ports:
      - "7002:80"  
networks:
  default:
    external:
      name: microservice_network
  • แก้ไข Dockerfile ตามตัวอย่างด้านล่าง
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY api.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD uvicorn api:app --host 0.0.0.0 --port 80
  • แก้ไขไฟล์ requirements.txt เพื่อติดตั้ง Libray ที่จำเป็น
fastapi
uvicorn
pydantic
nameko==2.10.0
  • แก้ไขไฟล์ api.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
from nameko.rpc import rpc
from nameko.standalone.rpc import ClusterRpcProxy
    
class Email(BaseModel):
    email:str
    
class EmailList(BaseModel):
    emails:List[str] = []
    
class Authen(BaseModel):
    email:str
    otp:str

app = FastAPI()

broker_cfg = {'AMQP_URI': "amqp://guest:guest@rabbitmq"}

@app.post("/getotp/")
def get_otp(email: Email):
    with ClusterRpcProxy(broker_cfg) as rpc:
        otp = rpc.otp.create(email.email)
        if otp != 0: 
           rpc.email_otp.send.call_async(otp, email.email)

    return {'results': str(otp)}
    
@app.post("/create_email_list/")
def create_email_list(email_list: EmailList):
    with ClusterRpcProxy(broker_cfg) as rpc:
        val = rpc.otp.create_email_list(email_list.emails)
  
    return {'results': val}
    
@app.post("/authen/")
def authen(authen: Authen):
    with ClusterRpcProxy(broker_cfg) as rpc:
        success = rpc.otp.authen(authen.email, authen.otp)
        if success == 1:
           rpc.otp.delete.call_async(authen.email)
           
    return {'results': success}
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps
  • ทดสอบ OTP Gateway Service ด้วย URL ด้านล่าง
http://labxx.cpsudevops.com:7002/docs
  • กด create_email_list เพื่อสร้างบัญชี Email ของผู้ผ่านการสอบสัมภาษณ์
  • กด Try it out เพิ่มรายชื่อ Email ด้วย JSON Format แล้วกด Execute
{
  "emails": [
    "nuttachot@hotmail.com", 
    "promrit_n@silpakorn.edu"
  ]
}
Reply Message แสดงผลการเพิ่มรายชื่อ Email ของผู้ผ่านการสอบสัมภาษณ์
  • กด getotp เพื่อขอ OTP โดยกด Try it out ใส่ Email ด้วย JSON Format แล้วกด Execute
{
  "email": "nuttachot@hotmail.com"
}
  • กด authen แล้วกด Try it out ใส่ OTP ด้วย JSON Format แล้วกด Execute
{
  "email": "nuttachot@hotmail.com",
  "otp": "668919"
}

API Authentication and Rate Limiting

  • Config Kong ผ่าน Konga จาก URL https://konga.labxx.cpsudevops.com สร้าง Create Email List, Get OTP และ Authen Service โดยกดที่เมนู SERVICES แล้วกด ADD NEW SERVICE
  • ใส่ชื่อ Service, Protocol, Host (Private IP Address บน Cloud Server), Port และ Path แล้วกด SUBMIT SERVICE
  • สร้าง Route สำหรับ Create Email List, Get OTP และ Authen Service โดยเลือก Service กดที่แถบ Routes แล้วกด ADD ROUTE
  • ทำ Rate Limiting, Basic Authen และ Key Authen โดยการกดที่เมนู ROUTE เลือกชื่อ Route แล้วกดแถบ Plugins
  • กด ADD PLUGIN เลือก ADD PLUGIN ที่ Basic Auth และ Key Auth
  • กด ADD PLUGIN
  • กด ADD PLUGIN เลือกเมนู Traffic Control แล้วกด ADD PLUGIN ที่ Rate Limiting
  • กำหนดให้ส่ง Request ได้ 5,000 ครั้ง ต่อชั่วโมง แล้วกด ADD PLUGIN
Plugins ของ createEmailList
Plugins ของ getOTP
Plugins ของ authen
  • Disabled key-auth เพื่อทดลองกดส่ง Request ใน Postman ไปยัง https://service.labxx.cpsudevops.com/create_email_list/ โดยให้ Kong ทำ Authen แบบ basic-auth
Reply Message แสดง Error เมื่อเพิ่ม Email ซ้ำ
X-RateLimit-Remaining-Hour จาก Reply Header
  • Enabled key-auth และ Disabled basic-auth เพื่อทดลองกดส่ง Request ใน Postman
Reply Message แสดง Error เมื่อเพิ่ม Email ซ้ำ
X-RateLimit-Remaining-Hour จาก Reply Header
  • ทดลองกดส่ง Request ใน Postman ไปยัง URL ของ API ที่เหลือ เพื่อให้ Kong ทำ Authen แบบ basic-auth และ key-auth รวมทั้งดู X-RateLimit-Remaining-Hour จาก Reply Header
https://service.labxx.cpsudevops.com/getotp/
https://service.labxx.cpsudevops.com/authen/
  • กลับมา Config Routes ทั้ง 4 Routes ให้ทำ Authen แบบ basic-auth

Session Server

การฝาก Session ไว้ที่ Redis Server นั้นทำให้ง่ายต่อการ Scale Out โดย Session ไม่หลุด ซึ่งผู้ใช้สามารถใช้งาน Web Application ได้อย่างต่อเนื่อง โดยเราจะติดตั้ง Redis Server ตามขั้นตอน ดังนี้

  • สร้าง Project ชื่อ session_server_dock ซึ่งภายใน Folder จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:    
  session_server:
    container_name: session_server
    image: "redis:alpine"
    restart: always
    
networks:
  default:
    external:
      name: microservice_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

Register UI with Flask

ภาพจาก https://en.wikipedia.org/wiki/Flask_(web_framework)

เราจะสร้างหน้า UI ทั้งหมด 3 หน้า ได้แก่ 1) หน้าขอ OPT 2) หน้ายืนยันตัวตน และ 3) หน้าลงทะเบียนนักศึกษาใหม่ โดยใช้ Flask ซึ่งเป็น Web Framework สำหรับ Python ตามขั้นตอนดังนี้

  • สร้าง Project ชื่อ register_ui_dock ซึ่งภายใน Folder จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ python/
   |__ Dockerfile
   |__ requirements.txt
   |__ ui.py
   |__ model.py
   |__ templates/
      |__ otp.html
      |__ authen.html
      |__ reg.html
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  register_ui:
    container_name: register_ui
    build: python/
    restart: always
    expose:
      - "80"
      
    environment:
      VIRTUAL_HOST: www.labxx.cpsudevops.com
      LETSENCRYPT_HOST: www.labxx.cpsudevops.com
      
    networks:
      - webproxy
      - default

networks:
  webproxy:
    external:
      name: webproxy
  default:
    external:
      name: microservice_network
  • แก้ไข Dockerfile ตามตัวอย่าง
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY ui.py .
COPY model.py .
COPY requirements.txt .
COPY templates templates
RUN pip install --no-cache-dir -r requirements.txt
CMD ["gunicorn", "--bind", ":80", "ui:app"]
  • แก้ไขไฟล์ requirements.txt
gunicorn
requests
Flask
Flask-Bootstrap
Flask-WTF
WTForms
redis
flask_session
email_validator
  • แก้ไขไฟล์ ui.py ซึ่งจะมีการฝาก Session ไว้บน Redis Server และการเรียก RESTful API ผ่าน requests.post() โดยทำ Authen แบบ Basic Authen
from flask import Flask, request, render_template, redirect, session
from model import OTPForm, AuthenForm, RegForm
from flask_bootstrap import Bootstrap
from requests.auth import HTTPBasicAuth
import requests
import json
import redis
from flask_session import Session

auth = HTTPBasicAuth('admin', 'devops101')

app = Flask(__name__)

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://session_server:6379')
sess = Session()
sess.init_app(app)

def getSession(key):
    return session.get(key, 'none')

def setSession(key, value):
    session[key] = value
    
def resetSession():
    session.clear()

app.config.from_mapping(
    SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
Bootstrap(app)

@app.route('/', methods=['GET', 'POST'])
def otp():
    form = OTPForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():

        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/getotp/'
        data = {'email': form.email.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')

        if result != '0':
            setSession('email', form.email.data)
            return redirect('https://www.lab20.cpsudevops.com/authen')
        else:
            return 'Email ของคุณไม่ถูกต้อง/คุณเคยลงทะเบียนแล้ว'

    return render_template('otp.html', form=form)

@app.route('/authen', methods=['GET', 'POST'])
def authen():
    form = AuthenForm(request.form)
    if request.method == 'POST' and form.validate_on_submit(): 
    
        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/authen/'
        data = {'email': getSession('email'), 'otp':  form.otp.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')
        
        if result == 1:
            setSession('authen', 'yes')
            return redirect('https://www.lab20.cpsudevops.com/reg')
        else:
            return 'กรุณาใส่ OTP/Email ใหม่'
   
    return render_template('authen.html', form=form)

@app.route('/reg', methods=['GET', 'POST'])
def registration():
    if getSession('authen') != 'yes':
        return 'คุณไม่ได้รับอนุญาตให้เข้าถึงหน้านี้'

    form = RegForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():
        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/register/'
        data = {'firstname': form.name_first.data, 'lastname': form.name_last.data, 'email': getSession('email')}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        resetSession()
        return 'ระบบจะแจ้งยืนยันการลงทะเบียนทาง Email'

    return render_template('reg.html', form=form)
  • แก้ไขไฟล์ model.py
from wtforms import SubmitField, BooleanField, StringField, PasswordField, validators
from flask_wtf import FlaskForm
    
class OTPForm(FlaskForm):
    email = StringField('Email Address', [validators.DataRequired(), validators.Email(), validators.Length(min=6, max=35)])
    submit = SubmitField('Submit')
    
class AuthenForm(FlaskForm):
    otp = StringField('OTP', [validators.DataRequired(), validators.Length(min=6, max=6)])
    submit = SubmitField('Submit')
    
class RegForm(FlaskForm):
    name_first = StringField('ชื่อ', [validators.DataRequired()])
    name_last = StringField('นามสกุล', [validators.DataRequired()])
    submit = SubmitField('Submit')    
  • แก้ไขไฟล์ otp.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่</h3>
  <hr>
  
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}
    
    <div class="form-group">
      {{ wtf.form_field(form.email, type='email', class='form-control', placeholder='Email Address') }}
    </div>

    <button type="submit" class="btn btn-primary">ขอ OTP</button>
    
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}
  • แก้ไขไฟล์ authen.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่ - ยืนยันตัวตน</h3>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}
    
    <div class="form-group">
      {{ wtf.form_field(form.otp, class='form-control', placeholder='OTP') }}
    </div>

    <button type="submit" class="btn btn-primary">ยืนยัน</button>
    
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}


  • แก้ไขไฟล์ reg.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่ - OTP</h3>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}

    <div class="row">
      <div class="form-group col-md-6">
        {{ wtf.form_field(form.name_first, class='form-control', placeholder='ชื่อ') }}
      </div>
      <div class="form-group col-md-6">
        {{ wtf.form_field(form.name_last, class='form-control', placeholder='นามสกุล') }}
      </div>
    </div>

    <button type="submit" class="btn btn-primary">ลงทะเบียน</button>
    
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}

  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

Try it out

  • ทดสอบการลงทะเบียนนักศึกษาใหม่ด้วย URL ด้านล่าง โดยใส่ Email address ของผู้ผ่านการสอบสัมภาษณ์ แล้วกด ขอ OTP
https://www.labxx.cpsudevops.com
  • สักครู่ระบบจะส่ง OTP ไปยัง Email ของผู้ใช้ นำ OTP มากรอกในแบบฟอร์ยืนยันตัวตน แล้วกด ยืนยัน
  • ใส่ ชื่อ นามสกุล ในฟอร์มลงทะเบียนนักศึกษาใหม่ แล้วกด ลงทะเบียน
  • ตรวจสอบข้อมูลนักศึกษาใหม่ใน Database จาก URL ด้านล่าง
https://mydb.labxx.cpsudevops.com
  • ตรวจสอบข้อมูลการลงทะเบียนของนักศึกษาใน Database จาก URL ด้านล่าง
https://mydb2.labxx.cpsudevops.com
  • ทดลองเข้าหน้าลงทะเบียนอีกครั้งจะพบข้อความ "คุณไม่ได้รับอนุญาตให้เข้าถึงหน้านี้"

หมายเหตุ เพื่อที่จะสามารถเพิ่ม ชื่อ นามสกุล ที่เป็นภาษาไทยลง Database ขอให้ผู้อ่านที่เคย Implement ตามในบนความก่อนหน้า กลับไปแก้ไขโครงสร้างของตาราง student และ enroll ให้เก็บข้อมูลแบบ utf8 (utf8_general_ci) จากบทความเรื่อง การพัฒนา Microservice บน Docker Container สำหรับผู้เริ่มต้น ครับ

ขอขอบคุณ Nipa.Cloud ที่ให้การสนับสนุน Environment ในการเรียนการสอน
รายวิชา Dev-Ops and Cloud Engineering 101