Flask 用户模块

主要内容

  • 获取验证码
  • 注册登录
  • 获取当前用户信息
  • 查看前端交互

原型效果
在这里插入图片描述
接口设计

# 获取短信验证码
/app/sms/codes/<mobile>

# 请求方式 
GET

# 请求参数  路径参数
mobile  手机号

响应数据 json
{
  "message": "ok",
  "data": {
    "mobile": 135xxxxxxxx    
  }
}

接口实现

  • 在 app/resource/user/passport文件中实现获取验证码视图函数
# app/resources/user/passport.py

from flask_restful import Resource
import random
from app import redis_client
from utils.constants import SMS_CODE_EXPIRE


class SMSCodeResource(Resource):
    """获取短信验证码"""
    def get(self, mobile):
        # 生成短信验证码
        rand_num = '%06d' % random.randint(0, 999999)

        # 保存验证码(redis)  app:code:18912341234   123456
        key = 'app:code:{}'.format(mobile)
        redis_client.set(key, rand_num, ex=SMS_CODE_EXPIRE)

        # 发送短信  第三方短信平台 celery
        print('短信验证码: "mobile": {}, "code": {}'.format(mobile, rand_num))

        # 返回结果
        return {'mobile': mobile}
  • 注意: 短信验证码的过期时间需要定义为常量, 在 common/utils/constants.py文件中设置
# common/utils/constants.py

...

SMS_CODE_EXPIRE = 300  # 短信验证码有效期

配置URL

  • 在 user包的初始化文件中设置类视图, 给类视图的URL添加路径参数
# app/resources/user/__init__.py

...

user_api.add_resource(SMSCodeResource, '/sms/codes/<mobile>')
  • 在 common/utils/constants文件中定义常量记录URL前缀, 并在user包的初始化文件中给蓝图设置统一的资源段前缀
# common/utils/constants.py

...

BASE_URL_PRIFIX = '/app'  # 基础URL的前缀
# app/resources/user/__init__.py

...

from utils.constants import BASE_URL_PRIFIX

...

user_bp = Blueprint('user', __name__, url_prefix=BASE_URL_PRIFIX)

配置路由转化器

  • 在 common/utils包中导入物料converters.py, 其中包含了常见的路由转换器, 以便对路由变量进行参数校验
# common/utils/converters.py

from werkzeug.routing import BaseConverter


class MobileConverter(BaseConverter):
    """
    手机号格式
    """
    regex = r'1[3-9]\d{9}'


def register_converters(app):
    """
    向Flask app中添加转换器

    :param app: Flask app对象
    """
    app.url_map.converters['mob'] = MobileConverter

在 app包的初始化文件中注册路由转换器

# app/__init__.py

def register_extensions(app):
    """组件初始化"""

    ...

    # 添加转换器
    from utils.converters import register_converters
    register_converters(app)

在 user包的初始化文件中, 给类视图的路径参数添加转换器

# app/resources/user/__init__.py

...

user_api.add_resource(SMSCodeResource, '/sms/codes/<mob:mobile>')

注册登录

在这里插入图片描述

# 注册登录
/app/authorizations

# 请求方式 
POST

# 请求参数  json
mobile  手机号
code   短信验证码

响应数据 json
{
  "message": "ok",
  "data": {
    "token": "xxxxxxxx"
  }
}

模型设计

  • SQLAlchemy-用户模型类
    在 common包中创建 models包, 用于存放各类模型数据
    在 common/models包中添加物料 user.py文件, 其中包含了用户模型类
# common/models/user.py

from app import db


class User(db.Model):
    """
    用户基本信息
    """
    __tablename__ = 'user_basic'

    id = db.Column(db.Integer, primary_key=True, doc='用户ID')
    mobile = db.Column(db.String(11), doc='手机号')
    name = db.Column(db.String(20), doc='昵称')
    last_login = db.Column(db.DateTime, doc='最后登录时间')
    introduction = db.Column(db.String(50), doc='简介')
    article_count = db.Column(db.Integer, default=0, doc='作品数')
    following_count = db.Column(db.Integer, default=0, doc='关注的人数')
    fans_count = db.Column(db.Integer, default=0, doc='粉丝数')
    profile_photo = db.Column(db.String(130), doc='头像')

    def to_dict(self):
        """模型转字典, 用于序列化处理"""

        return {
            'id': self.id,
            'name': self.name,
            'photo': self.profile_photo,
            'intro': self.introduction,
            'art_count': self.article_count,
            'follow_count': self.following_count,
            'fans_count': self.fans_count
        }

数据迁移
在 app包的初始化文件的 register_extensions函数中, 对数据迁移组件进行初始化

# app/__init__.py 

...

from flask_migrate import Migrate

...


def register_extensions(app):
    """组件初始化"""

    ...

    # 数据迁移组件初始化
    Migrate(app, db)

    # 导入模型类
    from models import user

执行数据迁移命令

