Flask之RESTFul API前后端分离

2,279 阅读23分钟

Flask REST-Framework

代码可见gitee--- gitee.com/ouyangguoyo…

一:起步与红图

1:虚拟环境搭建的两种方式

1 pipenv的使用

pip install --user pipenv安装pipenv在用户目录下
py -m site --user-site通过此命令找到用户基础目录,结果为C:\Users\u14e\AppData\Roaming\Python\Python35\site-packages
将用户基础目录结尾的site-packages换成Scripts,即C:\Users\u14e\AppData\Roaming\Python\Python35\Scripts,然后将这一路径添加到系统变量中
重新打开命令行工具,如cmd,pipenv --version检查是否安装成功
pipenv install创建一个虚拟环境
pipenv shell激活虚拟环境,exit推出虚拟环境
pipenv install requests安装python包,pipenv install django==1.11.7安装制定版本的包,pipenv uninstall requests卸载包
pipenv graph查看安装的包,以及依赖的其他包

2 pip virtualenv(使用workon进行管理)的使用

# 1 安装
pip install virtualenv
virtualenv --version

# 非Windows
# pip install virtualenvwrapper

# Windows
pip install virtualenvwrapper-win

# 查看当前所有的虚拟环境
workon

# 创建一个虚拟环境
mkvirtualenv new_env

# 删除一个虚拟环境
rmvirtualenv new_env

2 项目初始化

# 安装需要的包
pip install flask flask-sqlalchemy flask-wtf cymysql flask-cors flask-httpauth requests -i https://pypi.doubanio.com/simple
    
# 项目依赖
- 项目依赖 pip3 install pipreqs
- 生成依赖文件:pipreqs ./ --encoding=='utf-8'
- 安装依赖文件:pip3 install -r requirements.txt 

3 新建入口文件以及蓝图注册

# ginger.py--入口文件
from app.app import create_app

__author__ = '欧阳'

app = create_app()

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



# app/app.py--核心对象文件
from flask import Flask

__author__ = '欧阳'


def register_blueprints(app):
    from app.api.v1.user import user
    from app.api.v1.book import book
    app.register_blueprint(user)
    app.register_blueprint(book)


def create_app():
    app = Flask(__name__)
    # 注册配置文件
    app.config.from_object('app.config.secure')
    app.config.from_object('app.config.setting')
    # 注册蓝图
    register_blueprints(app)

    return app

# app/api/v1/user.py
from flask import Blueprint

__author__ = '欧阳'
user = Blueprint('user', __name__)


@user.route('/v1/user/get')
def get_user():
    return 'i am get_user'


# app/api/v1/book.py
from flask import Blueprint
__author__ = '欧阳'


book = Blueprint('book', __name__)


@book.route('/v1/book/get')
def get_user():
    return 'i am get_book'

4 红图的创建

蓝图适合在文件夹级别使用,具体到文件,还是使用红图

创建红图对象

# app/libs/redprint.py
"""
Created by Andy on 2020/2/27 0027
"""

__author__ = '欧阳'


class RedPrint:
    def __init__(self, name):
        # self.name 就是视图中的文件级别的路由
        self.name = name
        self.mound = []


    def route(self, rule, **options):
        def decorator(f):
            self.mound.append((f, rule, options))
            return f

        return decorator

    def register(self, bp, url_prefix=None):
        if url_prefix is None:
            url_prefix = '/' + self.name
        print(self.mound)
        for f, rule, options in self.mound:
            endpoint = options.pop("endpoint", f.__name__)
            bp.add_url_rule(url_prefix + rule, endpoint, f, **options)



在视图函数中使用红图

from app.libs.redprint import RedPrint

__author__ = '欧阳'
api = RedPrint('user')

# 定义号红图之后,由于/v1在蓝图中,/user在红图中,所以视图函数中只需要学后面的路由即可
@api.route('/get')
def get_user1():
    return 'i am get_user'

将红图注册到蓝图

# app/api/v1/__init__.py
from flask import Blueprint
from app.api.v1 import book, user

__author__ = '欧阳'


def create_blueprint_v1():
    bp_v1 = Blueprint('v1', __name__)
    user.api.register(bp_v1)  # 注册红图
    book.api.register(bp_v1)
    return bp_v1

在flask核心对象中注册蓝图

# app/app.py
from app.api.v1 import create_blueprint

def register_blueprint_v1(app):
    from app.api.v1 import create_blueprint_v1
    # url_prefix就是路由里面的v1
    app.register_blueprint(create_blueprint_v1(), url_prefix='/v1')
    

