Flask 构建自定义RETful API

一、目录结构

Flask 构建自定义RETful API 目录结构

1.1、项目入口文件

如何开始一个项目?

  1. 创建启动文件。
  2. 实例化 flask ,app = Flask(__name__)
  3. 定义视图函数,@app.route(’’,methods=[‘GET’])
# ginger.py
# _*_ coding:utf-8 _*_
from werkzeug.exceptions import HTTPException
from app import create_app
from app.libs.error import APIException
from app.libs.error_code import ServerError

app = create_app()

"""
	解决未知异常类型的方法:未知异常都是由框架返回的错误,是我们意识不到得,所以不确定。
    通过flask 提供的 errorhandler 捕获所有的异常,必须在入口文件才能捕获到所有错误类型。
    Exception 表示python异常基类
"""

@app.errorhandler(Exception)
def framework_error(e):
    """
        e表示所有异常类型:
            APIException
            HTTPException
            Exception
    """
    # 判断 e 的类型是不是 APIEception
    if isinstance(e, APIException):
        return e
    if isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return APIException(msg, code, error_code)
    else:
        # 这里可以记录集体错误信息,并记录到日志中
        # 基类异常不需要返回具体的异常,可以通过定义一个统一的异常类型返回就可以了。
        # 判断 debug 来处理异常返回的格式,如果是调试模式,直接返回错误信息,不是调试模式,返回自定义错误信息。
        if not app.config['DEBUG']:
            return ServerError()
        else:
            raise e

if __name__ == "__main__":
    app.run()

1.2、创建数据库表、创建Flask app项目、把蓝图注册到Flask中

app/__init__.py 文件

# app/__init__.py
# _*_ coding:utf-8 _*_
import pymysql

from .app import Flask

pymysql.install_as_MySQLdb()


# 蓝图管理函数
def create_blueprint(app):
    from .api.v1 import Create_blue_v1
    # 使用url_prefix 必须加上 “/”
    app.register_blueprint(Create_blue_v1(), url_prefix='/v1') # 把视图模块挂载到 Flask 核心对象中

# 创建所有数据表
def register_plugin(app):
    from app.models.base import db
    db.init_app(app)  # 注册插件
    with app.app_context():  # 把 db.create_all() 推入Flask上下文环境中
        db.create_all()  # 必须在 Flask 的上下文环境中才能完成 create_all 操作

# 创建app项目函数
def create_app():
    app = Flask(__name__)
    # 把配置文件加载到 Flask 中
    app.config.from_pyfile('./config/secure.py')
    app.config.from_object("app.config.setting")

    create_blueprint(app)
    register_plugin(app)
    return app

1.3、如何让jsonify()返回数据模型对象?

重写 Flask.json 中 JSONEncode 下 default 函数,让其支持返回数据模型对象。

jsonify 什么情况下执行 default 函数?理解 jsonify() 序列化时的default函数?
当 flask 知道 jsonify 接收的参数是可被python序列化时,不会执行 default 函数,default 可以递归调用。

  1. 类.__dict__ 只能把类中的实例变量序列化为字典,类变量不能序列化。
  2. 这里通过 dict() 来创建字典。
  3. dict() 序列化原理:
    在执行dict(o)时,会先执行 Dictlist 类中的 keys 方法,在keys中指定返回的key值,通过keys指定我们可以灵活的返回需要序列化内容。
    默认情况python是不支持 类[‘变量名’] 访问的,但是在内部添加 __getitem__ 方法后将支持,
    hasattr(对象,属性) 判断对象中是否有属性。
    isinstance(对象,类型) 判断对象是否为这个类型。
    Python 面向对象
    Python Class 继承
class DictList:
    # 类变量
    name = '八月'
    age = 30

    def __init__(self):
        self.sex = '男'  # 实例化变量

    def keys(self):
        return ('name', 'age', 'sex',) # 当返回只有一个元素的元祖时,必须这样写('name',)

    def __getitem__(self, item):
        # item 是keys指定返回变量的key,通过getattr()获取对象相应变量的值
        return getattr(self, item)


o = DictList()
print(dict(o))

