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 关于用户的思考
-
对于API而言,再叫做用户就不太合适 ,我们更倾向于把人,第三方的产品等同于成为客户端(client)来代替User。
-
客户端的种类非常多,注册的形式就非常多。如对于普通的用户而言,就是账号和密码,但是账号和密码又可以分成,短信,邮件,社交用户。对于多种的注册形式,也不是所有的都需要密码,如小程序就不需要。
-
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