二:REST的基本特征

三:自定义异常对象

1 关于用户的思考

  1. 对于API而言,再叫做用户就不太合适 ,我们更倾向于把人,第三方的产品等同于成为客户端(client)来代替User。

  2. 客户端的种类非常多,注册的形式就非常多。如对于普通的用户而言,就是账号和密码,但是账号和密码又可以分成,短信,邮件,社交用户。对于多种的注册形式,也不是所有的都需要密码,如小程序就不需要。

  3. API和普通的业务系统是不一样的,他具有开发性和通用性。

2 构建Client验证器

构建用户枚举类型

from enum import Enum

class ClientTypeEnum(Enum):
    USER_EMAIL = 100
    USER_MOBILE = 101

    # 微信小程序
    USER_MINA = 200
    # 微信公众号
    USER_WX = 201
    

client验证器

from wtforms import StringField, IntegerField,Form
from wtforms.validators import DataRequired, length, Email, Regexp
from wtforms import ValidationError

from app.libs.enums import ClientTypeEnum

class ClientForm(Form):
    account = StringField(validators=[Datarequired(message = '不允许为空'),length(min=5,max=32)])
    secret = StringField()
    type = IntegerField(validators=[DataRequired()])
    
    def validate_type(self,value):
        try:
            client = ClientTypeEnum(value.data)
        execpt valueError as e:
            raise e
        self.type.data = client
            

3 创建User模型

User模型

# models/user.py
from sqlalchemy import inspect, Column, Integer, String, SmallInteger, orm
from werkzeug.security import generate_password_hash

from app.models.base import Base, db
import datetime

__author__ = '欧阳'


class User(Base):
    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))

    def keys(self):
        return ['id', 'email', 'nickname', 'auth']

    @property
    def password(self):
        return self._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)

我们的User模型继承了base.py中的Base,base.py文件如下,就像倒一个第三方的包一样去使用

"""
Created by Andy on 2020/2/27 0027
"""

from datetime import datetime
from contextlib import contextmanager
from sqlalchemy import Column, Integer, SmallInteger
from flask import current_app
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery

__author__ = '欧阳'

__all__ = ['db', 'Base']


class SQLAlchemy(_SQLAlchemy):
    @contextmanager
    def auto_commit(self, throw=True):
        try:
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            current_app.logger.exception('%r' % e)
            if throw:
                raise e


class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)


db = SQLAlchemy(query_class=Query)


class Base(db.Model):
    __abstract__ = True
    create_time = Column('create_time', Integer)
    status = Column(SmallInteger, default=1)

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

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

    def delete(self):
        self.status = 0

    def set_attrs(self, attrs):
        for key, value in attrs.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

4 客户端注册

我们提交数据的时候,可以提交表单格式,json格式,data格式的

通常在我们的网页中,提交表单格式的数据,但是在我们小程序,app开发中使用json格式的数据提交方式

在forms.py中添加UserEmailForm方法

# forms.py
class UserEmailForm(ClientForm):
    account = StringField(validators=[
        Email(message='invalidate email')
    ])
    secret = StringField(validators=[
        DataRequired(),
        # password can only include letters , numbers and "_"
        Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')
    ])
    nickname = StringField(validators=[DataRequired(),
                                       length(min=2, max=22)])

    def validate_account(self, value):
        if User.query.filter_by(email=value.data).first():
            raise ValidationError()

视图函数

# v1/client.py
@api.route('/register', methods=['POST'])
def create_client():
    data = request.json
    form = ClientForm(data=data)
    if form.validate():
        promise = {
            ClientTypeEnum.USER_EMAIL: __register_user_by_email
        }
        promise[form.type.data]()  

    return 'success'


def __register_user_by_email():
    """
    使用邮箱注册
    :return:
    """
    form = UserEmailForm(data=request.data)
    if form.validate():
        User.register_by_email(form.nickname.data, form.account.data, form.secret.data)

5 自定义异常对象

我们在编码过程中,会发生各种各样的错误,如果我们要捕捉他们,就使用

from werkzeug.exceptions import HTTPException
# exceptions有很多的异常对象

# 我们要使用异常对象得时候,就会使用
class ClientTypeError(HTTPException):
    code = 400
    description = (
        'client is invalid'
    )

# 再需要使用得地方调用就可以了

6 自定义APIException对象

定义APIException对象,其他得错误对象直接继承APIException即可

# libs/error.py
"""
Created by Andy on 2020/2/28 0028
"""

from flask import json, request
from werkzeug.exceptions import HTTPException

