返回顶部

07 | 用户登录和注册

通过requests和云片网api发送短信

 云片网

https://www.yunpian.com/admin/main  

测试SDK

使用同步网络请求接口request发送

import requests

class YunPian:
    def __init__(self, api_key):
        self.api_key = api_key

    def send_single_sms(self,code, mobile):
        #发送单条短信
        url = "https://sms.yunpian.com/v2/sms/single_send.json"
        text = "【慕学生鲜】您的验证码是{}。如非本人操作,请忽略本短信".format(code)
        res = requests.post(url, data={
            "apikey":self.api_key,
            "mobile":mobile,
            "text":text
        })

        return res

if __name__ == "__main__":
    yun_pian = YunPian("写入自己的")
    res = yun_pian.send_single_sms("1234", "手机号")
    print(res.text)
apps/utils/YunPian.py

AsyncHttpClient异步发送短信

 因为tornado程序是单线程切换的,所有不能使用同步的网络请求request,因为这样会造成其它协程的阻塞,所以要使用异步的网络请求库,这里使用的是tornado自带的httcilent

apps/utils/AsyncYunPian.py

import json
from urllib.parse import urlencode

from tornado import httpclient
from tornado.httpclient import HTTPRequest

class AsyncYunPian:
    def __init__(self, api_key):
        self.api_key = api_key

    async def send_single_sms(self,code, mobile):
        http_client = httpclient.AsyncHTTPClient()
        url = "https://sms.yunpian.com/v2/sms/single_send.json"
        text = "您的验证码是{}。如非本人操作,请忽略本短信".format(code)
        post_request = HTTPRequest(url=url, method="POST", body=urlencode({
            "apikey": self.api_key,
            "mobile": mobile,
            "text": text
        }))
        res = await http_client.fetch(post_request)
        return json.loads(res.body.decode("utf8"))

if __name__ == "__main__":
    import tornado
    io_loop = tornado.ioloop.IOLoop.current()

    yun_pian = AsyncYunPian("自己的")

    #run_sync方法可以在运行完某个协程之后停止事件循环
    from functools import partial
    new_func = partial(yun_pian.send_single_sms, "1234", "手机号")
    io_loop.run_sync(new_func)

  因为Fetch方法只能接受单纯的url字符串或者request对象,如果是单纯的url没办法post,并且参数不好传,

  所以需要构建一个request对象,tornado文档里提供了这样一个类,tornado.httpclient.HttpRequest

  需要注意的是,body是参数,但是它只能接受unicode,不能接受字典,所以需要用到urllib.parse里面的urlencode方法

  另外,如果不想把所有参数都放到init函数里,可以用偏函数的方式把参数放到协程函数里

tornado集成异步短信发送接口

使用wtfrom做参数校验

users/forms.py

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

MOBILE_REGEX = "^1[358]\d{9}$|^1[48]7\d{8}$|^176\d{8}$"


class SmsCodeForm(Form):
    mobile = StringField("手机号码",
                         validators=[DataRequired(message="请输入手机号码"), Regexp(MOBILE_REGEX, message="请输入合法的手机号码")])

这里就有两个坑

  1.通过request.post方法直接传过来的数据,无法通过self.get_arguments()方法获得,需要用self.reqeust.body取到

  2.tornado和django这种web框架,都对传过来的数据进行过处理,处理后的数据像这样{‘a’:['b']},但是我们自己接收的数据是原始的字典格式,而验证方法会循环取值,同时字符串又是可迭代类型,所以它只会取到电话号码的第一位。那么如何处理呢,需要用到一个第三方包来接受json类型的数据

安装

pip install wtforms_json

github 地址

https://github.com/kvesteri/wtforms-json

官方文档

https://wtforms-json.readthedocs.io/en/latest/#what-does-it-do

 

只需在服务器启动之前加上以下代码,照常使用wtforn中的功能,只需在接受json数据的进行实例化的时候写为form = SmsForm.from_json(param)即可

import wtforms_json
 
 
wtforms_json.init()

 

设置全局配置

MxForm/settings.py

import os