export FLASK_APP=app.main  # 设置环境变量指定启动文件
flask db init  # 生成迁移文件夹
flask db migrate  # ⽣成迁移版本, 保存到迁移文件夹中
flask db upgrade  # 执行迁移
  • 由于数据迁移代码是自动生成的, 为了避免代码合并时出现冲突, 一般不会让git管理迁移文件夹
  • 将迁移文件夹设置到gitignore文件中进行忽略处理
# .gitignore

*.py[cod]
.idea
migration

配置请求校验函数

  • 在 common/utils包中导入物料parser.py, 其中包含了常见的请求解析器使用的自定义校验函数, 以便对请求数据进行参数解析
# common/utils/parser.py


import re
import base64
import imghdr
from datetime import datetime


def email(email_str):
    """
    检验邮箱格式
    :param email_str: str 被检验字符串
    :return: email_str
    """
    if re.match(r'^([A-Za-z0-9_\-\.\u4e00-\u9fa5])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,8})$', email_str):
        return email_str
    else:
        raise ValueError('{} is not a valid email'.format(email_str))


def mobile(mobile_str):
    """
    检验手机号格式
    :param mobile_str: str 被检验字符串
    :return: mobile_str
    """
    if re.match(r'^1[3-9]\d{9}$', mobile_str):
        return mobile_str
    else:
        raise ValueError('{} is not a valid mobile'.format(mobile_str))


def id_number(value):
    """检查是否为身份证号"""
    id_number_pattern = r'(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)'
    if re.match(id_number_pattern, value):
        return value.upper()
    else:
        raise ValueError('Invalid id number.')

接口实现

  • 在 app/resource/user/passport文件中实现注册登录视图函数
# app/resources/user/passport.py

...

from datetime import datetime, timedelta
from flask import current_app
from flask_restful.inputs import regex
from flask_restful.reqparse import RequestParser
from sqlalchemy.orm import load_only
from app import db
from utils.parser import mobile as mobile_type
from models.user import User

...


class LoginResource(Resource):
    """注册登录"""
    def post(self):
        # 获取参数
        parser = RequestParser()
        parser.add_argument('mobile', required=True, location='json', type=mobile_type)
        parser.add_argument('code', required=True, location='json', type=regex(r'^\d{6}$'))
        args = parser.parse_args()
        mobile = args.mobile
        code = args.code

        # 校验短信验证码
        key = 'app:code:{}'.format(mobile)
        real_code = redis_client.get(key)

        if not real_code or real_code != code:
            return {'message': 'Invalid Code', 'data': None}, 400

        # 删除验证码
        # redis_client.delete(key)

        # 校验成功, 查询数据库
        user = User.query.options(load_only(User.id)).filter(User.mobile == mobile).first()

        if user:  # 如果有, 取出用户id, 更新最后登录时间
             user.last_login = datetime.now()

        else:  # 如果没有, 创建新用户
            user = User(mobile=mobile, name=mobile, last_login=datetime.now())
            db.session.add(user)

        db.session.commit()

        # 返回结果
        return {'userid': user.id}, 201

注意点

  • 使用 load_only等语法, 尽量只查询目标字段, 提高查询效率

配置URL
在 user包的初始化文件中设置类视图的URL

# app/resources/user/__init__.py

from .passport import SMSCodeResource, LoginResource

# 添加类视图
user_api.add_resource(LoginResource, '/authorizations')

状态保持

PyJWT

  • 注册登录后需要对用户信息进行状态保持, 可选方案包括: Cookie Session JWT
  • 相比HTTP自带的状态保持机制, JWT的优点
    移动端不支持状态保持机制
    cookie有同源策略, 默认无法跨站传输 (nginx可以转发)
  • python中普遍使用的jwt拓展包 pip install pyjwt
    生成JWT需要设置秘钥, 可以使用随机字符串
base64.b64encode(os.urandom(40)).decode()

在这里插入图片描述

配置JWT工具函数

  • 在 common/utils包中导入物料jwt_util.py, 其中包含了使用 PYJWT 封装的JWT的生成和校验函数
# common/utils/jwt_util.py

import jwt
from flask import current_app


def generate_jwt(payload, expiry, secret=None):
    """
    生成jwt
    :param payload: dict 载荷
    :param expiry: datetime 有效期
    :param secret: 密钥
    :return: jwt
    """
    _payload = {'exp': expiry}
    _payload.update(payload)

    if not secret:
        secret = current_app.config['JWT_SECRET']

    token = jwt.encode(_payload, secret, algorithm='HS256')
    return token.decode()


def verify_jwt(token, secret=None):
    """
    检验jwt
    :param token: jwt
    :param secret: 密钥
    :return: dict: payload
    """
    if not secret:
        secret = current_app.config['JWT_SECRET']

    try:
        payload = jwt.decode(token, secret, algorithm=['HS256'])
    except jwt.PyJWTError:
        payload = None

    return payload
  • 注意: JWT的秘钥和默认过期时间封装成了应用配置, 在 app/settings/config.py文件中设置