__author__ = '欧阳'


class APIException(HTTPException):
    code = 500
    msg = 'sorry, we made 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
        super(APIException, self).__init__(msg, None)

    def get_body(self, environ=None):
        body = dict(
            msg=self.msg,
            error_code=self.error_code,
            request=request.method + ' ' + self.get_url_no_param()
        )
        text = json.dumps(body)
        return text

    def get_headers(self, environ=None):
        """Get a list of headers."""
        return [('Content-Type', 'application/json')]

    @staticmethod
    def get_url_no_param():
        full_path = str(request.full_path)
        main_path = full_path.split('?')
        return main_path[0]

那我们再需要调用得地方调用即可

from app.libs.error import APIException

__author__ = '欧阳'

class ClientTypeError(APIException):
    code = 400
    error_code = 1006
    msg = 'client is invalid'

四:理解WTForms并灵活改造他

1 重写WTForms

再error_code中写一个参数错误得类

class ParameterException(APIException):
    code = 300
    error_code = 1000
    msg = 'invalid parameter'

再validate中写一个名叫base.py得方法,重写了Form方法,新写一个validate_for_api得方法

from wtforms import Form
from app.libs.error_code import ParameterException

__author__ = '欧阳'


class BaseForm(Form):
    def __init__(self, data):
        super(BaseForm, self).__init__(data=data)

    def validate_for_api(self):
        valid = super(BaseForm, self).validate()
        if not valid:
            raise ParameterException(msg=self.errors)

那我们再视图函数中验证传过来得数据得时候

data = request.json
    form = ClientForm(data=data)
    form.validate_for_api()  # 这个地方不再是form.validate(),避免了再写if else 
    promise = {
        ClientTypeEnum.USER_EMAIL: __register_user_by_email
    }
    promise[form.type.data]()
    return 'success'

2 接受定义得复杂,但是决不接受调用得复杂

在我们全段传过来得参数得时候,每一次都要写

data = request.json

这个时候,我们可以再validate得BaseForm中写

class BaseForm(Form):
    def __init__(self):
        data = request.json  # 这样就避免了在每一个视图函数中都写data = request.json
        super(BaseForm, self).__init__(data=data)

    def validate_for_api(self):
        valid = super(BaseForm, self).validate()
        if not valid:
            raise ParameterException(msg=self.errors)

        # 下面一行代码就实现了参数得获取,校验,以及过滤后参数得获取
        # form = ClientForm().validate_for_api()
        return self  # 这个self就是form

3 已知异常和未知异常

第一种类型:我们可以预知的类型 已知异常 APIExceotion

第二种类型:未知异常

4 解决未知异常的方案

在入口文件写

下面的方法就可以捕捉比如一些代码异常等未知异常的错误,会以json个格式字符串的方式传出

# ginger
@app.errorhandler(Exception)
def framework_error(e):
    """获取未知异常"""
    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:
        # 调试模式
        # log
        if not app.config['DEBUG']:
            return ServerError()
        else:
            raise e
            

定义一个ServerError类

app/libs/error_code

class ServerError(APIException):
    code = 500
    msg = 'sorry, we made a mistake (* ̄︶ ̄)!'
    error_code = 999

五: Token与HTTPBasic验证 —— 用令牌来管理用户

1 获取Token令牌

在flask中,新建一个模块token.py

# v1/tokern.py
from flask import current_app, jsonify

from app.libs.enums import ClientTypeEnum
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm

# 加密算法,生成令牌
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

__author__ = '欧阳'

api = Redprint('token')


@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify
    }
    identity = promise[form.type.data](
        form.account.data,
        form.secret.data
    )
    expiration = current_app.config['TOKEN_EXPIRATION']
    token = generate_auth_token(
        identity['uid'],
        form.type.data,
        None,  # identity['scope']
        expiration
    )
    t = {
        'token':token.decode('ascii')
    }
    return jsonify(t),201


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

在User中新建一个校验参数的静态方法

# models.user.py
class User(Base):
    ...
    @staticmethod
    def verify(email, password):
        """登录验证"""
        user = User.query.filter_by(email=email).first()
        if not user:  # 要是用户不存在
            raise NotFound(msg='user not found')
        if not user.check_password(password):  # 检查密码
            raise AuthFailed()
        return {'uid': user.id}

    def check_password(self, raw):
        if not self._password:
            return False
        return check_password_hash(self._password, raw)

在setting.py配置文件中

TOKEN_EXPIRATION = 30*24*3600  # token的过期时间