import peewee_async

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
settings = {
    "static_path": "C:/projects/tornado_overview/chapter03/static",
    "static_url_prefix": "/static/",
    "template_path": "templates",
    "secret_key":"ZGGA#Mp4yL4w5CDu",
    "jwt_expire":7*24*3600,
    "MEDIA_ROOT": os.path.join(BASE_DIR, "media"),
    "SITE_URL":"http://127.0.0.1:8888",
    "db": {
        "host": "127.0.0.1",
        "user": "root",
        "password": "mysql",
        "name": "message",
        "port": 3306
    },
    "redis":{
        "host":"127.0.0.1"
    }
}

 

 封装全局redis的handler 提供连接操控redis的功能

MxForm/handler.py

from tornado.web import RequestHandler
import redis

class RedisHandler(RequestHandler):
    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)
        self.redis_conn = redis.StrictRedis(**self.settings["redis"])

处理验证码请求的handler

 apps/users/handler.py

import json
from functools import partial
from random import choice

from tornado.web import RequestHandler

from MxForm.handler import RedisHandler
from apps.users.forms import SmsCodeForm
from apps.utils.AsyncYunPian import AsyncYunPian


class SmsHandler(RedisHandler):
    def generate_code(self):
        """
        生成随机4位数字的验证码
        :return:
        """
        seeds = "1234567890"
        random_str = []
        for i in range(4):
            random_str.append(choice(seeds))
        return "".join(random_str)

    async def post(self, *args, **kwargs):
        re_data = {}

        param = self.request.body.decode("utf-8")
        param = json.loads(param)
        sms_form = SmsCodeForm.from_json(param)
        if sms_form.validate():
            mobile = sms_form.mobile.data
            code = self.generate_code()
            yun_pian = AsyncYunPian("d6c4ddbf50ab36611d2f52041a0b949e")

            re_json = await yun_pian.send_single_sms(code, mobile)
            if re_json["code"] != 0:
                self.set_status(400)
                re_data["mobile"] = re_json["msg"]
            else:
                #将验证码写入到redis中
                self.redis_conn.set("{}_{}".format(mobile,code), 1, 10*60)
        else:
            self.set_status(400)
            for field in sms_form.errors:
                re_data[field] = sms_form.errors[field][0]

        self.finish(re_data)

这一部分主要是错误信息的提取,一个逻辑是取短信发送后返回的消息里的msg

还一个是表单里如果验证失败获取errors属性,是一个字典,可以直接取。

至此,发送短信的接口就算完成了。

配置路由 apps/users/urls.py

from tornado.web import url
from apps.users.handler import SmsHandler

urlpattern = (
    url("/code/", SmsHandler),
) 

启动程序 server.py

from tornado import web
import tornadofrom MxForm.urls import urlpattern
from MxForm.settings import settings, database

if __name__ == "__main__":

    #集成json到wtforms
    import wtforms_json
    wtforms_json.init()

    app = web.Application(urlpattern, debug=True, **settings)
    app.listen(8888)

    tornado.ioloop.IOLoop.current().start()

调试 发送验证码接口 

浏览器防止ssr攻击,出现了跨域的问题

为了解决这种情况可以写一个全局的BaseHandler处理它

MxForm/handler.py

from tornado.web import RequestHandler
import redis

class BaseHandler(RequestHandler):

    def set_default_headers(self):
        self.set_header('Access-Control-Allow-Origin', '*')
        self.set_header('Access-Control-Allow-Headers', '*')
        self.set_header('Access-Control-Max-Age', 1000)
        self.set_header('Content-type', 'application/json')
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, DELETE, PUT, PATCH, OPTIONS')
        self.set_header('Access-Control-Allow-Headers',
                        'Content-Type, tsessionid, Access-Control-Allow-Origin, Access-Control-Allow-Headers, X-Requested-By, Access-Control-Allow-Methods')

    def options(self, *args, **kwargs):
        pass

class RedisHandler(RequestHandler):
    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)
        self.redis_conn = redis.StrictRedis(**self.settings["redis"])

 

再次请求可以看到测试成功

 

手机号码注册功能开发

server.py

from tornado import web
import tornado

from MxForm.urls import urlpattern
from MxForm.settings import settings, database
from peewee_async import Manager