# 输出:{'name': '八月', 'age': 30, 'sex': '男'}

app/app.py 文件

# app/app.py
# _*_ coding:utf-8 _*_
from datetime import date
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
from app.libs.error_code import ServerError

# 重写 Flask 中 JSONEncode 下 default 函数
# 只要 jsonify 传递的参数,Flask不能被序列化都会调用default
class JSONEncoder(_JSONEncoder):
	# o 就是jsonify()接收的参数
    def default(self, o):
    	# 判断传入的对象 o 中是否有 keys 和 __getitem__
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date): # 判断对象 o 是否是 date 类型
            return o.strftime('%Y-%m-%d')
        raise ServerError()

class Flask(_Flask):
    json_encoder = JSONEncoder

二、自定义 Redprint 对象

2.1、自定义Redprint对象挂载 Flask 架构图

自定义Redprint对象挂载 Flask 架构图

Redprint实现思路?

  1. v1蓝图是所有视图函数公用的。
  2. 视图函数向Redprint对象注册。
  3. Redprint 注册到 Blueprint v1中。
  4. Redprint -> register 完成 Redprint 注册到Blueprint v1中的任务

2.2、为什么要自定义Redprint?

  1. Redprint 功能与Blueprint 一样。
  2. Flask 中的 Blueprint (蓝图)最好用来定义模块级别的视图请求
  3. 统一管理URL请求
  4. 简化视图函数传入的URL

2.2.1、Redprint 代码实现

定义 Redprint 对象
app/libs/redprint.py 文件功能,重写 route 装饰器,构建Redprint对象。

# app/libs/redprint.py 文件
# _*_ coding:utf-8 _*_
"""
 自定义路由管理 Redprint 对象:
    要完成与 flask 中的 Blueprint 相同的功能,
    必须把 Redprint 注册到Blueprint 中。
"""

class Redprint:
    def __init__(self, name):
        self.name = name
        self.mound = []

    # 构建 route 装饰器
    def route(self, rule, **options):
        def decorator(f):
            # 将传入的内容保存到一个列表中,等待向flask的Blueprint里面注册
            self.mound.append((f, rule, options))
            return f
        return decorator

    # 登记函数,通过 register 把视图函数注册到 Blueprint 中
    # bp 表示 flask 中的 Blueprint
    def register(self, bp, url_prefix=None):
        # 判断是否传入 url_prefix ,不传就等于视图名称
        if url_prefix is None:
            url_prefix = '/' + self.name
        for f, rule, options in self.mound:
            """
                options.pop('endpoint',f.__name__) 
                如果路由传入endpoint参数:
                从 options 字典找到key为 endpoint 的参数,将其删除,并返回 endpoint 对应的 value,
                如果没有 key 为 endpoint,返回第二参数,也就是装饰器对应函数的函数名。
                Redprint 中 endpoint 包含 蓝图.模块+视图函数 ==> 文件夹.视图文件+文件中的视图函数
            """
            endpoint = self.name + '+' + options.pop('endpoint', f.__name__)
            # 通过 Blueprint中add_url_rule方法注册route接收到的信息。
            bp.add_url_rule(url_prefix + rule, endpoint, f, **options)

2.2.2、.pop() 一些理解

	a = {
		'b': 'jsom',
		# 'f':'age'
		'c': 'name'
	}
	# 如果a字典中有‘b’这个key,返回‘b’这个key的value,如果没有则返回第二参数‘e’
	d = a.pop('b','e')
	print('d = >', d)
	print('a = >', a)
	输出:
	d = > jsom
	a = > {'c': 'name'}
	没有’b‘输出:
	d = > e
	a = > {'f': 'age', 'c': 'name'}

api/v1/__init__.py 通过 Redprint.register 对象把视图函数注册到 Blueprint 对象中

# _*_ coding:utf-8 _*_
from flask import Blueprint
from app.api.v1 import user, book, client, token


# 蓝图与自定义 redprint 的连接函数
def Create_blue_v1():
    # 定义蓝图为 v1
    blue = Blueprint('v1', __name__)
    # 通过 redprint.register把Redprint 对象注册到 Blueprint v1 中
    user.api.register(blue)
    book.api.register(blue)
    client.api.register(blue)
    token.api.register(blue)
    return blue