2 Token的用处

验证token是否合法

验证token是否过期

3 @auth拦截器执行流程

我们想要在只想某个视图函数之前继续校验,就需要auth

新建一个token_auth的模块

# libs/token_auth.py
from flask_httpauth import HTTPBasicAuth

__author__ = '欧阳'

auth = HTTPBasicAuth()
"""
在需要认证的视图函数上方加上
@auth.login_required()
但是在这之前写一段让@auth.verify_password装饰的代码,校验token
代码如下 def verify_password():
"""


@auth.verify_password
def verify_password(account, password):
    pass

然后在需要认证的视图函数上面加@auth.login_required()

from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth

@auth.verify_password
def verify_password(account,password):
    pass

# 当我们的verify_password函数的返回值为True的时候,说明token校验通过,
# 接下来流程就会进入@auth.login_required装饰的视图函数里面

4 HTTPBasicAuth基本原理

想要使用HTTP的方式发送账号密码,就必须将账号和密码加入到HTTP的头里面去

header

而我们http头的数据类型是一个个的键值对,我们把

key设置为Authorzation

value是账号:密码

但是做成这样还不行,要这样写

key=Authorization

value = basic base64(andy:123456)

base 空格,后面跟的是经过base64加密后的账号和密码

操作方法

# 那么我们怎么在我们的verify_password中传入account和password呢
1 将用户名和密码进行base64加密
2 在post相关路由下面点击Headers
3 key=Authorization  value=basic YW5keToxMjM0NTY=

# 运行之后,即可看见verify_password函数中的
account='andy'
password='123456'

在postman中我们可以简化这一过程

postman中,我们点击Authorization,TYPE中选择Basic Auth

username中输入我们的令牌就可以在verify_password中获取我们的token,即获取

account='token令牌'

5 验证Token

我们的token的校验就在token_auth.py这个模块中

from collections import namedtuple

from flask import current_app, g
from flask_httpauth import HTTPBasicAuth
from itsdangerous import TimedJSONWebSignatureSerializer \
    as Serializer, BadSignature, SignatureExpired

from app.libs.error_code import AuthFailed

__author__ = '欧阳'

auth = HTTPBasicAuth()
User = namedtuple('User', ['uid', 'ac_type', 'scope'])  # 使用namedtuple

"""
在需要认证的视图函数上方加上
@auth.login_required()
但是在这之前写一段让@auth.verify_password装饰的代码,校验token
代码如下 def verify_password():
"""


@auth.verify_password
def verify_password(token, password):
    user_info = verify_auth_token(token)
    if not user_info:
        return False
    else:
        g.user = user_info
        return True


def verify_auth_token(token):
    """检验我们的令牌"""

    # 同生成token一样,需要序列化
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(token)  # 解密token
    except BadSignature:  # 解密失败的话
        raise AuthFailed(msg='token is inval', error_code=1002)
    except SignatureExpired:  # 校验token是否过期
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['ac_type']
    return User(uid, ac_type, '')

6 重写first_or_404与get_or_404

当我们查询一个用户的数据的时候

user = User.query.get(uid)
if not user:
	raise NotFound()
return 'get user'

一旦我们这样写,我们每一次使用查询的时候都要写if else判断,太麻烦,于是使用get_or_404()

但是使用get_or_404的话,返回的就不是一个json格式的字符串了

还有就是我们的for_or_404方法,也可以重写

# models/base.py
class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs:
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)

    def get_or_404(self, ident, description=None):
        """get id 找不到资源返回404"""

        rv = self.get(ident)
        if rv is None:
            raise NotFound()
        return rv

    def first_or_404(self, description=None):
        """找不到资源返回404"""

        rv = self.first()
        if rv is None:
            raise NotFound()
        return rv

之后我们在查询数据的时候i,只需要写下面这样的代码,就可以实现判断和取值了

user = User.query.filter_by(email=email).first_or_404()

六:模型对象的序列化

1 理解序列化时的default函数

1-1 序列化序言

如果Flask的jsonify知道如何序列化传入的数据结构的时候,是不会去调用defalut函数的

不知道怎么序列化你的数据结构的时候,就会进入序列化

转而言之,我们可以在default中实现自定义的序列化

类变量是不会存放在__dict__中的

1-2我们重写我们的JsonEncoder方法

在创建app的模块中

# app/app.py
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
class JsonEncoder(_JSONEncoder):
    """自动逸的序列化对象"""

    def default(self, o):
        """遇见不能序列化的对象,就添更多的if来处理"""
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)

        if isinstance(o, date):
            """如果o是时间类型"""
            return o.strftime('%Y-%m-%d')

        raise ServerError()