if __name__ == "__main__":

    #集成json到wtforms
    import wtforms_json
    wtforms_json.init()

    app = web.Application(urlpattern, debug=True, **settings)
    app.listen(8888)

    objects = Manager(database)
    database.set_allow_sync(False)
    app.objects = objects

    tornado.ioloop.IOLoop.current().start()

 

使用异步用peewee_async来代替了peewee,根据官方文档只需在程序启动的时候,指定即可,最后把objects变量赋值给app.objects这样在所有的handler中,我们就可以直接使用self.application.objects进行异步的操作数据库。

创建全局的BaseModel,供model子类继承

MxForm/models.py  使用peewee_async 对数据库异步操作

from datetime import datetime

from peewee import *
import peewee_async

database = peewee_async.MySQLDatabase(
    'mxforum', host="127.0.0.1", port=3306, user="root", password="mysql"
)

class BaseModel(Model):
    add_time = DateTimeField(default=datetime.now, verbose_name="添加时间")
    class Meta:
        database = database

 

创建用户表

peewee在早期的版本中,提供了对密码进行加密的功能,在最新的版本中给取消掉了,所以现在我们要使用的话可以去git上搜它的源码,把历史版本的加密功能拿过来使用

https://github.com/coleifer/peewee/commit/04979500366d4b8b31694eb629d7ea28f05c58c4#diff-56fcfb44764a22a2dcd8613a4d6e50a6

peewee以前版本对密码处理的源码如下

from bcrypt import hashpw, gensalt 

class PasswordHash(bytes): def check_password(self, password): password = password.encode('utf-8') return hashpw(password, self) == self class PasswordField(BlobField): def __init__(self, iterations=12, *args, **kwargs): if None in (hashpw, gensalt): raise ValueError('Missing library required for PasswordField: bcrypt') self.bcrypt_iterations = iterations self.raw_password = None super(PasswordField, self).__init__(*args, **kwargs) def db_value(self, value): """Convert the python value for storage in the database.""" if isinstance(value, PasswordHash): return bytes(value) if isinstance(value, unicode_type): value = value.encode('utf-8') salt = gensalt(self.bcrypt_iterations) return value if value is None else hashpw(value, salt) def python_value(self, value): """Convert the database value to a pythonic value.""" if isinstance(value, unicode_type): value = value.encode('utf-8') return PasswordHash(value)

继承到我们的用户表中

安装依赖

pip install bcrypt

 apps/users/models.py

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


class PasswordHash(bytes):
    def check_password(self, password):
        password = password.encode('utf-8')
        return hashpw(password, self) == self


class PasswordField(BlobField):
    def __init__(self, iterations=12, *args, **kwargs):
        if None in (hashpw, gensalt):
            raise ValueError('Missing library required for PasswordField: bcrypt')
        self.bcrypt_iterations = iterations
        self.raw_password = None
        super(PasswordField, self).__init__(*args, **kwargs)

    def db_value(self, value):
        """Convert the python value for storage in the database."""
        if isinstance(value, PasswordHash):
            return bytes(value)

        if isinstance(value, str):
            value = value.encode('utf-8')
        salt = gensalt(self.bcrypt_iterations)
        return value if value is None else hashpw(value, salt)

    def python_value(self, value):
        """Convert the database value to a pythonic value."""
        if isinstance(value, str):
            value = value.encode('utf-8')

        return PasswordHash(value)

GENDERS = (
    ("female", ""),
    ("male", "")
)
class User(BaseModel):
    mobile = CharField(max_length=11, verbose_name="手机号码", index=True, unique=True)
    password = PasswordField(verbose_name="密码") #1. 密文,2.不可反解
    nick_name = CharField(max_length=20, null=True, verbose_name="昵称")
    head_url = CharField(max_length=200, null=True, verbose_name="头像")
    address = CharField(max_length=200, null=True, verbose_name="地址")
    desc = TextField(null=True, verbose_name="个人简介")
    gender = CharField(max_length=200, choices=GENDERS, null=True, verbose_name="地址")

 

 tools/init_db.py 创建创建生成数据表的脚本

from peewee import MySQLDatabase

from apps.users.models import User

from MxForm.settings import database
database = MySQLDatabase(
    'mxforum', host="127.0.0.1", port=3306, user="root", password="mysql"
)

