Tornado Practice: Implementing User Login and Registration Interfaces Based on Peewee, Marshmallow, and Aioredis

1. First look at the required directory structure

auth_demo/
├── apps
│   ├── __init__.py
│   └── public
│   	   ├── handler.py
│   	   ├── __init__.py
│   	   ├── models.py
│   	   ├── schemas.py
│   	   ├── tests.py
│   	   └── urls.py
├── base
│   ├── handler.py
│   ├── __init__.py
│   ├── models.py
│   ├── schema.py
│   ├── settings.py
│   └── urls.py
├── __init__.py
├── logs
│   └── web.log
├── manage.py
├── requirements.txt
└── utils
	├── __init__.py
    ├── db_manage.py
    ├── decorators.py
    ├── logger.py
    └── utils.py

Directory and file description:

base is a directory used to store some basic classes that the project needs to use.

​ handler.py is the basic request processing class file used by the project

​ models.py is used to store basic model classes

​ schema.py is used to store basic serializer classes

​ settings.py is used to store configuration files

​ urls.py is used to collect all routing configurations and assemble the entire project for manage.

apps is the directory used to store the actual app, and it is recommended to store in different apps corresponding to different applications.

​ public The actual app of this project

​ handler.py is the request processing class file used by the corresponding app

​ models.py is the model class file used by the corresponding app

​ schema.py is the serializer class file used by the corresponding app

​ urls.py is the routing file used by the corresponding app

logs is the directory used to store project running logs.

utils is a directory used to store some plugins and extensions.

​ db_manage.py is the processing file for database migration

​ decorators.py is a processing decorator file for user login verification

​ utils.py is some plugin package files used by the project

​ logger.py is the log output processing file used by the project

requirements.txt is a project dependency file, which is used when installing the package pip install -r requirements.txt.

__init__.pyThe directory in which the file exists, which in Python is a package (or module).

manage.py is the entry file

Due to the reference to the project structure of Django, running the manage.py file is also the way to run the demo. The running command set will python manage.py runserverstart the service from the local port 8080 by default.

2. Let me first introduce what functions are provided by each major package

First look at the dependencies in the requirements.txt file as follows:

tornado==6.0.4
aiomysql==0.0.20
peewee==3.13.3
peewee-async==0.7.0
marshmallow==3.6.0
aioredis==1.3.1
redis==3.5.3
aiofiles==0.5.0
Pillow==7.2.0
swagger-py-codegen==0.4.0
swagger-spec-validator==2.4.3
urllib3==1.24.2
six==1.12.0
requests==2.21.0
requests-toolbelt==0.9.1
pyotp==2.3.0
chardet==3.0.4
crypto==1.4.1
aiohttp==3.6.2

The packages that are highlighted are: peewee, peewee-async, marshmallow, aioredis, etc.

peewee: It provides a lightweight ORM framework for the project, supporting MySQL, SQLite, PostgreSQL and other main relational databases.

peewee-async: An asynchronous extension package belonging to peewee, which supports making SQL operations asynchronous.

marshmallow: It provides a serialization and deserialization package for the project, which is very simple and easy to use.

aioredis: Provide an asynchronous redis driver for the project, which is used to connect and operate redis asynchronously.

3. Source code

Since there are too many actual codes, the source code of this example is put on Code Cloud for your reference and use.

The source code address of the project is: https://gitee.com/aeasringnar/auth_demo.git

Explanation of source code structure and relevance:

manage.py is the entry file, and provides some database operation commands, use it python manage helpto get help. Use is python manage.py migrateused to migrate the database and python manage.py updateupdate the database content. The command to migrate the database is provided by db_mange.py in the uitls directory. The main command python manage.py runserverwill run the instantiated tornado app to provide services.

The code of the actual app in the apps directory is based on the basic classes in the base directory, and then the app will be integrated with the urls in the base through urls, and finally provided to the tornado instantiated app in manage.py to receive and configure tornado app. Finally, the app will be bound to the httpserver instance provided by tornado, and the contents of the settings configuration file under the base directory will also be loaded to run the entire service. At this point, the entire project is basically coupled, and there are some small plug-ins, such as logger, permission decorator, asynchronous redis driver, etc., which are loaded and used in actual use.

The models code in the public directory describes the basic model and user model

from base.models import BaseModel
from peewee import *
from bcrypt import hashpw, gensalt
from base.settings import settings


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):
        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):
        if isinstance(value, str):
            value = value.encode('utf-8')

        return PasswordHash(value)


class Group(BaseModel):
    '''用户组表模型'''
    group_type_choices = (
        ('SuperAdmin', '超级管理员'),
        ('Admin', '管理员'),
        ('NormalUser', '普通用户'),
    )
    group_type = CharField(max_length=128, choices=group_type_choices, verbose_name='用户组类型')
    group_type_cn = CharField(max_length=128, verbose_name='用户组类型_cn')

    class Meta:
        table_name = 'Group'

        