2.2.3、Redprint 使用

api/v1/user.py 视图文件
我们需要把从数据模型中获取到的数据序列化为字典,但是jsonify() 不支持数据模型的序列化。在app/app.py重写JSONEncode 中的default属性来实现对数据模型序列化。

# _*_ coding:utf-8 _*_
# from flask import Blueprint
# user = Blueprint('User',__name__)
from flask import jsonify, g

from app.libs.error_code import DeleteSuccess, AuthFailed
from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.base import db
from app.models.user import User

api = Redprint('user')

"""
    view_model 视图层 返回对前端友好的个性化视图模型
    比如 User模型是数据库存储的原始模型,对前端不友好
"""


# 管理员可以获取所有用户信息
@api.route('', methods=['GET'])
@auth.login_required
def super_get_user():
    # 通过uid 获取用户
    user = User.query.filter_by(status=1).all()
    # 如何返回数据模型对象?
    # jsonify() 方法重写 default 函数,让其支持返回数据模型对象
    # jsonify 什么情况下执行 default 函数?
    # 当 flask 知道 jsonify 接收的参数是可被序列化时,不会执行 default 函数
    return jsonify(user)


# 普通用户、管理员都能使用id获取信息
@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.filter_by(id=uid).first_or_404()
    return jsonify(user)


# 管理员权限删除可以删除所有用户
@api.route('/<int:uid>', methods=['DELETE'])
@auth.login_required
def super_delete_user(uid):
    with db.auto_commit():
        if uid is g.user.uid:
            return NotFound('非法请求!')
        user = User.query.filter_by(id=uid).first_or_404()
        user.delete()
    return DeleteSuccess()


# 普通用户只能删除自己,现实应用是不允许删除自己的。
@api.route('', methods=['DELETE'])
@auth.login_required
def delete_user():
    uid = g.user.uid
    # g 变量被线程隔离,多个用户请求时,不会冲突
    with db.auto_commit():
        user = User.query.filter_by(id=uid).first_or_404()
        user.delete()
    return DeleteSuccess()

api/v1/user.py 视图文件

# _*_ coding:utf-8 _*_
# from flask import Blueprint
# book = Blueprint('Book',__name__)

# 自定义URL管理类
from flask import jsonify
from sqlalchemy import or_
from app.libs.redprint import Redprint
from app.models.book import Book
from app.validators.forms import BookSearchForm

api = Redprint('book')


@api.route('/search')
def search():
    form = BookSearchForm().validate_for_api()
    q = '%' + form.q.data + '%'
    books = Book.query.filter(
                or_(Book.title.like(q), Book.publisher.like(q))
            ).all()
    books = [book.hide('summary').append('pages') for book in books]
    return jsonify(books)


@api.route('/<isbn>/detail')
def detail(isbn):
    book = Book.query.filter_by(isbn=isbn).first_or_404()
    return jsonify(book)

三、数据返回处理

数据返回处理

四、重写 werkzeug.exceptions --> HTTPException 异常处理

4.1、为什么要自定义异常处理?

为什么要自定义异常处理?
因为原有的异常处理返回的是HTML格式的错误信息,前端获取到这样的错误信息不好处理,对于API开发来说,需要返回统一JSON格式错误信息。
利用werkzeug.exceptions中HTTPException异常处理,通过继承的方式,自定义符合前端使用的异常处理。

自定义错误处理实现步骤:

  1. 继承 HTTPException 错误对象,让其处理自定义错误信息;
  2. 编写构造函数,传入自定义类参数;
  3. 调用父类的构造函数,把 msg 传入,通过父类的response处理后返回;
  4. 重写 get_body、get_headers 让其返回JSON格式的错误信息。

Python Web 框架工具包 werkzeug

# app/libs/error.py 文件
# werkzeug.exceptions 异常处理
from flask import request, json
from werkzeug.exceptions import HTTPException