class Flask(_Flask):
    """将自定义的序列化对象替换flask中json模块的JsonEncoder"""
    json_encoder = JsonEncoder

2 深入理解dict的机制

2-1理解dict机制

在我们使用dict这个内置函数来定义字典的时候,内部调用keys方法

在我们想实现通过类对象中括号的方式拿到类变量的时候,会调用类的__getitem__方法

class Andy:
    name = 'andy'
    age = 18

    def __init__(self):
        self.gender = 'mail'

    def keys(self):
        """指明我们要序列化的属性"""
        # 当我们元组只有一个元素的时候,记得在元素后面加','
        return ('name', 'age', 'gender')

    def __getitem__(self, item):
        """将keys的返回结果一个个作用过来"""
        return getattr(self, item)  # 通过g


# o = Andy()
# print(o['name'], o['age'])
# 每次通过字典取值的方式去类中取类变量的时候,会调用类的keys方法
# 而__getitem__后面的参数就是传过来的字符串,通过getattr取类中取该key对应的值
# 这样就实现了类变量通过字典方式取值

o = Andy()
print(dict(o))

"""
上面代码实现的过程
就是先将类实例化成字典--dict(类对象)
然后会去调用类中的keys方法,获取返回的值,是一个元组
将元组的值依次作用域类中的__getitem__方法
最后生成一个按照keys中返回值为key生成的一个字典
从而实现将对象转化为字典格式的数据


"""

2-2实现我们根据id获取User对象数据的示例

首先视图函数

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid)
    return jsonify(user)  # 序列化的时候,会调用上面1-2中重写的default方法

2-3在我们的User模型类中

定义keys方法和__getitem__方法

class User(Base):
    ...
    # 在我们需要对User有返回结果的时候,就会按照下面两个方法序列化我们的类
    def keys(self):
        """指明我们要序列化的属性"""
        return ['id', 'email', 'nickname', 'auth']

# 我们可以将__getitme__放到模型的基类中
class Base(db.Model):
    ...
    def __getitem__(self, item):
        """将__dict__的返回结果一个个作用过来"""
        return getattr(self, item)

2-4查看返回结果

{
    "auth": 1,
    "email": "111@qq.com",
    "id": 1,
    "nickname": "甜心"
}

至此,我们就实现了jsonify直接序列化我们模型对象的想法

注:在我们需要序列化的队中,要是出现了不能序列化的数据,就会让这个数据单独的再去调用default方法,并且返回serverError()

强调:在我们后面需要序列化的时候,只需要注意在模型类中写上我们的keys的方法就好

3 ViewModel

view_model为视图层提供个性化的视图模型

比如说我们返回的数据中定义的是1和2,但是前端需要具体的说明,这个时候就要使用我们的view_model

还有就是我们如果需要两个或者很多个模型组合到一起返回的数据,这个时候与要去使用我们的view_model

restful表示的是资源节点的获取,view_model对其的意义不大,但是我们在项目的开发过程中,一定要合理且灵活的使用

七:权限控制

1 删除模型注意事项

在我们删除数据库数据的时候,不要去真的删除数据,而是需要去伪删除

# v1/user.py
@api.route('', methods=['DELETE'])
@auth.login_required
def delete_user():
    uid = g.user.uid # 在校验是否登录的时候,就把uid和ac_type存放到了g变量中,而且g变量是线程隔离的
    with db.auto_commit():
        user = User.query.filter_by(id=uid).first_or_404()
        user.delete()
    return DeleteSuccess()

在我们的模型的基类中

class Base(db.Model):
    ...
    def delete(self):
        self.status = 0

为了防止超权操作

而且因为这是一个删除操作,只能自己操作自己的数据,所以将在登陆时放在g变量中的uid取出来,即

uid = g.user.uid # 在校验是否登录的时候,就把uid和ac_type存放到了g变量中,而且g变量是线程隔离的

有的时候我们需要通过管理去封禁一个用户的id,那么就可以通过传递id来封禁用户的id

2 生成超级管理员账号

我们访问一个视图函数的流程是

登录(创建token)-->访问视图函数-->权限校验-->通过或者不通过

1 登录的时候创建token,这个时候把auth,uid字段的值传到通过verify方法返回出去

# models/user.py
# 在user的模型类中,把id和scope的值传过去,是根据auth判断是什么身份,scope是字符串
class User(Base):
    @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)