class User(BaseModel):
    '''用户表模型'''
    username = CharField(max_length=32, default='', verbose_name='用户账号')
    # password = CharField(max_length=255, default='',verbose_name='用户密码')
    password = PasswordField(default='123456', verbose_name="密码")
    mobile = CharField(max_length=12, default='', verbose_name='用户手机号')
    email = CharField(default='', verbose_name='用户邮箱')
    real_name = CharField(max_length=16, default='', verbose_name='真实姓名')
    id_num = CharField(max_length=18, default='', verbose_name='身份证号')
    nick_name = CharField(max_length=32, default='', verbose_name='昵称')
    region = CharField(max_length=255, default='', verbose_name='地区')
    avatar_url = CharField(max_length=255, default='', verbose_name='头像')
    open_id = CharField(max_length=255, default='', verbose_name='微信openid') 
    union_id = CharField(max_length=255, default='', verbose_name='微信unionid')
    gender = IntegerField(choices=((0, '未知'), (1, '男'), (2, '女')), default=0, verbose_name='性别')
    birth_date = DateField(verbose_name='生日', null=True)
    is_freeze = IntegerField(default=0, choices=((0, '否'),(1, '是')),  verbose_name='是否冻结/是否封号')
    # is_admin = BooleanField(default=False, verbose_name='是否管理员')
    group = ForeignKeyField(Group, on_delete='RESTRICT', verbose_name='用户组')
    # 组权分离后 当有权限时必定为管理员类型用户,否则为普通用户
    bf_logo_time = DateTimeField(null=True, verbose_name='上次登录时间')

    class Meta:
        db_table = 'User'

urls code in the public directory

from tornado.web import url
from .handler import UploadFileHandler, GetMobielCodeHandler, TestHandler, MobileLoginHandler,  UserInfoHandler

urlpatterns = [
    url('/public/test/', TestHandler), # 实际挂在的路径,访问时,当名字路径时,tornado会处理并返回response
    url('/public/uploadfile/', UploadFileHandler),
    url('/public/getcode/', GetMobielCodeHandler),
    url('/public/mobilelogin/', MobileLoginHandler), # 手机号验证码登录接口,不存在手机号时创建新用户
    url('/public/userinfo/', UserInfoHandler),
]

Part of the code of the handler in the public directory

...
from base.handler import BaseHandler
from .models import *
from .schemas import *
...

class TestHandler(BaseHandler): # urls 文件中指定的 handler 处理类,继承的类来自与 base 目录下的 BaseHandler
    '''
    测试接口
    get -> /public/test/
    '''
    async def get(self, *args, **kwargs):
        res_format = {
    
    "message": "ok", "errorCode": 0, "data": {
    
    }}
        try:
            res_format['message'] = 'Hello World'
            return self.finish(res_format)
        except Exception as e:
            logger.error('出现异常:%s' % str(e))
            return self.finish({
    
    "message": "出现无法预料的异常:{}".format(str(e)), "errorCode": 1, "data": {
    
    }})
        

class MobileLoginHandler(BaseHandler): #  urls 文件中指定的 MobileLoginHandler 处理类
    '''
    手机号登录
    POST -> /mobilelogin/
    payload:
        {
            "mobile": "手机号",
            "code": "验证码"
        }
    '''
    @validated_input_type()
    async def post(self, *args, **kwargs):
        res_format = {
    
    "message": "ok", "errorCode": 0, "data": {
    
    }}
        try:
            data = self.request.body.decode('utf-8') if self.request.body else "{}"
            validataed = MobielLoginSchema().load(json.loads(data))
            mobile = validataed['mobile']
            code = validataed['code']
            redis_pool = await aioredis.create_redis_pool('redis://127.0.0.1/0')
            value = await redis_pool.get(mobile, encoding='utf-8')
            if not value:
                return self.finish({
    
    "message": "验证码不存在,请重新发生验证码。", "errorCode": 2, "data": {
    
    }})
            if value != code:
                return self.finish({
    
    "message": "验证码错误,请核对后重试。", "errorCode": 2, "data": {
    
    }})
            redis_pool.close()
            await redis_pool.wait_closed()
            query = User.select().where(User.mobile == mobile)
            user = await self.application.objects.execute(query)
            if not user:
                # 创建用户
                user = await self.application.objects.create(
                    User,
                    username = mobile,
                    mobile = mobile,
                    group_id = 3
                )
            else:
                user = user[0]
            payload = {
    
    
                'id': user.id,
                'username': user.username,
                'exp': datetime.utcnow()
            }
            token = jwt.encode(payload, self.settings["secret_key"], algorithm='HS256')
            res_format['data']['token'] = token.decode('utf-8')
            return self.finish(res_format)
        except ValidationError as err:
            return self.finish({
    
    "message": str(err.messages), "errorCode": 2, "data": {
    
    }})
        except Exception as e:
            logger.error('出现异常:%s' % str(e))
            return self.finish({
    
    "message": "出现无法预料的异常:{}".format(str(e)), "errorCode": 1, "data": {
    
    }})