class APIException(HTTPException):
    code = 500
    msg = 'sorry,we make a mistake (* _^_ )!'
    error_code = 999

    # 自定义异常处理类的构造函数
    def __init__(self, msg=None, code=None, error_code=None, headers=None):
        if code:
            self.code = code
        if error_code:
            self.error_code = error_code
        if msg:
            self.msg = msg
        # 调用 HTTPException 中的构造函数,第一个参数为错误提示信息,必须传;
        # 第二个参数为 response,不传会根据 HTTPException 接收到的 code、description 自动生成 response。
        super(APIException, self).__init__(msg, None)

    # 重写HTTPException 中个get_body,让其返回json格式错误信息
    def get_body(self, environ=None):
        body = dict(
            msg=self.msg,
            error_cod=self.error_code,
            # request 返回请求信息,请求方法,发生错误的URL
            request=request.method + ' ' + self.get_url_no_param()
        )
        return json.dumps(body)

    # 重写HTTPException 中个get_headers,改变返回请求头的返回信息。
    def get_headers(self, environ=None):
        return [("Content-Type", "application/json")]

    # 处理返回 get_body 中 request url
    @staticmethod
    def get_url_no_param():
        full_path = str(request.full_path)
        main_path = full_path.split('?')
        return main_path[0]

4.2、自定返回异常

# app/libs/error_code.py 文件
# _*_ coding:utf-8 _*_
# 自定义异常提示对象
# 导入自定义异常返回处理对象
"""
    异常的类型:
        已知错误类型:提前知道的类型
        未知错误类型:错误不固定,随时可能会报错
    解决未知异常的方法:
        不管未知异常在什么地方出现,在项目启动文件捕获所有的异常,
        捕获到异常后再进行格式化处理后统一抛出。
"""
from app.libs.error import APIException


class Success(APIException):
    code = 201
    msg = 'OK'
    error_code = 0


# 删除成功
class DeleteSuccess(Success):
    code = 202
    error_code = -1


# 未知错误
class ServerError(APIException):
    code = 500
    msg = 'sorry,we make a mistake (* _^_ )!'
    error_code = 999


class ClientTypeError(APIException):
    code = 400
    msg = 'Client Type Error'
    error_code = 1006


class ParameterException(APIException):
    code = 400
    msg = 'Invalid paramter'
    error_code = 1000


class NotFound(APIException):
    code = 404
    msg = 'the resource are not_found O_O'
    error_code = 1001


class AuthFailed(APIException):
    code = 401
    error_code = 1005
    msg = 'authorization failed'


class Forbidden(APIException):
    code = 403
    error_code = 1004
    msg = 'forbidden, not in scope'

五、枚举模块

app/libs/enums.py 文件定义用户通过什么平台登录。

# app/libs/enums.py 文件
# 枚举模块
from enum import Enum  # 引入python枚举模块

class ClientTypeEnum(Enum):
    USER_EMAIL = 100  # email 登录
    USER_MOBILE = 101  # 手机登录
    USER_MINA = 200  # 微信小程序登录
    USER_WX = 201  # 微信公众号登录

六、数据模型基类

为什么需要基类模型?
基类模型定义每个模型中相同的公共数据模型字段,也就是每个数据模型中都会出现的数据表字段。

数据模型基类 app/models/base.py

# _*_ coding:utf-8 _*_
# 重写 SQLAlchemy 中的方法,定义数据模型基类
from contextlib import contextmanager # 导入 Flask 的上下文管理器
from datetime import datetime
from flask_sqlalchemy import BaseQuery, SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger, orm, inspect
from app.libs.error_code import NotFound


class SQLAlchemy(_SQLAlchemy):
    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            db.session.rollback()
            raise e


# 重写 Query 中的一些方法
class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)

    # 重写 get_or_404 first_or_404 让其返回json格式的错误代码
    def get_or_404(self, ident):
        rv = self.get(ident)
        if rv is None:
            raise NotFound()
        return rv

    def first_or_404(self):
        rv = self.first()
        if rv is None:
            raise NotFound(msg='user not found')
        return rv


db = SQLAlchemy(query_class=Query)