# app/settings/config.py

class DefaultConfig:
    """默认配置"""

    ...

    # JWT
    JWT_SECRET = 'TPmi4aLWRbyVq8zu9v82dWYW17/z+UvRnYTt4P6fAXA'  # 秘钥
    JWT_EXPIRE_DAYS = 14  # JWT过期时间14天

接口实现

  • 在 app/resource/user/passport文件的注册登录视图函数中生成jwt
# app/resources/user/passport.py

...

from utils.jwt_util import generate_jwt

...


class LoginResource(Resource):
    """注册登录"""
    def post(self):

        ...

        db.session.commit()

        # 生成jwt
        token = generate_jwt({'userid': user.id}, expiry=datetime.utcnow() + timedelta(days=current_app.config['JWT_EXPIRE_DAYS']))

        # 返回结果
        return {'token': token}, 201

获取用户信息

在这里插入图片描述

接口设计

# 获取当前用户信息
/app/user

# 请求方式 
GET

# 请求头  
Authorization   用户token

响应数据 json
{
    "message": "OK",
    "data": {
        "id": 1155,
        "name": "18912341234",
        "photo": "xxxxx",
        "intro": "xxx",
        "art_count": 0,
        "follow_count": 0,
        "fans_count": 0
    }
}

相关模型类

  • SQLAlchemy-用户模型类
# common/models/user.py

from app import db


class User(db.Model):
    """
    用户基本信息
    """
    __tablename__ = 'user_basic'

    id = db.Column(db.Integer, primary_key=True, doc='用户ID')
    mobile = db.Column(db.String(11), doc='手机号')
    name = db.Column(db.String(20), doc='昵称')
    last_login = db.Column(db.DateTime, doc='最后登录时间')
    introduction = db.Column(db.String(50), doc='简介')
    article_count = db.Column(db.Integer, default=0, doc='作品数')
    following_count = db.Column(db.Integer, default=0, doc='关注的人数')
    fans_count = db.Column(db.Integer, default=0, doc='粉丝数')
    profile_photo = db.Column(db.String(130), doc='头像')

    def to_dict(self):
        """模型转字典, 用于序列化处理"""

        return {
            'id': self.id,
            'name': self.name,
            'photo': self.profile_photo,
            'intro': self.introduction,
            'art_count': self.article_count,
            'follow_count': self.following_count,
            'fans_count': self.fans_count
        }

代码实现
实现权限控制

获取用户信息接口有访问权限要求: 用户登录才能访问, 所以需要实现权限控制, 需要实现以下两步:

定义钩子函数: 获取用户信息, 并使用g变量传递数据
定义装饰器: 根据用户信息进行访问限制

  • 在 common/utils包中新建middlewares.py, 其中定义钩子函数, 用于获取用户信息
# common/utils/middlewares.py

from flask import request, g
from utils.jwt_util import verify_jwt


def get_userinfo():
    """获取用户信息"""
    # 获取请求头中的token
    token = request.headers.get('Authorization')

    g.userid = None  # 如果未登录, userid=None

    if token:  # 如果传递了token
        # 校验token
        data = verify_jwt(token)

        if data:  # 校验成功
            g.userid = data.get('userid')  # 如果已登录, userid=11
  • 在 app包的初始化文件中注册钩子函数
# app/__init__.py

def register_extensions(app):
    """组件初始化"""

    ...

    # 添加请求钩子
    from utils.middlewares import get_userinfo
    app.before_request(get_userinfo)
  • 在 common/utils包中新建decorators.py, 其中定义装饰器, 用于访问限制
# common/utils/decorators.py


from flask import g
from functools import wraps


def login_required(f):

    @wraps(f)
    def wrapper(*args, **kwargs):
        # 如果用户已登录, 正常访问
        if g.userid:
            return f(*args, **kwargs)
        else:
            return {'message': 'Invalid Token', 'data': None}, 401

    return wrapper

接口实现

  • 在 app/resource/user包中新建 profile.py文件, 并在其中实现获取用户信息视图函数
# app/resources/user/profile.py

from flask import g
from flask_restful import Resource
from sqlalchemy.orm import load_only

from models.user import User
from utils.decorators import login_required


class CurrentUserResource(Resource):
    """个人中心-当前用户"""
    method_decorators = {'get': [login_required]}

    def get(self):
        # 获取用户id
        userid = g.userid

        # 查询用户数据
        user = User.query.options(load_only(User.id, User.name, User.profile_photo, User.introduction, User.article_count, User.following_count, User.fans_count)).filter(User.id == userid).first()

        return user.to_dict()

配置URL

  • 在 user包的初始化文件中设置类视图的URL
# app/resources/user/__init__.py

from .profile import CurrentUserResource

# 添加类视图
user_api.add_resource(CurrentUserResource, '/user')

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

季布,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值