2 然后生成token的时候,调用1中的方法,token中携带uid,type,scope

# v1/token.py
"""
Created by Andy on 2020/3/8 0008
"""
from flask import current_app, jsonify

from app.libs.enums import ClientTypeEnum
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm

# 加密算法,生成令牌
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

__author__ = '欧阳'

api = Redprint('token')


@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify  # 调用User模型类中的方法
    }
    identity = promise[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


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


3 在libs中定义一个token_auth.py的模块,这个模块的作用是对用户做权限校验的

# token_auth.py
"""
Created by Andy on 2020/3/8 0008
"""
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

__author__ = '欧阳'

auth = HTTPBasicAuth()
User = namedtuple('User', ['uid', 'ac_type', 'scope'])

"""
在需要认证的视图函数上方加上
@auth.login_required()
但是在这之前写一段让@auth.verify_password装饰的代码,校验token
代码如下 def verify_password():
"""


@auth.verify_password
def verify_password(token, password):
    user_info = verify_auth_token(token)
    if not user_info:
        return False
    else:
        g.user = user_info
        return True


def verify_auth_token(token):
    """检验我们的令牌"""

    # 同生成token一样,需要序列化
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(token)  # 解密token
    except BadSignature:  # 解密失败的话
        raise AuthFailed(msg='token is inval', error_code=1002)
    except SignatureExpired:  # 校验token是否过期
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['ac_type']
    scope = data['scope']
    # 还可以拿到当前请求访问的视图函数
    # 通过request.endpoint

    # 下main大三行代码就是做权限验证的
    allow = is_in_scope(scope, request.endpoint)
    if not allow:
        raise Forbidden()

    return User(uid, ac_type, scope)

中间涉及到权限的一个方法在下面

在libs中创建于给scope.py的模块

# scope.py
"""
Created by Andy on 2020/3/9 0009
"""

__author__ = '欧阳'
"""
权限校验
管理员可以访问哪些接口
普通用户可以访问哪些接口
"""

class AdminScope:
    allow_api = ['v1.super_get_user','v1.get_user']


class UserScope:
    allow_api = ['v1.get_user']


def is_in_scope(scope, endpoint):
    """
    将刚传过来的字符串变成模块中的类对象,视图函数
    :param scope: str传过来的字符串,比如‘AdminScope’
    :param endpoint: str视图函数的名字
    :return:
    """
    scope = globals()[scope]()  # 返回一个字典,是这个模块中的所有的对象
    if endpoint in scope.allow_api:
        return True
    else:
        return False

当我们访问视图函数的时候,因为加了@auth.login_required装饰器

4 在我们需要做权限验证的视图函数上面加装饰器@auth.login_required

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def super_get_user(uid):
    user = User.query.filter_by(id=uid).first_or_404()
    return jsonify(user)

3 Scope优化一:权限相加

在我们做权限设置的时候,对于普通用户的权限和管理员的权限,其实不用说管理员可以操作的

api多就一定要在SuperScope中allow_api列表中写过多的元素,只需要将普通用户的api加过来就可以

class UserScope:
    allow_api = ['v1.get_user']

class SuperScope:
    """
    通过下面__init__.py的方式和add的函数,
    可以将UserScope()类中的allow_api的元素添加到自己类中的allow_api的列表中
    """
    allow_api = ['v1.A', 'v1.B']


    def __init__(self):
        # 通过add的方式实现链式相加
        self.add(UserScope()).add(AdminScope())

    def add(self, other):
        self.allow_api = self.allow_api + other.allow_api】
        return self
    

print(SuperScope().allow_api)  # ['v1.A', 'v1.B', 'v1.get_user']

4 Scope优化二:运算符重载

运算符重载就是将类中重写__add__方法这样,就可以支持self+类方法的操作,从而触发__add__方法eg:self+UserScope()
class Scope:
    def __add__(self, other):
        self.allow_api = self.allow_api + other.allow_api
        return self


class AdminScope(Scope):
    allow_api = ['v1.super_get_user']

    def __init__(self):
        # 
        self + UserScope()


class UserScope(Scope):
    allow_api = ['v1.get_user']


class SuperScope(Scope):
    """
    通过下面__init__.py的方式和add的函数,
    可以将UserScope()类中的allow_api的元素添加到自己类中的allow_api的列表中
    """
    allow_api = ['v1.A', 'v1.B']

    def __init__(self):
        # 
        self + UserScope() + AdminScope()


print(SuperScope().allow_api)  # ['v1.A', 'v1.B', 'v1.get_user']
a = 2

5 权限控制总结

首先,我们需要知道

1 endpoint详解

for flask import request
# 这个request不仅可以去到请求类型
# eg:
request.method

# 还可以取到endpoint的
request.endpoint

# 同时,这个endpoint还是可以自定义的,就是在我们定义红图类的时候
class Redprint:
    ...
    ...
    ...
    def register(self, bp, url_prefix=None):
        if url_prefix is None:
            url_prefix = '/' + self.name
            for f, rule, options in self.mound:
                # 这个里面的endpoint的结构是‘版本.redprint_name+view_funk’
                # eg 'v1.user+get_user'
                endpoint = self.name + '+' + options.pop("endpoint", f.__name__)
                bp.add_url_rule(url_prefix + rule, endpoint, f, **options)

回到我们的token_auth中,在我们对用户有进行权限校验的时候,调用了is_in_scope方法

并且将用户类型和endpoint当作参数传了过去

返回值为True校验通过,为False校验失败

def verify_auth_token(token):
    """检验我们的令牌"""

   	...
	...

    # 下main大三行代码就是做权限验证的
    allow = is_in_scope(scope, request.endpoint)
    if not allow:
        raise Forbidden()

    return User(uid, ac_type, scope)

2 scope完整写法

新建一个scope模块

我们可以看到在模块中的有AdminScope类和UserScope类

还有基类Scope,里面定义了三个空列表

allow_api = []  # 视图函数
allow_module = []  # 红图
forbidden = []  # 排除

运行的过程

allow_api中添加此类型用户可以访问的视图函数 ‘v1.user+get_user’

allow_modele中添加此类型用户可以访问的模块,就是我们的红图 'v1.user'

forbidden中存放的是限制访问的视图函数 ‘v1.user+get_user’

在构造函数中可以通过 self+类名的方法将对方的allow_api和allow_module通过类似继承的方式加到自己相对的allow_api和allow_module中去

"""
Created by Andy on 2020/3/9 0009
"""

__author__ = '欧阳'
"""
权限校验
管理员可以访问哪些接口
普通用户可以访问哪些接口
"""

"""
运算符重载
就是将类中重写__add__方法
这样,就可以支持self+类方法的操作,从而触发__add__方法
eg:
self+UserScope()

"""


class Scope:
    allow_api = []  # 视图函数
    allow_module = []  # 红图
    forbidden = []  # 排除

    def __add__(self, other):
        self.allow_api = self.allow_api + other.allow_api
        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))

        return self


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

    def __init__(self):
        pass

        # self + UserScope()


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

    def __init__(self):
        self + AdminScope()