# 定义基类数据模型
class Base(db.Model):
    __abstract__ = True
    create_time = Column(Integer)
    status = Column(SmallInteger, default=1)

    def __init__(self):
        self.create_time = int(datetime.now().timestamp())

    def __getitem__(self, item):
        return getattr(self, item)

    @property
    def create_datetime(self):
        if self.create_time:
            return datetime.fromtimestamp(self.create_time)
        else:
            return None

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
        	# hasattr() 判断 self 是否包含 key 的属性
        	# setattr() 对 self 中的 key 属相赋值 value
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

    def delete(self):
        self.status = 0

    # 序列化模型
    def keys(self):
        return self.fields

	# 隐藏数据模型字段
    def hide(self, *keys):
        for key in keys:
            self.fields.remove(key)
        return self

	# 添加数据模型字段
    def append(self, *keys):
        for key in keys:
            self.fields.append(key)
        return self


# 第二种序列化模型类方法
class MixinJSONSerializer:
    @orm.reconstructor
    def init_on_load(self):
        self._fields = []
        # self._include = []
        self._excludes = []

        self._set_fields()
        self.__prune_fields()

    def _set_fields(self):
        pass

    def __prune_fields(self):
        columns = inspect(self.__class__).columns
        if not self._fields:
            all_columns = set(columns.keys())
            self._fields = list(all_columns) - set(self._excludes)
	
    def hide(self, *args):
        for key in args:
            self._fields.remove(key)
        return self

    def keys(self):
        return self._fields

    def __getitem__(self, key):
        return getattr(self, key)

user.py用户数据模型。

# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, SmallInteger
from werkzeug.security import generate_password_hash, check_password_hash
from app.libs.error_code import NotFound, AuthFailed
from app.models.base import Base, db


# User 表
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)  # 用户暱称
    auth = Column(SmallInteger, default=1)  # 权限的标识
    _password = Column('password', String(100))

    # 第一种通过 keys和 base.py中的__getitem__ 联合实现数据模型的序列化
    def keys(self):
        return ('id', 'email', 'nickname', 'auth',)

    # 通过@property 预处理 password
    # 获取 数据模型中 _password 返回
    @property
    def password(self):
        return self._password

    # 获取 form 表单提交过来的 password,加密后再设置模型中的_password
    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)  # 给密码加密

    # 在对象下创建对象本身是不合理的,通过静态方法或者类方法创建。
    @staticmethod
    def register_by_email(nickname, account, secret):
        with db.auto_commit():
            user = User()
            user.nickname = nickname
            user.email = account
            user.password = secret
            db.session.add(user)
            db.session.commit()

    @staticmethod
    def verify(email, password):
        user = User.query.filter_by(email=email).first_or_404()
        if not user.check_password(password):
            raise AuthFailed()
        scope = 'AdminScope' if user.auth == 2 else 'UserScope'
        return {'uid': user.id, 'scope': scope}

    # 比较密码是否一致
    def check_password(self, raw):
        if not self._password:
            return False
        return check_password_hash(self._password, raw) # 验证密码是否一致

book.py书籍数据模型。

# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, orm
from app.models.base import Base


class Book(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))

    @orm.reconstructor
    def __init__(self):
        self.fields = ['id', 'title', 'author', 'binding',
                       'publisher', 'price',  'pubdate',
                       'isbn', 'summary', 'image'
                       ]

@orm.reconstructor
sqlalchemy 在通过元类实例化模型对象时,不会执行__init__
可以通过 orm.reconstructor 装饰器来执行__init__

七、用户知识

7.1、重新Form中的一些方法

form.validate() 方法,遇到错误是不会抛出错误信息的,只会把错误信息保存在form.erroes 属性中,既然这样我们只能手动让它抛出错误异常信息。

# _*_ coding:utf-8 _*_
from flask import request
from wtforms import Form
from app.libs.error_code import ParameterException

class BaseForm(Form):
    def __init__(self):
       '''
        request.json 把表单的提交数据转换为json
        request.args.to_dict()
        data = request.json
        request.json 调用的是 request.get_json(self, force=False, silent=False, cache=True) 这个函数
       '''
        data = request.get_json(silent=True)  # silent 保持静默,不会报错
        args = request.args.to_dict()
        super(BaseForm, self).__init__(data=data, **args) # 参数为json的时必须为data=json值,要不然不能解析

    # 重写 validate 方法,把不抛出异常修改为抛出异常
    def validate_for_api(self):
        # 调用父类的validate方法并返回出现的异常
        valid = super(BaseForm, self).validate()
        if not valid:
            raise ParameterException(msg=self.errors)
        return self