def init():
    #生成表
    database.create_tables([User])

if __name__ == "__main__":
    init()

运行该脚本生成User表

apps/users/forms.py  注册表单验证

class RegisterForm(Form):
    mobile = StringField("手机号码",
                         validators=[DataRequired(message="请输入手机号码"), Regexp(MOBILE_REGEX, message="请输入合法的手机号码")])
    code = StringField("验证码", validators=[DataRequired(message="请输入验证码")])
    password = StringField("密码", validators=[DataRequired(message="请输入密码")])

 

apps/users/handler.py  用户注册处理器 

from .models import User
from .forms import RegisterForm

class RegisterHandler(RedisHandler):
    async def post(self, *args, **kwargs):
        re_data = {}

        param = self.request.body.decode("utf-8")
        param = json.loads(param)
        register_form = RegisterForm.from_json(param)
        if register_form.validate():
            mobile = register_form.mobile.data
            code = register_form.code.data
            password = register_form.password.data

            #验证码是否正确
            redis_key = "{}_{}".format(mobile, code)
            if not self.redis_conn.get(redis_key):
                self.set_status(400)
                re_data["code"] = "验证码错误或者失效"
            else:
                #验证用户是否存在
                try:
                    existed_users = await self.application.objects.get(User, mobile=mobile)
                    self.set_status(400)
                    re_data["mobile"] = "用户已经存在"
                except User.DoesNotExist as e:
                    user = await self.application.objects.create(User, mobile=mobile, password=password)
                    re_data["id"] = user.id
        else:
            self.set_status(400)
            for field in register_form.erros:
                re_data[field] = register_form[field][0]

        self.finish(re_data)

 

apps/users/urls.py  用户注册url

from tornado.web import url
from apps.users.handler import SmsHandler,RegisterHandler

urlpattern = (
    url("/code/", SmsHandler),
    url("/register/", RegisterHandler),
)

前后端联调

查看数据库可以看到该记录

用户登录

登陆成功返回token

pip install PyJWT  

 

apps/users/forms.py 登陆参数参数校验

class LoginForm(Form):
    mobile = StringField("手机号码",
                         validators=[DataRequired(message="请输入手机号码"), Regexp(MOBILE_REGEX, message="请输入合法的手机号码")])
    password = StringField("密码", validators=[DataRequired(message="请输入密码")])

 

apps/users/handler.py  登陆handler

import jwt
from .forms import LoginForm
from datetime import datetime

class LoginHandler(RedisHandler):
    async def post(self, *args, **kwargs):
        re_data = {}

        param = self.request.body.decode("utf-8")
        param = json.loads(param)
        form = LoginForm.from_json(param)

        if form.validate():
            mobile = form.mobile.data
            password = form.password.data

            try:
                user = await self.application.objects.get(User, mobile=mobile)
                if not user.password.check_password(password):
                    self.set_status(400)
                    re_data["non_fields"] = "用户名或密码错误"
                else:
                    #登录成功
                    #1. 是不是rest api只能使用jwt
                    # session实际上是服务器随机生成的一段字符串, 保存在服务器的
                    # jwt 本质上还是加密技术,userid, user.name

                    #生成json web token
                    payload = {
                        "id":user.id,
                        "nick_name":user.nick_name,
                        "exp":datetime.utcnow()
                    }
                    token = jwt.encode(payload, self.settings["secret_key"], algorithm='HS256')
                    re_data["id"] = user.id
                    if user.nick_name is not None:
                        re_data["nick_name"] = user.nick_name
                    else:
                        re_data["nick_name"] = user.mobile
                    re_data["token"] = token.decode("utf8")

            except User.DoesNotExist as e:
                self.set_status(400)
                re_data["mobile"] = "用户不存在"

            self.finish(re_data)

 

apps/users/urls.py

from tornado.web import url
from apps.users.handler import SmsHandler, RegisterHandler, LoginHandler

urlpattern = (
    url("/code/", SmsHandler),
    url("/register/", RegisterHandler),
    url("/login/", LoginHandler),
)

 

测试结果如下

 

posted @ 2018-12-28 00:27  Crazymagic  阅读(848)  评论(0编辑  收藏  举报