...

urls code in base

from tornado.web import url
from tornado.web import StaticFileHandler
from base.settings import settings
from apps.public import urls as public_urls # 将实际 app 内的 urls 导入并整合
from .handler import OtherErrorHandler

urlpatterns = [
    (url("/media/(.*)", StaticFileHandler, {
    
    "path": settings["media_path"]}))
]


urlpatterns += public_urls.urlpatterns
urlpatterns.append(url(".*", OtherErrorHandler))

Part of the code that the service runs in manage

........
from base.urls import urlpatterns # 将 base 目录下的所有路由导入
......
    elif sys.argv[1] == 'runserver':
            if len(sys.argv) != 3:
                sys.argv.append('8080') # 设置默认的端口
            if ':' in sys.argv[2]:
                host, port = sys.argv[2].split(':')
            else:
                port = sys.argv[2]
                host = '127.0.0.1' # 设置默认的监听地址
            app = web.Application(
                urlpatterns, # 将路由配置到实例化后的 tornado web服务上
                **settings
            )
            async_db.set_allow_sync(False)
            app.objects = Manager(async_db) #设置用于操作数据的管理器
            loop = asyncio.get_event_loop()
            # app.redis = RedisPool(loop=loop).get_conn()
            app.redis = loop.run_until_complete(redis_pool(loop))
            logger.info("""[%s]Wellcome...
Starting development server at http://%s:%s/       
Quit the server with CTRL+C.""" % (('debug' if settings['debug'] else 'line'), host, port))
            server = HTTPServer(app)
            server.listen(int(port),host)
            if not settings['debug']:
                # 多进程 运行
                server.start(cpu_count() - 1)
            ioloop.IOLoop.current().start() # 运行服务
................

Important login authentication decorator code

from functools import wraps
import jwt
from apps.users.models import User, Auth, AuthPermission
from .logger import logger


def authenticated_async(verify_is_admin=False):
    ''''
    JWT认证装饰器
    '''
    def decorator(func):
        @wraps(func)
        async def wrapper(self, *args, **kwargs):
            try:
                Authorization = self.request.headers.get('Authorization', None)
                if not Authorization:
                    self.set_status(401)
                    return self.finish({
    
    "message": "身份认证信息未提供。", "errorCode": 2, "data": {
    
    }})
                auth_type, auth_token = Authorization.split(' ')
                data = jwt.decode(
                    auth_token,
                    self.settings['secret_key'],
                    leeway=self.settings['jwt_expire'],
                    options={
    
    "verify_exp": True}
                )
                user_id = data.get('id')
                user = await self.application.objects.get(
                    User,
                    id=user_id
                )
                if not user:
                    self.set_status(401)
                    return self.finish({
    
    "message": "用户不存在", "errorCode": 1, "data": {
    
    }})
                self._current_user = user
                await func(self, *args, **kwargs)
            except jwt.exceptions.ExpiredSignatureError as e:
                self.set_status(401)
                return self.finish({
    
    "message": "Token过期", "errorCode": 1, "data": {
    
    }})
            except jwt.exceptions.DecodeError as e:
                self.set_status(401)
                return self.finish({
    
    "message": "Token不合法", "errorCode": 1, "data": {
    
    }})
            except Exception as e:
                self.set_status(401)
                logger.error('出现异常:{}'.format(str(e)))
                return self.finish({
    
    "message": "Token异常", "errorCode": 2, "data": {
    
    }})
        return wrapper
    return decorator

4. Project operation logic and testing

This example is recommended to run in a virtual environment. The recommended development environment is Python>=3.6, MySQL5.7, Redis5.x, and the development system is recommended to use Linux.

Running process:

Step 1: After entering the project directory, install and use the virtual environment.

Step 2: Enter the virtual environment and install dependent files pip install -r requirements.txt.

Step 3: Run the project python manage.py runserver.

Step 4: Use curl or postman to test.

After the project runs successfully, the terminal will print the log in real time in debug mode, and the terminal will output the following information:

# python manage.py runserver
2020-08-09 16:11:14,719 - selector_events.py[line:54] - DEBUG - Using selector: EpollSelector
2020-08-09 16:11:14,720 - connection.py[line:110] - DEBUG - Creating tcp connection to ('localhost', 6379)
2020-08-09 16:11:14,721 - manage.py[line:55] - INFO - [debug]Wellcome...
Starting development server at http://127.0.0.1:8080/       
Quit the server with CTRL+C.

Some interface test diagrams
test interface
insert image description here
insert image description here
insert image description here

Guess you like

Origin blog.csdn.net/haeasringnar/article/details/108330618