7.1、用户注册

登录方式
用户注册的方式,通过枚举的方式定义。
client.py用户注册

# app/api/v1/client.py
# _*_ coding:utf-8 _*_
# flask 中所有用户提交的数据都在request中

from app.libs.enums import ClientTypeEnum
from app.libs.error_code import Success
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, UserEmailForm

api = Redprint('client')


# 注册登录
@api.route('/register', methods=['GET', 'POST'])
def create_client():
    # 考虑的问题:参数 校验 接收参数
    # 调用重写后验证器,对提交的数据进行验证并抛出异常
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: __register_user_by_email
    }
    promise[form.type.data]()
    return Success()


# email 登录处理函数
def __register_user_by_email():
    form = UserEmailForm().validate_for_api()
    # 表单对象 ClientForm 无法获取 nickname ?
    # 1、request.json['nickname'] 中包含了这个参数,但是 request 是没有通过验证的
    #    所以不建议使用这种方法获取。
    # 2、通过 form 获取,
    #    利用python的继承特性,编写一个特殊的 Form 对象 UserEmailForm ,让其去继承
    #    ClientForm 这个基类,在 UserEmailForm 编写个性的 Form 属性。
    User.register_by_email(form.nickname.data, form.account.data, form.secret.data)

7.2、用户注册校验 WTForms

forms.py对form表单提交过来的数据验证。

为什么设置ClientForm这个基类?

  1. 增加代码的复用性,减少代码量;
  2. 通过面向对象的继承特性,适用多种用户注册方式,比如email,小程序等等;
  3. 验证共有的参数,比如不管通过什么方式注册用户都必须传入 type 这个参数,所以就可以在基类中进行统一的验证。
# _*_ coding:utf-8 _*_
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, length, Email, Regexp, ValidationError
from app.libs.enums import ClientTypeEnum
from app.models.user import User
from app.validators.base_form import BaseForm as Form


# 注册基类,
class ClientForm(Form):
    account = StringField(validators=[DataRequired(message='用户名不能为空'), length(min=5, max=32)])
    secret = StringField()  # 密码
    type = IntegerField(validators=[DataRequired()])

    # 自定义验证器,验证type
    def validate_type(self, value):
        try:
            # 把用户输入的数字转换为枚举类型
            client = ClientTypeEnum(value.data)
        except ValueError as e:
            raise e
        self.type.data = client


# 继承基类,并编写独立的email注册类
class UserEmailForm(ClientForm):
    account = StringField(validators=[Email(message='invalidate email')])
    secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
    nickname = StringField(validators=[DataRequired(), length(min=2, max=22)])

    # 验证用户是否注册,必须以 validate_ 开头
    def validate_account(self, value):
        if User.query.filter_by(email=value.data).first():
            raise ValidationError()


class BookSearchForm(Form):
    q = StringField(validators=[DataRequired()])


class TokenForm(Form):
    token = StringField(validators=[DataRequired()])

7.3、通过 Token 登录API接口

Token的基本规则:

  1. token必须得有有效期时间;
  2. token中必须存储用户的登录信息;
  3. token必须加密;
  4. 登录就是获取token的过程;
  5. 通过POST传入Token信息。

token.py 生成token与登录验证token。

# _*_ coding:utf-8 _*_
from flask import current_app, jsonify
from app.libs.enums import ClientTypeEnum
from app.libs.error_code import AuthFailed
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, TokenForm
# token令牌生成加密模块
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature

api = Redprint('token')


# 生成 Token 令牌
@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify # 调用User.verify获取到用户信息
    }
    identity = promise[ClientTypeEnum(form.type.data)](
        form.account.data,
        form.secret.data
    )
    # 生成令牌
    expiration = current_app.config['TOKEN_EXPIRATION']
    token = generate_auth_token(identity['uid'],
                                form.type.data,
                                identity['scope'],
                                expiration
                                )
    t = {
        'token': token.decode('ascii')
    }
    return jsonify(t), 201


