Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Tornado-based User Authentication Flow with SMS, Registration, and JWT Login

Tech May 8 3

This guide walks through a complete user lifecycle—SMS verification, reigstration, and login—implemented in a non-blocking Tornado application. We will integrate Yunpian for text messages, Redis for transient codes, Peewee-async for database access, and PyJWT for stateless authentication.

  1. Sending SMS via Yunpian

1.1 Synchronous client (for scripts or tests)

import requests

class YunpianClient:
    def __init__(self, key: str):
        self.key = key

    def send_code(self, mobile: str, code: str) -> requests.Response:
        payload = {
            "apikey": self.key,
            "mobile": mobile,
            "text": f"【FreshMart】Your verification code is {code}. Ignore if not requested."
        }
        return requests.post(
            "https://sms.yunpian.com/v2/sms/single_send.json",
            data=payload
        )

if __name__ == "__main__":
    cli = YunpianClient("<your-key>")
    resp = cli.send_code("13800000000", "5729")
    print(resp.json())

1.2 Asynchronous client (for Tornado coroutines)

import json
from urllib.parse import urlencode
from tornado.httpclient import AsyncHTTPClient, HTTPRequest

class AsyncYunpianClient:
    def __init__(self, key: str):
        self.key = key

    async def send_code(self, mobile: str, code: str) -> dict:
        client = AsyncHTTPClient()
        body = urlencode({
            "apikey": self.key,
            "mobile": mobile,
            "text": f"Your verification code is {code}. Ignore if not requested."
        })
        req = HTTPRequest(
            url="https://sms.yunpian.com/v2/sms/single_send.json",
            method="POST",
            body=body
        )
        resp = await client.fetch(req)
        return json.loads(resp.body.decode())

  1. Tornado handler for SMS codes

2.1 Form validation with WTForms-Tornado

from wtforms_tornado import Form
from wtforms import StringField
from wtforms.validators import DataRequired, Regexp

PHONE_RE = r"^1[3-9]\d{9}$"

class SmsForm(Form):
    mobile = StringField(validators=[
        DataRequired(message="Mobile required"),
        Regexp(PHONE_RE, message="Invalid mobile format")
    ])

2.2 Redis-backed hendler

import json
import random
from tornado.web import RequestHandler
import redis

class BaseHandler(RequestHandler):
    def prepare(self):
        self.redis = redis.StrictRedis(**self.settings["redis"])

class SmsHandler(BaseHandler):
    def _random_code(self) -> str:
        return "".join(random.choices("0123456789", k=4))

    async def post(self):
        params = json.loads(self.request.body.decode())
        form = SmsForm.from_json(params)
        if not form.validate():
            self.set_status(400)
            self.finish(form.errors)
            return

        mobile = form.mobile.data
        code = self._random_code()
        yun = AsyncYunpianClient(self.settings["yunpian_key"])
        res = await yun.send_code(mobile, code)
        if res["code"] != 0:
            self.set_status(400)
            self.finish({"mobile": res["msg"]})
            return

        self.redis.setex(f"{mobile}_{code}", 600, 1)   # 10 min TTL
        self.finish({})

  1. Registration endpoint

3.1 User model with bcrypt passwords

from peewee import *
from bcrypt import hashpw, gensalt
from MxForm.models import BaseModel

class PasswordHash(bytes):
    def check(self, raw: str) -> bool:
        return hashpw(raw.encode(), self) == self

class PasswordField(BlobField):
    def db_value(self, value):
        if isinstance(value, PasswordHash):
            return bytes(value)
        salt = gensalt(12)
        return hashpw(value.encode(), salt)

    def python_value(self, value):
        return PasswordHash(value)

class User(BaseModel):
    mobile = CharField(max_length=11, unique=True)
    password = PasswordField()
    nickname = CharField(max_length=20, null=True)
    avatar = CharField(max_length=255, null=True)

3.2 Registraiton handler

class RegisterHandler(BaseHandler):
    async def post(self):
        params = json.loads(self.request.body.decode())
        form = RegisterForm.from_json(params)
        if not form.validate():
            self.set_status(400)
            self.finish(form.errors)
            return

        mobile = form.mobile.data
        code = form.code.data
        if not self.redis.exists(f"{mobile}_{code}"):
            self.set_status(400)
            self.finish({"code": "Invalid or expired code"})
            return

        try:
            await self.application.objects.get(User, mobile=mobile)
            self.set_status(400)
            self.finish({"mobile": "Already registered"})
            return
        except User.DoesNotExist:
            user = await self.application.objects.create(
                User, mobile=mobile, password=form.password.data
            )
            self.finish({"id": user.id})

  1. Login and JWT issuance

4.1 Login form

class LoginForm(Form):
    mobile = StringField(validators=[
        DataRequired(), Regexp(PHONE_RE)
    ])
    password = StringField(validators=[DataRequired()])

4.2 Login handler

import jwt
from datetime import datetime, timedelta

class LoginHandler(BaseHandler):
    async def post(self):
        params = json.loads(self.request.body.decode())
        form = LoginForm.from_json(params)
        if not form.validate():
            self.set_status(400)
            self.finish(form.errors)
            return

        try:
            user = await self.application.objects.get(User, mobile=form.mobile.data)
            if not user.password.check(form.password.data):
                raise ValueError
        except (User.DoesNotExist, ValueError):
            self.set_status(400)
            self.finish({"non_field_errors": "Wrong credentials"})
            return

        payload = {
            "uid": user.id,
            "nick": user.nickname or user.mobile,
            "exp": datetime.utcnow() + timedelta(days=7)
        }
        token = jwt.encode(payload, self.settings["jwt_secret"], algorithm="HS256")
        self.finish({
            "id": user.id,
            "nick": payload["nick"],
            "token": token.decode()
        })

  1. Routing and CORS

from tornado.web import url, Application
from tornado.ioloop import IOLoop
from MxForm.settings import settings

routes = [
    url(r"/code/", SmsHandler),
    url(r"/register/", RegisterHandler),
    url(r"/login/", LoginHandler),
]

app = Application(routes, **settings)
app.listen(8888)
IOLoop.current().start()

Enable CORS globally by subclassing RequestHandler and overriding set_default_headers to inject the appropriate Access-Control-* headers.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.