"""
class SuperScope(Scope):
    '''
    通过下面__init__.py的方式和add的函数,
    可以将UserScope()类中的allow_api的元素添加到自己类中的allow_api的列表中
    '''
    allow_api = ['v1.A', 'v1.B']
    allow_module = ['v1.user']

    def __init__(self):
        self + UserScope() + AdminScope()


# print(SuperScope().allow_api)  # ['v1.A', 'v1.B', 'v1.get_user']
# a = 2
"""


def is_in_scope(scope, endpoint):
    """
    将刚传过来的字符串变成模块中的类对象,视图函数
    :param scope: str传过来的字符串,比如‘AdminScope’
    :param endpoint: str视图函数的名字
    :return:
    """
    scope = globals()[scope]()  # 返回一个字典,是这个模块中的所有的对象
    splits = endpoint.split('+')
    red_name = splits[0]  # v1.user  红图
    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

另外,我们可以根据相关业务逻辑灵活的改写is_in_scope中的代码

八:实现部分api

1 调用外部api获取数据并写入数据库

1 创建Book模型类

from sqlalchemy import inspect, Column, Integer, String, SmallInteger, orm, Text
from app.models.base import Base, db
class Book(Base):
    __tablename__ = 'book'
    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(Text)
    image = Column(String(50))

2 在Flask入口文件创建一个fake.py的文件

"""
 Created by 七月 on 2018/5/1.
"""
import json

__author__ = '欧阳'
"""
离线脚本
"""

from app import create_app
from app.models.base import db
from app.models.user import User
from app.models.book import Book
import requests
import re
app = create_app()