# 获取 Token 令牌
@api.route('/secret', methods=['POST'])
def get_token_info():
    form = TokenForm().validate_for_api()
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(form.token.data, return_header=True)
    except SignatureExpired:
        raise AuthFailed(msg='token is expired', error_code=1002)
    except BadSignature:
        raise AuthFailed(msg='token is invalid', error_code=1002)
        
	# token中包含的信息
    r = {
        'scope': data[0]['scope'],
        'create_at': data[1]['iat'],  # 令牌创建时间
        'expire_in': data[1]['exp'],  # 令牌过期时间
        'uid': data[0]['uid']
    }
    return jsonify(r)


# 生成令牌函数
def generate_auth_token(uid, ac_type, scope=None, expiration=7200):
    s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    return s.dumps({
        'uid': uid,
        'type': ac_type.value,
        'scope': scope
    })

token_auth.py 验证token是否合法,提供视图函数访问保护。
通过flask_httpauth中的 HTTPBasicAuth 来为视图函数访问提供保护,,验证Token的合法性与有效时间。

# _*_ coding:utf-8 _*_
"""
HTTPBasicAuth 发送账号和密码的方式必须在HTTP请求头中发送
格式:
    固定写法 --> Authorization : basic bse64(账号:密码)
"""
from collections import namedtuple
from flask import current_app, g, request
from flask_httpauth import HTTPBasicAuth # 提供视图函数访问保护。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, \
    BadSignature, SignatureExpired

from app.libs.error_code import AuthFailed, Forbidden
from app.libs.scope import is_in_scope

auth = HTTPBasicAuth()
# 构建一个对象式结构
User = namedtuple('User', ['uid', 'ac_type', 'scope'])

"""
    在这里获取token的时候,只要把token通过账号传递进来即可,密码不用传值
    因为token中已经包含了账号密码信息。
"""

# user 登录,通过 HTTPBasicAuth 验证 token 合法函数
@auth.verify_password
def verify_password(account, password):
    # account == token
    # 验证token是否合法
    user_info = verify_auth_token(account)
    if not user_info:
        return False
    else:
        # 将 user_info 保存到全局的 g.user 中
        g.user = user_info
    return True

# 验证token是否合法、有效时间是否过期
# 1.验证令牌是否合法
# 2.验证令牌时间是否过期
def verify_auth_token(token):
    s = Serializer(current_app.config['SECRET_KEY']) # SECRET_KEY 在配置文件中必须设置
    try:
        data = s.loads(token)
    except BadSignature:  # 捕获BadSignature,验证 Token 是否合法
        raise AuthFailed(msg='token is invalid', error_code=1002)
    except SignatureExpired:  # 捕获SignatureExpired,验证 Token 是否过期
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['type']
    scope = data['scope']
    allow = is_in_scope(scope, request.endpoint) # 判断视图函数是否在权限中
    if not allow:
        raise Forbidden()
    # 最佳返回结果为对象式结构
    return User(uid, ac_type, scope)
7.3.1、python中namedtuple介绍

namedtuple类位于collections模块,namedtuple能够用来创建类似于元祖的数据类型,除了能够用索引来访问数据,能够迭代,还能够方便的通过属性名来访问数据。
在python中,传统的tuple类似于数组,只能通过下标来访问各个元素,我们还需要注释每个下表代表什么数据。通过使用namedtuple,每个元素有了自己的名字。声明namedtuple是非常简单方便的。

from collections import namedtuple

Friend = namedtuple("Friend", ['name', 'age', 'email'])

f1 = Friend('xiaowang', 33, '[email protected]')
print('f1=>', f1)
print('f1.age=>', f1.age)
print('f1.email=>', f1.email)
f2 = Friend(name='xiaozhang', email='[email protected]', age=30)
print('f2=>', f2)

name, age, email = f2
print('name, age, email=>', name, age, email)

输出:

