07 | 用户登录和注册
通过requests和云片网api发送短信
云片网
https://www.yunpian.com/admin/main
测试SDK
使用同步网络请求接口request发送
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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)
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), )
测试结果如下