class Push:
    """从鱼书api获取数据写道book数据库"""

    def __init__(self, url):
        self.url = url
        self.books = []  # 存放书籍数据
        self.go()
        self.push_in_database()

    def get_data(self, url):
        data = requests.get(url)
        json_str = data.text
        data = json.loads(json_str, encoding='utf-8')
        return data

    def parse(self, data):
        books = data['books']
        for book in books:
            data = self.__matching(book)
            self.books.append(data)

    def __matching(self, book):
        data = {
            'title': book['title'],
            'author': book['author'][0],
            'binding': book['binding'] if book['binding'] is not None else '不详',
            'publisher': book['publisher'],
            'price': book['price'],
            'pages': re.findall("\d*",book['pages'])[0],
            'pubdate': book['pubdate'],
            'isbn': book['isbn'],
            'summary': bytes(book['summary'],'utf-8'),
            'image': book['image'],

        }
        a = 1
        return data

    def go(self):
        data = self.get_data(self.url)
        self.parse(data)

    def push_in_database(self):
        with app.app_context():
            for book_data in self.books:
                print(book_data)
                if Book.query.filter_by(isbn=book_data['isbn']).first():
                    continue
                with db.auto_commit():
                    book = Book()
                    book.set_attrs(book_data)
                    db.session.add(book)


Push('http://t.yushu.im/v2/book/search?q=村上春树&count=100')
Push('http://t.yushu.im/v2/book/search?q=红楼梦&count=100')
Push('http://t.yushu.im/v2/book/search?q=柯南&count=100')
Push('http://t.yushu.im/v2/book/search?q=b&count=100')

注意:可以使用with app.app_context():实现上下文管理

2 使用模糊搜索查询书籍数据

视图函数

@api.route('/search', methods=['GET'])
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') for book in books]  # 去除keys里面的指定的元素
    return jsonify(books)


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

在我们序列化类对象的时候,是使用的类里面的keys方法,但是在上面的视图函数中我们可以看出,要是直接这样写会报错,因为keys内部的列表

3 序列化时使用hide和orm.reconstructor装饰器

模型类Book

这样,在我们使用jsonify序列化查询到的对象的时候,调用hide方法,将需要剔除的参数在keys方法中去除掉

 from sqlalchemy import orm  # 使用orm,就可以在程序执行的时候执行所装饰的方法,这里是构造函数
class Book(Base):
   	...
    ...
    @orm.reconstructor
    def __init__(self):
        self.fields = ['id', 'title', 'author', 'binding', 'publisher',
                       'price', 'pages', 'pubdate', 'isbn', 'summary',
                       'image']

    def keys(self):
        return self.fields

    def hide(self, field):
        self.fields.remove(field)
        return self

类变量,所有的对象都共享的

实例变量,每一次实例化都是都会恢复原样

4 模型类中hide,append以及__init__的用法总结

在我们模型类中定义下列的构造函数

使用jsonify传对象的时候,像实例化什么字典就在模型类中按照下面添加方式添加即可

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

我们在模型类的基类Base中定义keys,hide,append的方法

class Base(db.Model):    
    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

在视图函数中

# 查询到一个queryset对象之后
books = Book.query.filter(or_(Book.title.like(q), Book.publisher.like(q))).all()
# 循环这个对象使用hide或者append方法
books = [book.hide('summary,id').append('pages') for book in books]

因为在hide或者append方法里面,有一个形参*args

这样,穿过来的参数可以当作元组接收,所以对象调用hide或者append方法的时候,可以传一个参数或者多个参数

5 实现获取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=1003)
    except BadSignature:
        raise AuthFailed(msg='token is invalid', error_code=1002)

    r = {
        'scope': data[0]['scope'],
        'create_at': data[1]['iat'],
        'expire_in': data[1]['exp'],
        'uid': data[0]['uid']
    }
    return jsonify(r)

在postman中

{"token": "eyJhbGciOiJIUzUxMiIsImlhdCI6MTU4MzgyMzE4NCwiZXhwIjoxNTg2NDE1MTg0fQ.eyJ1aWQiOjYsImFjX3R5cGUiOjEwMCwic2NvcGUiOiJBZG1pblNjb3BlIn0.kvuciQXeI23r1DYvAbFXgasH8cToRuUYlt0S2vUqpV3rjEgALQlpT-cFD37GqTUB-mV0vo24apQOpluFw0TfmA"}

结果
{
    "create_at": 1583823184,
    "expire_in": 1586415184,
    "scope": "AdminScope",
    "uid": 6
}

九:仓库的创建方法

git config --global user.name "andy"
git config --global user.email "15837699890@139.com"

mkdir flask_rest_framework
cd flask_rest_framework
git init
touch README.md
git add README.md
git commit -m "first commit"
git remote add origin git@gitee.com:ouyangguoyong/flask_rest_framework.git
git push -u origin master