f1=> Friend(name='xiaowang', age=33, email='[email protected]')
f1.age=> 33
f1.email=> [email protected]
f2=> Friend(name='xiaozhang', age=30, email='[email protected]')
name, age, email=> xiaozhang 30 [email protected]

八、访问权限管理

权限管理设计实例方案

权限管理对应表写在什么地方?

  1. 写到数据库中,mysql,redis等;
  2. 写在代码中,我们这里写在代码中。

scope.py权限管理文件

# _*_ coding:utf-8 _*_
# 权限管理


class Scope:
    allow_api = []  # 设置视图函数的访问权限
    allow_module = []  # 设置模块级别的访问权限
    forbidden = []  # 设置需要排除的视图函数
	
	# 通过__add__ 完成 “+” 操作,利用类的继承性,让每个继承了Scope 都可以使用 “+” 操作
    def __add__(self, other):
        # 处理视图函数访问权限
        self.allow_api = self.allow_api + other.allow_api
        # 利用 set 集合类型不重复特性,去除重复
        self.allow_api = list(set(self.allow_api))

        # 处理模块级别
        self.allow_module = self.allow_module + other.allow_module
        self.allow_module = list(set(self.allow_module))

        # 处理排除
        self.forbidden = self.forbidden + other.forbidden
        self.forbidden = list(set(self.forbidden))
        return self


class AdminScope(Scope):
    # allow_api = ['v1.user+get_user', 'v1.user+super_get_user', 'v1.user+delete_user']

    allow_module = ['v1.user']

    def __init__(self):
        self + UserScope()


class UserScope(Scope):
    forbidden = ['v1.user+delete_user', 'v1.user+super_get_user']
    # allow_api = ['v1.user+get_user', 'v1.user+delete_user']


# class SuperScope(Scope):
#     allow_api = ['v1.C', 'v1.D']
#     allow_module = ['v1.user']
#
#     def __init__(self):
#         self + UserScope() + AdminScope()

# 判断视图函数是否在权限中
# endpoint 接收的是 Redprint 对象中 endpoint ,我们将其改写为了包含 蓝图.模块+视图函数 ==> 文件夹.视图文件+文件中的视图函数 这样的形式,方便处理。
def is_in_scope(scope, endpoint):
    # 利用 globals() 实现反射
    scope = globals()[scope]()
    splits = endpoint.split('+')
    red_name = splits[0]
    if endpoint in scope.forbidden:
        return False
    if endpoint in scope.allow_api:
        return True
    if red_name in scope.allow_module:
        return True
    else:
        return False

8.1、globals() 实现反射

globals() 的定义:返回一个字典, 表示当前位置的所有全局符号表。 这个符号表始终针对当前模块(对函数或方法来说, 是指定义它们的模块, 而不是调用它们的模块)。
现在有个问题,如果只想调用函数名以 _super 结尾的函数,但是又不确定具体方法名和有几个函数,该怎么办呢?这时候我们可以用 globals() 函数来实现,注意示例中 main 函数中的代码部分

def zero_super():
    return 0

def one_super():
    return 1

def two_super():
    return 2

def hello():
    print("Hello")

if __name__ == '__main__':
    promos = [name for name in globals()]
    print(promos)

输出:

['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__','zero _super', 'one _super', 'two _super','hello'] 

前面打印的都是一些内置函数,我们不关注,我们只关注自己定义的函数,那么只想调用某一类型的函数,比如后缀是 _super 结尾的怎么办呢?加上一个个过滤条件:

if __name__ == '__main__':
    promos = [name for name in globals() if name.endswith(" _super")]
    print(promos)

输出:

['zero_promo', 'one_promo', 'two_promo']

现在得到的是一个字符串,肯定是不能执行的,那该如何是好呢?这里就可以利用 globals() [name] 来获取函数体。

if __name__ == '__main__':
    promos = [globals()[name] for name in globals() if name.endswith(" _super")]
    print(promos)
	print(promos[0]())

输出:

[<function zero_promo at 0x0000013872FE2E18>, <function one_promo at 0x00000138735D5730>, <function two_promo at 0x00000138735D57B8>]

获得函数体后就可以加‘()’调用函数。

	promos[0]()

输出:

0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章