├── chapter1.md ├── 1. 起步与红图 ├── README.md ├── 1.1 初始化项目.md └── 1.2 蓝图的缺陷.md ├── 7. 权限控制 ├── README.md ├── 7.2 权限管理方案.md ├── 7.3 Scope权限管理的实现.md ├── 7.1 删除模型的注意事项.md └── 7.4 Scope优化.md ├── 3. 自定义异常对象 ├── README.md ├── 3.1 关于用户的思考.md ├── 3.2 构建client验证器.md └── 3.3 重构代码-自定义验证对象.md ├── 4. 理解WTForms并灵活改造她 ├── README.md ├── 4.2 全局异常处理.md └── 4.1 重写WTForms.md ├── 5. Token与HTTPBasic验证 ├── README.md ├── 5.2 HTTPBasicAuth.md ├── 5.3 Token的发送与验证.md └── 5.1 Token.md ├── README.md ├── .DS_Store ├── .gitignore ├── 2. REST基本特征 └── README.md ├── SUMMARY.md └── 6. 模型对象的序列化.md /chapter1.md: -------------------------------------------------------------------------------- 1 | # First Chapter 2 | -------------------------------------------------------------------------------- /1. 起步与红图/README.md: -------------------------------------------------------------------------------- 1 | # 1. 起步与红图 2 | 3 | -------------------------------------------------------------------------------- /7. 权限控制/README.md: -------------------------------------------------------------------------------- 1 | # 7. 权限控制 2 | 3 | -------------------------------------------------------------------------------- /3. 自定义异常对象/README.md: -------------------------------------------------------------------------------- 1 | # 3. 自定义异常对象 2 | 3 | -------------------------------------------------------------------------------- /4. 理解WTForms并灵活改造她/README.md: -------------------------------------------------------------------------------- 1 | # 4. 理解WTForms并灵活改造她 2 | 3 | -------------------------------------------------------------------------------- /5. Token与HTTPBasic验证 /README.md: -------------------------------------------------------------------------------- 1 | # 5. Token与HTTPBasic验证 —— 用令牌来管理用户 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Initial page 2 | 3 | 慕课网-Python Flask构建可扩展的RESTful API-笔记 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGao11520/python-flask-restful-api-book/HEAD/.DS_Store -------------------------------------------------------------------------------- /5. Token与HTTPBasic验证 /5.2 HTTPBasicAuth.md: -------------------------------------------------------------------------------- 1 | # 5.2 HTTPBasicAuth 2 | 3 | ### 1.HTTPBasicAuth基本原理 4 | 除了自定义发送账号和密码之外,HTTP这种协议本身就有多种规范,来允许我们来传递账号和密码。其中一种就是HTTPBasic 5 | 6 | HTTPBasic:需要在HTTP请求的头部设置一个固定的键值对key=Authorization,value=basic base64(account:psd) 7 | 8 | ### 2.以BasicAuth方式来发送token 9 | 我们可以将token作为上面所说的账号account,而密码psd传递空值 10 | 11 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-b8865624102b86f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-923d0fec7d96b390.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | _book/ 28 | book.pdf 29 | book.epub 30 | book.mobi 31 | -------------------------------------------------------------------------------- /3. 自定义异常对象/3.1 关于用户的思考.md: -------------------------------------------------------------------------------- 1 | # 3.1 关于用户的思考 2 | 3 | > 不管是网站也好,还是API也好,我们都逃脱不了用户这个概念,我们下面就要来讨论用户的相关操作 4 | 5 | >对于用户而言,第一件事情,我们就要完成用户注册的操作,说到注册用户,我们想到,可以在视图函数文件中增加一个注册用户的视图函数--```create_user```,并且我们会在其中接受账号和密码,来完成用户的注册,这个逻辑是通常意义上的用户的概念。 6 | 7 | >普通用户:使用鱼书的人相对于鱼书来说,就是用户;我们相对于QQ和微信,也是他的用户。 8 | 9 | >但是我们在做API的时候,不能只考虑这些普通意义的用户,我们还要考虑一些特别的用户。例如:我们开发了一个向外提供数据的API,加入有一天,有一个公司,想使用我们的API开发他们自己的产品(小程序或者APP),这些其他的客户端,都是我们API的用户 10 | 11 | 12 | ### 根据以上的分析,我们可以得出几个结论: 13 | 1. 对于API而言,再叫做用户就不太合适 ,我们更倾向于把人,第三方的产品等同于成为客户端(client)来代替User。 14 | 2. 客户端的种类非常多,注册的形式就非常多。如对于普通的用户而言,就是账号和密码,但是账号和密码又可以分成,短信,邮件,社交用户。对于多种的注册形式,也不是所有的都需要密码,如小程序就不需要。 15 | 3. API和普通的业务系统是不一样的,他具有开发性和通用性。 16 | 17 | 18 | 因为注册的形式就非常多,所以我们不可能用万能的方式来解决。如果我们不能很好的处理多种多样的形式,我们的代码就会非常的杂乱 -------------------------------------------------------------------------------- /1. 起步与红图/1.1 初始化项目.md: -------------------------------------------------------------------------------- 1 | # 1.1 初始化项目 2 | 3 | ### 1.一个项目的初始化流程如下: 4 | 5 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-56f810c0e55dde7a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 6 | 7 | 8 | 9 | ### 2.新建入口文件 10 | app/app.py 11 | 12 | ```python 13 | from flask import Flask 14 | 15 | __author__ = "gaowenfeng" 16 | 17 | 18 | def create_app(): 19 | app = Flask(__name__) 20 | app.config.from_object('app.config.secure') 21 | app.config.from_object('app.config.setting') 22 | 23 | return app 24 | ``` 25 | 26 | ginger.py 27 | ```python 28 | from app.app import create_app 29 | 30 | __author__ = "gaowenfeng" 31 | 32 | app = create_app() 33 | 34 | if __name__ == '__main__': 35 | app.run(debug=True) 36 | ``` -------------------------------------------------------------------------------- /4. 理解WTForms并灵活改造她/4.2 全局异常处理.md: -------------------------------------------------------------------------------- 1 | # 4.2 全局异常处理 2 | 3 | 当系统抛出不是我们自己定义的APIException的时候,返回的结果仍然会变成一个HTML文本。 4 | 5 | 我们在写代码的过程中,有那么类型的异常: 6 | 1.已知异常:我们可以预知的。如枚举转换的时候抛出的异常,这时候我们就会提前使用try-except进行处理。也可以抛出APIException 7 | 2.未知异常:完全没有预料到的。会由框架抛出的内置异常 8 | 9 | 我们可以使用flask给我们提供的处理全局异常的装饰器,采用AOP的设计思想,捕捉所有类型的异常。 10 | 11 | ```python 12 | @app.errorhandler(Exception) 13 | def framework_error(e): 14 | if isinstance(e, APIException): 15 | return e 16 | if isinstance(e, HTTPException): 17 | code = e.code 18 | msg = e.description 19 | error_code = 1007 20 | return APIException(msg, code, error_code) 21 | else: 22 | # TODO log 23 | if not app.config['DEBUG']: 24 | return ServerError() 25 | else: 26 | raise e 27 | ``` -------------------------------------------------------------------------------- /2. REST基本特征/README.md: -------------------------------------------------------------------------------- 1 | # 2. REST基本特征 2 | 3 | ### 1.REST的最基本特征 4 | 5 | 我们把服务器提供的服务统一称为资源。 6 | 我们可以使用URL来定位资源,如/v1/book/user/1 来定位一个用户 7 | 定位到资源以后,可以使用HTPP动词来操作资源,类似使用DDL操作数据库。 8 | 9 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-3c2a7daa6f0cb9c4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 对于视图函数的URL,尽量不应该包含动词,因为URL是用来定位资源的,例如我们之前的试图函数,应该这样改写 12 | ```python 13 | @api.route('', methods=['GET']) 14 | def get_book(): 15 | return 'get book' 16 | 17 | 18 | @api.route('', methods=['POST']) 19 | def create(): 20 | return 'create book' 21 | ``` 22 | 23 | ### 2.为什么标准的REST不适合内部开发 24 | REST的使用场景有两个:内部开发API,开放API。 25 | 标准的REST比较适合开放性的API。只负责提供数据,不负责业务逻辑 26 | 27 | 1. 由于内部的开发,业务逻辑非常复杂,想用简单的四个接口来标示所有的业务逻辑,基本上是不可能的 28 | 2. REST的接口粒度比较粗(返回的资源属性比较多;服务器不会负责处理数据),这样前端的开发是不太方便的 29 | 3. 标准的REST会造成HTTP请求的数量大幅度的增加 30 | 31 | 32 | ### 3.建议 33 | - 尽量遵从REST的设计风格规范 34 | - 要灵活一些,如果前端要考虑业务逻辑的话,我们就不要遵从资源的限制了,应该让API具有业务逻辑的性质 35 | - 如果前端需要几个资源合并在一起的数据,那么我们就为前端定制一个合并数据的接口 36 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [介绍](README.md) 4 | * [1. 起步与红图](1. 起步与红图/README.md) 5 | * [1.1 初始化项目](1. 起步与红图/1.1 初始化项目.md) 6 | * [1.2 红图](1. 起步与红图/1.2 蓝图的缺陷.md) 7 | * [2. REST基本特征](2. REST基本特征/README.md) 8 | * [3. 自定义异常对象](3. 自定义异常对象/README.md) 9 | * [3.1 关于用户的思考](3. 自定义异常对象/3.1 关于用户的思考.md) 10 | * [3.2 注册client](3. 自定义异常对象/3.2 构建client验证器.md) 11 | * [3.3 重构代码-自定义验证对象](3. 自定义异常对象/3.3 重构代码-自定义验证对象.md) 12 | * [4. 理解WTForms并灵活改造她](4. 理解WTForms并灵活改造她/README.md) 13 | * [4.1 重写WTForms](4. 理解WTForms并灵活改造她/4.1 重写WTForms.md) 14 | * [4.2 全局异常处理](4. 理解WTForms并灵活改造她/4.2 全局异常处理.md) 15 | * [5. Token与HTTPBasic验证 —— 用令牌来管理用户](5. Token与HTTPBasic验证 /README.md) 16 | * [5.1 Token](5. Token与HTTPBasic验证 /5.1 Token.md) 17 | * [5.2 HTTPBasicAuth](5. Token与HTTPBasic验证 /5.2 HTTPBasicAuth.md) 18 | * [5.3 Token的发送与验证](5. Token与HTTPBasic验证 /5.3 Token的发送与验证.md) 19 | * [6. 模型对象的序列化](6. 模型对象的序列化.md) 20 | * [7. 权限控制](7. 权限控制/README.md) 21 | * [7.1 删除模型的注意事项](7. 权限控制/7.1 删除模型的注意事项.md) 22 | * [7.2 权限管理方案](7. 权限控制/7.2 权限管理方案.md) 23 | * [7.3 Scope权限管理的实现](7. 权限控制/7.3 Scope权限管理的实现.md) 24 | * [7.4 Scope优化](7. 权限控制/7.4 Scope优化.md) 25 | 26 | * [8. 实现部分鱼书小程序功能](8. 实现部分鱼书小程序功能/README.md) 27 | 28 | 29 | -------------------------------------------------------------------------------- /7. 权限控制/7.2 权限管理方案.md: -------------------------------------------------------------------------------- 1 | # 7.2 权限管理方案 2 | 3 | 4 | 通过之前的分析,我们可以发现,我们之前的get_user,实际上应该是super_get_user,而我们应该在多添加一个get_user作为普通用户的获取方法 5 | ```python 6 | @api.route('/', methods=['GET']) 7 | @auth.login_required 8 | def super_get_user(uid): 9 | user = User.query.filter_by(id=uid).first_or_404(uid) 10 | return jsonify(user) 11 | 12 | 13 | @api.route('', methods=['GET']) 14 | @auth.login_required 15 | def get_user(): 16 | uid = g.user.uid 17 | user = User.query.filter_by(id=uid).first_or_404(uid) 18 | return jsonify(user) 19 | ``` 20 | ### 1.不太好的权限管理方案 21 | 22 | 我们只要可以在视图函数中获取到用户的权限,就可以根据权限来判断,用户的身份,来做出不同的控制。 23 | 24 | 要做到这一点,我们只需要在生成令牌的时候,将is_admin的字段写入到token中。然后再视图函数中取出这个字段来进行不同的判断就好了。 25 | 26 | 这样的方案有两个缺点: 27 | 1.代码太啰嗦了,每个视图函数都需要做这样的判断。 28 | 2.我们把全新想的太简单了,我们这个项目只有管理员和普通用户两种,但是真正的权限应该有各种分组,每个分组每个用户都有不同的权限,如果这样,再在视图函数里进行控制基本上是不可能 29 | 30 | 31 | ### 2.比较好的权限管理方案 32 | 假如说我们在代码里做三张表(Mysql,Redis,配置文件),每一张表都记录着某一种权限,现在假如某一个请求过来了。当用户访问@auto.login的接口的话,他必须要带有一个token令牌中的,而我们是可以从token中读取到当前的权限种类的,并且我们是可以知道他所访问的接口的。我们可以拿权限种类和接口做匹配,然后来做判断。 33 | 这样做还有一个很好的优势,是我们可以在进入方法前进行权限判断,如果不能够访问根本就不会进入该方法。 34 | 35 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-ffa8d8564219ce87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 36 | 37 | 38 | -------------------------------------------------------------------------------- /7. 权限控制/7.3 Scope权限管理的实现.md: -------------------------------------------------------------------------------- 1 | # 7.3 Scope权限管理的实现 2 | 3 | ### 1.编码实现 4 | 5 | 根据上一小节的编写,我们来动手编写权限管理方案 6 | 7 | #### 1.1 scope配置 8 | 9 | libs/scope.py 10 | ```python 11 | class AdminScope: 12 | allow_api = ['v1.super_get_user'] 13 | 14 | 15 | 16 | class UserScope: 17 | allow_api = ['v1.get_user'] 18 | 19 | 20 | # 判断当前访问的endpoint是否在scope中 21 | def is_in_scope(scope, endpoint): 22 | # 反射获取类 23 | scope = globals()[scope]() 24 | return endpoint in scope.allow_api 25 | ``` 26 | #### 1.2 生成令牌 27 | 28 | models/user.py 29 | ```python 30 | @staticmethod 31 | def verify(email, password): 32 | user = User.query.filter_by(email=email).first_or_404() 33 | if not user.check_password(password): 34 | raise AuthFailed() 35 | # 根据权限码转换成对应的Scope 36 | scope = 'AdminScope' if user.auth == 2 else 'UserScope' 37 | return {'uid': user.id, 'scope': scope} 38 | ``` 39 | 40 | api/v1/token.py 41 | ```python 42 | def generator_auth_token(uid, ac_type, scope=None, 43 | expiration=7200): 44 | """生成令牌""" 45 | s = Serializer(current_app.config['SECRET_KEY'], 46 | expires_in=expiration) 47 | return s.dumps({ 48 | 'uid': uid, 49 | 'type': ac_type.value, 50 | # 将scope写入token 51 | 'scope': scope 52 | }) 53 | ``` 54 | 55 | #### 1.3 验证令牌 56 | ```python 57 | def verify_auth_token(token): 58 | s = Serializer(current_app.config['SECRET_KEY']) 59 | try: 60 | data = s.loads(token) 61 | except BadSignature: 62 | raise AuthFailed(msg='token is valid', error_code=1002) 63 | except SignatureExpired: 64 | raise AuthFailed(msg='token is expired', error_code=1003) 65 | uid = data['uid'] 66 | ac_type = data['type'] 67 | scope = data['scope'] 68 | 69 | # 判断是否有权限,如果没有,抛出异常 70 | allow = is_in_scope(scope, request.endpoint) 71 | if not allow: 72 | raise Forbidden() 73 | return User(uid, ac_type, scope) 74 | ``` -------------------------------------------------------------------------------- /7. 权限控制/7.1 删除模型的注意事项.md: -------------------------------------------------------------------------------- 1 | # 7.1 删除模型的注意事项 2 | 3 | 1.204 的HTTP状态码代表的是NO CONTENT,无内容。所以如果状态码是204,那么无论返回什么,前端都接受不到,但是我们要尽量返回格式化的信息,让前端能够判断,为此,我们可以使用状态码202,并且添加一个特殊的error_code=-1 来代表删除操作 4 | 5 | 6 | 2.由于我们的删除是逻辑删除,使用get的方法会一直可以查询出当前用户,这里我们应该使用filter_by(),传入status=1,好在,我们之前已经在基类重写了filter_by(),所以我们只需要调用filter_by()传入id即可 7 | 8 | 9 | ```python 10 | @api.route('/', methods=['DELETE']) 11 | @auth.login_required 12 | def delete_user(uid): 13 | with db.auto_commit(): 14 | user = User.query.filter_by(id=uid).first_or_404() 15 | user.delete() 16 | return DeleteSuccess() 17 | ``` 18 | 19 | ```python 20 | class DeleteSuccess(Success): 21 | code = 202 22 | error_code = -1 23 | ``` 24 | 25 | 3.防止超权现象 26 | id=1的用户,不能删除id=2的用户,为了解决这个问题,我们的uid不能由用户传入,而是应该从他传入的token中取出来。由于我们之前做token验证的时候,已经把取出来的信息存入到了flask的g中,所以我们只需要从g中取出来做判断即可 27 | ```python 28 | @api.route('', methods=['DELETE']) 29 | @auth.login_required 30 | def delete_user(): 31 | uid = g.user.uid 32 | with db.auto_commit(): 33 | user = User.query.filter_by(id=uid).first_or_404() 34 | user.delete() 35 | return DeleteSuccess() 36 | ``` 37 | 38 | > 两个知识点 39 | 1.g.user.uid之所以可以这样用.的方式获取uid,是因为我们在向g中存储user的时候,使用的是namedtuple,而不是dict,不然我们就只能g.user['uid']这样获取了 40 | 2.即使两个用户同时访问这个接口,我们也不会出错,g会正确的指向每一个请求的user,这是因为g是线程隔离的 41 | 42 | 43 | 44 | 4.我们是需要一个超级管理员用户的试图函数super_delete_user,可以通过传入uid来删除指定用户的。但是对这两个接口,普通用户应该只能访问delete_user,而超级管理员都能够访问。 45 | 46 | 首先我们需要创建一个管理员用户,不过管理员用户不能通过公开API来创建,而应该直接在数据库里创建,但是这又涉及到一个问题,就是直接在数据库里创建,密码不好生成。所以最好的方式是创建一个离线脚本文件 47 | ```python 48 | from app import create_app 49 | from app.models.base import db 50 | from app.models.user import User 51 | 52 | __author__ = "gaowenfeng" 53 | 54 | app = create_app() 55 | with app.app_context(): 56 | with db.auto_commit(): 57 | user = User() 58 | user.nickname = 'super' 59 | user.password = '123456' 60 | user.email = '999@qq.com' 61 | user.auth = 2 62 | db.session.add(user) 63 | ``` 64 | 65 | 这个脚本不仅仅可以生成管理员,还可以使用它生成大量的假数据,测试数据 -------------------------------------------------------------------------------- /4. 理解WTForms并灵活改造她/4.1 重写WTForms.md: -------------------------------------------------------------------------------- 1 | # 4.1 重写WTForms 2 | 3 | ### 优化1 4 | 之前的代码,修改完成之后,已经修复了之前的缺陷,但是这样爆出了两个问题: 5 | 1.代码太啰嗦了,每个试图函数里,都需要这么写 6 | 2.ClientTypeError只是代表客户端类型异常,其他的参数校验不通过也抛出这个异常的话不合适 7 | 8 | 为了解决上面的问题,我们需要重写wtforms 9 | 10 | 定义一个自定义BaseForm,让其他的Form来继承 11 | 12 | ```python 13 | 14 | class BaseForm(Form): 15 | def __init__(self, data): 16 | super(BaseForm, self).__init__(data=data) 17 | 18 | def validate_for_api(self): 19 | valid = super(BaseForm, self).validate() 20 | if not valid: 21 | raise ParameterException(msg=self.errors) 22 | ``` 23 | 24 | 以后我们的试图函数就可以这样编写 25 | ```python 26 | @api.route('/register', methods=['POST']) 27 | def create_client(): 28 | data = request.json 29 | form = ClientForm(data=data) 30 | 31 | form.validate_for_api() 32 | promise = { 33 | ClientTypeEnum.USER_EMAIL: __register_user_by_email 34 | } 35 | promise[form.type.data]() 36 | 37 | return 'success' 38 | ``` 39 | 40 | 41 | ### 优化2 42 | 43 | 目前我们每次都需要从request中取出json信息再传入到Form对象中,优化的思路是,直接传入request,在BaseForm中取出json 44 | 45 | ### 优化3 46 | 47 | 每次都需要实例化Form对象,再调用validate_for_api()方法,我们可以让validate_for_api方法返回一个self对象,这样就只需要一行代码就可以解决了 48 | 49 | ```python 50 | class BaseForm(Form): 51 | def __init__(self, request): 52 | # 优化2 53 | data = request.json 54 | super(BaseForm, self).__init__(data=data) 55 | 56 | def validate_for_api(self): 57 | valid = super(BaseForm, self).validate() 58 | if not valid: 59 | raise ParameterException(msg=self.errors) 60 | # 优化3 61 | return self 62 | ``` 63 | 64 | ### 优化4 65 | 操作成功也需要返回json结构,且结构应该和异常的时候一样,所以我们可以定义一个Success继承APIException 66 | 67 | ```python 68 | class Success(APIException): 69 | code = 201 70 | msg = 'ok' 71 | error_code = 0 72 | ``` 73 | 74 | 视图函数 75 | ```python 76 | @api.route('/register', methods=['POST']) 77 | def create_client(): 78 | form = ClientForm(request).validate_for_api() 79 | promise = { 80 | ClientTypeEnum.USER_EMAIL: __register_user_by_email 81 | } 82 | promise[form.type.data]() 83 | 84 | return Success() 85 | ``` 86 | 87 | 我们可以接受定义时候的复杂,但是不能够接受调用的时候复杂 88 | 89 | 定义是一次性的,但是调用是多次的,如果调用太过于复杂,会使得我们的 代码太过于臃肿 -------------------------------------------------------------------------------- /5. Token与HTTPBasic验证 /5.3 Token的发送与验证.md: -------------------------------------------------------------------------------- 1 | # 5.3 Token的发送与验证 2 | 3 | ### 1.验证token 4 | ```python 5 | auth = HTTPBasicAuth() 6 | User = namedtuple('User', ['uid', 'ac_type', 'scope']) 7 | 8 | @auth.verify_password 9 | def verify_password(token, password): 10 | user_info = verify_auth_token(token) 11 | if not user_info: 12 | return False 13 | else: 14 | g.user = user_info 15 | 16 | return True 17 | 18 | 19 | def verify_auth_token(token): 20 | s = Serializer(current_app.config['SECRET_KEY']) 21 | try: 22 | data = s.loads(token) 23 | # token不合法抛出的异常 24 | except BadSignature: 25 | raise AuthFailed(msg='token is valid', error_code=1002) 26 | # token过期抛出的异常 27 | except SignatureExpired: 28 | raise AuthFailed(msg='token is expired', error_code=1003) 29 | uid = data['uid'] 30 | ac_type = data['type'] 31 | return User(uid, ac_type, '') 32 | ``` 33 | 34 | ### 2.视图函数的编写 35 | ```python 36 | @api.route('/', methods=['GET']) 37 | @auth.login_required 38 | def get_user(uid): 39 | user = User.query.get_or_404(uid) 40 | r = { 41 | 'nickname': user.nickname, 42 | 'email': user.email 43 | } 44 | return jsonify(r), 200 45 | ``` 46 | 47 | ### 3.重写后的get_or_404,抛出自定义异常 48 | ```python 49 | def get_or_404(self, ident): 50 | rv = self.get(ident) 51 | if not rv: 52 | raise NotFound() 53 | return rv 54 | 55 | def first_or_404(self): 56 | rv = self.first() 57 | if not rv: 58 | raise NotFound() 59 | return rv 60 | ``` 61 | 62 | ### 4.获取令牌信息 63 | ```python 64 | @api.route('/secret', methods=['POST']) 65 | def get_token_info(): 66 | """获取令牌信息""" 67 | form = TokenForm().validate_for_api() 68 | s = Serializer(current_app.config['SECRET_KEY']) 69 | try: 70 | data = s.loads(form.token.data, return_header=True) 71 | except SignatureExpired: 72 | raise AuthFailed(msg='token is expired', error_code=1003) 73 | except BadSignature: 74 | raise AuthFailed(msg='token is invalid', error_code=1002) 75 | 76 | r = { 77 | 'scope': data[0]['scope'], 78 | 'create_at': data[1]['iat'], 79 | 'expire_in': data[1]['exp'], 80 | 'uid': data[0]['uid'] 81 | } 82 | return jsonify(r) 83 | ``` -------------------------------------------------------------------------------- /5. Token与HTTPBasic验证 /5.1 Token.md: -------------------------------------------------------------------------------- 1 | # 5.1 Token 2 | 3 | ### 1.Token概述 4 | 5 | 以下是网站登录和使用API登录的区别 6 | 7 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-bd0b117b4d96a154.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 8 | 9 | 与网站登录不同的是,网站登录将登录信息写入cookie存储在浏览器,而API只负责生成token发送给客户端,而客户端怎么存储有自己决定。 10 | - Token具有有效期 11 | - Token可以标示用户身份,如存储用户id 12 | 13 | ### 2.获取Token令牌 14 | 密码校验--models/user.py 15 | ```python 16 | @staticmethod 17 | def verify(email, password): 18 | user = User.query.filter_by(email=email).first() 19 | if not user: 20 | raise NotFound('user not found') 21 | if not user.check_password(password): 22 | raise AuthFailed() 23 | return {'uid': user.id} 24 | 25 | def check_password(self, raw): 26 | if not self._password: 27 | return False 28 | return check_password_hash(self._password, raw) 29 | ``` 30 | 31 | 返回token的试图函数,这里稍微破坏一下REST的规则,由于登录操作密码安全性较高,使用GET的话会泄漏 32 | ```python 33 | @api.route('', methods=['POST']) 34 | def get_token(): 35 | form = ClientForm(request).validate_for_api() 36 | promise = { 37 | ClientTypeEnum.USER_EMAIL: User.verify, 38 | } 39 | identity = promise[form.type.data]( 40 | form.account.data, 41 | form.secret.data 42 | ) 43 | expiration = current_app.config['TOKEN_EXPIRATION'] 44 | token = generator_auth_token(identity['uid'], 45 | form.type.data, 46 | None, 47 | expiration=expiration) 48 | t = { 49 | 'token': token.decode('utf-8') 50 | } 51 | return jsonify(t), 201 52 | 53 | 54 | def generator_auth_token(uid, ac_type, scope=None, 55 | expiration=7200): 56 | """生成令牌""" 57 | s = Serializer(current_app.config['SECRET_KEY'], 58 | expires_in=expiration) 59 | return s.dumps({ 60 | 'uid': uid, 61 | 'type': ac_type.value 62 | }) 63 | ``` 64 | 65 | ### 3.Token的用处 66 | 我们不可能让任何一个用户都来访问我们获取用户资料的接口,必须对这个加以控制,也就是说只有确定了身份的用户可以访问我们的接口。 67 | 68 | 如何对这个接口做保护呢? 69 | 70 | 当用户访问问的接口的时候,我们需要获取他传来的token并进行解析验证,只有token是合法的且没有过期,我们才允许访问。 71 | 72 | 73 | 由于每个需要验证token的试图函数都需要上面的业务逻辑,所以我们可以编写一个装饰器,以面向切面的方式统一处理,编写一个函数验证token,如果验证通过,我们就继续执行试图函数的方法,如果不通过,我们就返回一个自定义异常。 74 | 75 | libs/token_auth.py 76 | ```python 77 | from flask_httpauth import HTTPBasicAuth 78 | 79 | __author__ = "gaowenfeng" 80 | 81 | auth = HTTPBasicAuth() 82 | 83 | 84 | @auth.verify_password 85 | def verify_password(account, password): 86 | return False 87 | ``` 88 | 89 | ```python 90 | @api.route('/get') 91 | @auth.login_required 92 | def get_user(): 93 | return 'i am gwf' 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /3. 自定义异常对象/3.2 构建client验证器.md: -------------------------------------------------------------------------------- 1 | # 3.2 注册client 2 | 3 | 对于登录/注册这些比较重要的接口,我们建议提供一个统一的调用接口,而不应该拆分成多个。 4 | 5 | 我们可以编写一个枚举类,来枚举所有的客户端类型。 6 | 7 | 8 | ### 1.构建client验证器 9 | ```python 10 | class ClientForm(Form): 11 | account = StringField(validators=[DataRequired(), length( 12 | min=1, max=32 13 | )]) 14 | secret = StringField() 15 | type = IntegerField(validators=[DataRequired()]) 16 | 17 | # 验证client_type 18 | def validate_type(self, value): 19 | try: 20 | # 将用户传来的参数去枚举类中匹配,如果匹配失败,则抛出异常 21 | # 如果匹配成功则将int转换成枚举 22 | client = ClientTypeEnum(value.data) 23 | except ValueError as e: 24 | raise e 25 | ``` 26 | 27 | 28 | ### 2.处理不同客户端注册的方案 29 | 由于python没有switch-case,我们可以使用dict来替换 30 | ```python 31 | @api.route('/register') 32 | def create_client(): 33 | # request.json 34 | 用来接收json类型的参数 35 | data = request.json 36 | # 关键字参数data是wtform中用来接收json参数的方法 37 | form = ClientForm(data=data) 38 | 39 | if form.validate(): 40 | # 替代switchcase-{Enum_name:handle_func} 41 | promise = { 42 | ClientTypeEnum.USER_EMAIL: __register_user_by_email 43 | } 44 | ``` 45 | 46 | ### 3.用户模型的设计 47 | ```python 48 | class User(Base): 49 | id = Column(Integer, primary_key=True) 50 | email = Column(String(50), unique=True, nullable=False) 51 | auth = Column(SmallInteger, default=1) 52 | nickname = Column(String(24), nullable=False) 53 | _password = Column('password', String(128)) 54 | 55 | @property 56 | def password(self): 57 | return self._password 58 | 59 | @password.setter 60 | def password(self, raw): 61 | self._password = generate_password_hash(raw) 62 | 63 | # 从面向对象的角度考虑,在一个对象中创建一个对象本身这个是不合理的。 64 | # 但是如果将他声明为一个静态方法,那么就是合理的 65 | @staticmethod 66 | def register_by_email(nikename, account, secert): 67 | with db.auto_commit(): 68 | user = User() 69 | user.nickname = nikename 70 | user.email = account 71 | user.password = secert 72 | db.session.add(user) 73 | ``` 74 | 75 | ### 4.完成客户端注册 76 | 之前我们的ClientForm并没有nickname,但是注册email用户的时候是需要的,所以我们建立一个UserEmailForm继承ClientForm完成他自己的业务 77 | ```python 78 | class UserEmailForm(ClientForm): 79 | account = StringField(validators=[ 80 | Email(message='validate email') 81 | ]) 82 | secret = StringField(validators=[ 83 | DataRequired(), 84 | Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$') 85 | ]) 86 | nickname = StringField(validators=[DataRequired(), 87 | length(min=2, max=22)]) 88 | 89 | def validate_account(self, value): 90 | if User.query.filter_by(email=value.data).first(): 91 | raise ValidationError() 92 | ``` 93 | 94 | 完成视图函数的编写 95 | ```python 96 | 97 | @api.route('/register') 98 | def create_client(): 99 | data = request.json 100 | form = ClientForm(data=data) 101 | 102 | if form.validate(): 103 | promise = { 104 | ClientTypeEnum.USER_EMAIL: __register_user_by_email 105 | } 106 | promise[form.type.data]() 107 | pass 108 | 109 | 110 | def __register_user_by_email(): 111 | form = UserEmailForm(data=request.json) 112 | if form.validate(): 113 | User.register_by_email(form.nickname.data, 114 | form.account.data, 115 | form.secret.data) 116 | ``` 117 | 118 | create_client和__register_user_by_email是一个总-分的关系,客户端注册的种类是比较多的,但是这些众多的种类又有一些共通的东西,比如处理客户端的type的值,就是所有的客户端都要携带的参数。对于这些共有的参数,我们就统一在create_client,ClientForm中进行处理 119 | 对于不同的客户端的特色的属性和功能,我们放在“分”里面来,比如email的nikename -------------------------------------------------------------------------------- /1. 起步与红图/1.2 蓝图的缺陷.md: -------------------------------------------------------------------------------- 1 | # 1.2 红图 2 | 3 | ### 1.蓝图拆分视图函数的缺陷的缺陷 4 | 5 | 1.蓝图的作用并不是用来拆分视图函数的,而是用来拆分模块的 6 | 2.使用蓝图,统一个业务模型的试图函数的前缀都一样,代码重复啰嗦 7 | 8 | ### 2.打开思维,创建自己的redprint-红图 9 | 10 | 为了解决上面的两个问题,我们可以模仿蓝图,构建一个自定义的对象-红图,红图的定位是用来拆分视图,也就是视图函数层 11 | 12 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-26c3213bb5c70767.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 13 | 14 | 我们采用自顶向下的编程思想,先编写redprint在试图函数中的使用代码,再编写redprint具体的实现 15 | 16 | #### 2.1 视图函数向红图的注册 17 | 18 | app/api/v1/book.py 19 | 20 | ```python 21 | from app.libs.redprint import RedPrint 22 | 23 | __author__ = "gaowenfeng" 24 | 25 | api = RedPrint('book') 26 | 27 | 28 | @api.route('/get') 29 | def get_book(): 30 | return 'get book' 31 | 32 | @api.route('/create') 33 | def create(): 34 | return 'create book' 35 | 36 | ``` 37 | 38 | app/api/v1/user.py 39 | ```python 40 | from app.libs.redprint import RedPrint 41 | 42 | __author__ = "gaowenfeng" 43 | 44 | api = RedPrint('user') 45 | 46 | 47 | @api.route('/get') 48 | def get_user(): 49 | return 'i am gwf' 50 | ``` 51 | 52 | #### 2.2 红图向蓝图的注册 53 | 54 | app/api/\_\_init__.py 55 | ``` 56 | from flask import Blueprint 57 | from app.api.v1 import book, user 58 | 59 | __author__ = "gaowenfeng" 60 | 61 | 62 | def create_blueprint_v1(): 63 | bp_v1 = Blueprint('v1', __name__) 64 | 65 | # 假设api有register的方法,后面再实现, url_prefix解决前缀问题 66 | book.api.register(bp_v1, url_prefix='/book') 67 | user.api.register(bp_v1, url_prefix='/user') 68 | return bp_v1 69 | ``` 70 | 71 | #### 2.3 蓝图向Flask核心对象的注册 72 | app/app.py 73 | ```python 74 | from flask import Flask 75 | 76 | __author__ = "gaowenfeng" 77 | 78 | 79 | def register_blueprint(app): 80 | from app.api.v1 import create_blueprint_v1 81 | # url_prefix定义url前缀 82 | app.register_blueprint(create_blueprint_v1(), url_prefix='/v1') 83 | 84 | 85 | def create_app(): 86 | app = Flask(__name__) 87 | app.config.from_object('app.config.secure') 88 | app.config.from_object('app.config.setting') 89 | 90 | register_blueprint(app) 91 | return app 92 | ``` 93 | 94 | ### 3.实现Redprint 95 | 96 | 因为我们的红图的作用就是要代替蓝图来实现试图函数的拆分,所以功能实现上可以参考蓝图的实现。 97 | 98 | #### 3.1 装饰性route的实现 99 | 蓝图的实现 100 | ```python 101 | def route(self, rule, **options): 102 | """Like :meth:`Flask.route` but for a blueprint. The endpoint for the 103 | :func:`url_for` function is prefixed with the name of the blueprint. 104 | """ 105 | def decorator(f): 106 | # 获取endpoint,默认为试图函数名 107 | endpoint = options.pop("endpoint", f.__name__) 108 | # 注册视图函数 109 | self.add_url_rule(rule, endpoint, f, **options) 110 | return f 111 | return decorator 112 | ``` 113 | 114 | 红图的实现可以模仿蓝图的实现结构 ,由于红图的route里没有办法拿到蓝图的对象,所以我们可以先把他们存储起来,等碰到的时候再进行注册 115 | ```python 116 | class Redprint: 117 | 118 | def __init__(self, name): 119 | self.name = name 120 | self.mound = [] 121 | 122 | def route(self, rule, **options): 123 | def decorator(f): 124 | self.mound.append((f, rule, options)) 125 | return f 126 | 127 | return decorator 128 | ``` 129 | 130 | #### 3.2 register方法 131 | 在register方法中可以获取到蓝图对象,所以之前route中视图函数的注册延迟到这里进行 132 | ```python 133 | def register(self, bp, url_prefix=None): 134 | # 如果不传url_prefix 则默认使用name 135 | if url_prefix is None: 136 | url_prefix = '/'+self.name 137 | # python的自动拆包 138 | for f, rule, options in self.mound: 139 | endpoint = options.pop("endpoint", f.__name__) 140 | # 将视图函数注册到蓝图上来 141 | bp.add_url_rule(url_prefix + rule, endpoint, f, **options) 142 | ``` -------------------------------------------------------------------------------- /3. 自定义异常对象/3.3 重构代码-自定义验证对象.md: -------------------------------------------------------------------------------- 1 | # 3.3 重构代码-自定义验证对象 2 | 3 | 我们之前写的代码,有一些细节问题。 4 | 5 | ### 1.传入错误的参数,虽然没有添加到数据库,但是返回 结果显示正常 6 | 7 | 这是因为,form.validate()如果校验不通过,他不会抛出异常,而是会将异常信息存储在form对象中。 8 | 所以这个时候我们应该判断如果校验不通过,就抛出一个自定义的异常。 9 | 10 | 11 | werkzeug为我们提供的大量的异常,都继承自HTTPException,但是这些异常都很具体,不能为我们所用。不过我们可以自己定义一个异常来继承HTTPException 12 | 13 | 14 | ### 2.自定义异常 15 | 16 | #### rest中状态码代表的意义 17 | - 400 参数错误 18 | - 401 未授权 19 | - 403 禁止访问 20 | - 404 没有找到资源或者页面 21 | - 500 服务器未知错误 22 | - 200 查询成功 23 | - 201 更新/创建成功 24 | - 204 删除成功 25 | - 301/302 重定向 26 | 27 | ```python 28 | class ClientTypeError(HTTPException): 29 | code = 400 30 | 31 | description = ( 32 | 'client is invalid' 33 | ) 34 | ``` 35 | 36 | 修改后的试图函数 37 | ```python 38 | @api.route('/register', methods=['POST']) 39 | def create_client(): 40 | data = request.json 41 | form = ClientForm(data=data) 42 | 43 | if form.validate(): 44 | promise = { 45 | ClientTypeEnum.USER_EMAIL: __register_user_by_email 46 | } 47 | promise[form.type.data]() 48 | else: 49 | raise ClientTypeError() 50 | return 'success' 51 | ``` 52 | 53 | 修改完成之后,已经修复了之前的缺陷,但是这样爆出了两个问题: 54 | 1.代码太啰嗦了,每个试图函数里,都需要这么写 55 | 2.ClientTypeError只是代表客户端类型异常,其他的参数校验不通过也抛出这个异常的话不合适 56 | 57 | 58 | ### 2.异常返回的标准与重要性 59 | 我们的restapi返回的信息主要分为以下三类: 60 | 1.页数数据信息 61 | 2.操作成功提示信息 62 | 3.错误异常信息 63 | 64 | 如果错误异常信息不够标准,那么客户端很难去处理我们的错误异常。 65 | 66 | 无论上面三种,都属于输出,REST-API要求输入输出都要返回JSON 67 | 68 | 69 | ### 3.自定义ApiException 70 | 71 | 通过分析HttpException的get_body,get_header源码我们可以知道,这两个方法分别组成了默认异常页面的header和html文本,所以如果要让我们的异常返回json格式的信息,需要继承HttpException并重写这两个方法 72 | HttpException 73 | ```python 74 | class HTTPException(Exception): 75 | 76 | """ 77 | Baseclass for all HTTP exceptions. This exception can be called as WSGI 78 | application to render a default error page or you can catch the subclasses 79 | of it independently and render nicer error messages. 80 | """ 81 | 82 | code = None 83 | description = None 84 | 85 | def __init__(self, description=None, response=None): 86 | Exception.__init__(self) 87 | if description is not None: 88 | self.description = description 89 | self.response = response 90 | 91 | def get_body(self, environ=None): 92 | """Get the HTML body.""" 93 | return text_type(( 94 | u'\n' 95 | u'%(code)s %(name)s\n' 96 | u'

%(name)s

\n' 97 | u'%(description)s\n' 98 | ) % { 99 | 'code': self.code, 100 | 'name': escape(self.name), 101 | 'description': self.get_description(environ) 102 | }) 103 | 104 | def get_headers(self, environ=None): 105 | """Get a list of headers.""" 106 | return [('Content-Type', 'text/html')] 107 | ``` 108 | 109 | 110 | APIException 111 | ```python 112 | class APIException(HTTPException): 113 | code = 500 114 | error_code = 999 115 | msg = 'sorry, we make a mistake' 116 | 117 | def __init__(self, msg=None, code=None, error_code=None, 118 | headers=None): 119 | if code: 120 | self.code = code 121 | if error_code: 122 | self.error_code = error_code 123 | if msg: 124 | self.msg = msg 125 | super(APIException, self).__init__(self.msg, None) 126 | 127 | def get_body(self, environ=None): 128 | body = dict( 129 | msg=self.msg, 130 | error_code=self.error_code, 131 | request=request.method+' '+self.get_url_no_param() 132 | ) 133 | text = json.dumps(body) 134 | return text 135 | 136 | def get_headers(self, environ=None): 137 | return [('Content-Type', 'application/json')] 138 | 139 | @staticmethod 140 | def get_url_no_param(): 141 | full_path = request.full_path 142 | main_path = full_path.split('?') 143 | return main_path[0] 144 | 145 | ``` 146 | -------------------------------------------------------------------------------- /7. 权限控制/7.4 Scope优化.md: -------------------------------------------------------------------------------- 1 | # 7.4 Scope优化 2 | 3 | #### 1.支持权限相加 4 | 5 | 假如我们的UserScope的权限是A,B,C。而AdminScope的权限是A,B,C,D。按照我们的写法,我们的A,B,C就需要些两遍。况且这只是一个简单的例子,实际情况下会更复杂。所以我们需要实现一种方法,可以让AdminScope的allow_api可以和UserScope的allow_api相加得到新的allow_api。 6 | 7 | ```python 8 | class AdminScope: 9 | allow_api = ['v1.super_get_user'] 10 | 11 | def __init__(self): 12 | self.add(UserScope()) 13 | 14 | 15 | # 这个方法可以将其他的Scope合并到当前Scope。省去重复代码的编写 16 | def add(self, other): 17 | self.allow_api = self.allow_api + other.allow_api 18 | ``` 19 | 20 | #### 2.支持权限链式相加 21 | 22 | 现在我们只能讲AdminScope和UserScope的权限相加,如果还想再加上其他的Scope,就需要链式的操作 23 | ```python 24 | class SuperScope: 25 | allow_api = ['v1.super_get_user'] 26 | 27 | def __init__(self): 28 | self.add(UserScope()).add(AdminScope()) 29 | 30 | 31 | # 这个方法可以将其他的Scope合并到当前Scope。省去重复代码的编写 32 | def add(self, other): 33 | self.allow_api = self.allow_api + other.allow_api 34 | return self 35 | 36 | ``` 37 | 38 | #### 3.所有子类支持相加 39 | 40 | add方法不应该写在具体的Scope类中,因为这样就只有当前Scope类有该功能了。应该将add方法写在基类Scope中 41 | ```python 42 | class Scope: 43 | allow_api = [] 44 | 45 | def add(self, other): 46 | self.allow_api = self.allow_api + other.allow_api 47 | return self 48 | 49 | 50 | class SuperScope(Scope): 51 | allow_api = ['v1.super_get_user'] 52 | 53 | def __init__(self): 54 | self.add(UserScope()) 55 | 56 | 57 | class UserScope(Scope): 58 | allow_api = ['v1.get_user'] 59 | ``` 60 | 61 | #### 4.运算符重载 62 | 63 | 现在我们一直使用add()方法,太啰嗦了,我们可以修改我们的代码,使得我们可以使用+号来完成add()方法的功能。 64 | 要完成这个功能,就要使用到运算符重载的技术 65 | ```python 66 | class Scope: 67 | allow_api = [] 68 | 69 | def __add__(self, other): 70 | self.allow_api = self.allow_api + other.allow_api 71 | return self 72 | 73 | 74 | class SuperScope(Scope): 75 | allow_api = ['v1.D'] 76 | 77 | def __init__(self): 78 | self +AdminScope()+UserScope() 79 | 80 | class AdminScope(Scope): 81 | allow_api = ['v1.B', 'v1.C'] 82 | 83 | def __init__(self): 84 | self + (UserScope()) 85 | 86 | 87 | class UserScope(Scope): 88 | allow_api = ['v1.A'] 89 | ``` 90 | 91 | #### 5.去重 92 | 93 | 我们现在的scope,编写完成之后,由于可能会连续相加,会有很多重复的试图函数,如SuperScope()中会出现两次v1.A,现在我们就需要将这些重复的试图函数去除掉。我们只需要使用set这个数据结构,就可以完成。 94 | 95 | ```python 96 | class Scope: 97 | allow_api = [] 98 | 99 | def __add__(self, other): 100 | self.allow_api = self.allow_api | other.allow_api 101 | return self 102 | 103 | 104 | class SuperScope(Scope): 105 | allow_api = {'v1.super_get_user'} 106 | 107 | def __init__(self): 108 | self + (UserScope()) 109 | 110 | 111 | class UserScope(Scope): 112 | allow_api = {'v1.get_user'} 113 | 114 | 115 | def is_in_scope(scope, endpoint): 116 | scope = globals()[scope]() 117 | return endpoint in scope.allow_api 118 | ``` 119 | 120 | #### 6.模块级别的Scope 121 | 122 | 现在我们的Scope都是试图函数级别的,加入我们的user下面有100个试图函数,我们就需要把这100个全都加入进来,我们可以想办法,让我们的Scope支持可以添加一个模块下的试图函数。 123 | 124 | 我们可以添加一个变量,allow_moudle,来标示允许通过的模块。然后现在我们的is_in_scope只是简单的判断endpoint是否在scope.allow_api中,endpoint默认的形式是blueprint.view_func 的形式,我们可以自定义endpoint为blueprint.moudle_name+view_func这样的形式,这样我们我们就可以在is_in_scope进行模块的判断 125 | 126 | 127 | 修改红图的注册: 128 | ```python 129 | def register(self, bp, url_prefix=None): 130 | if url_prefix is None: 131 | url_prefix = '/'+self.name 132 | for f, rule, options in self.mound: 133 | # 修改endpoint的定义 134 | endpoint = self.name + '+' + options.pop("endpoint", f.__name__) 135 | bp.add_url_rule(url_prefix + rule, endpoint, f, **options) 136 | ``` 137 | 138 | scope.py 139 | ```python 140 | class Scope: 141 | allow_api = set() 142 | allow_module = set() 143 | 144 | def __add__(self, other): 145 | self.allow_api = self.allow_api | other.allow_api 146 | return self 147 | 148 | 149 | class SuperScope(Scope): 150 | allow_module = {'v1.user'} 151 | 152 | 153 | class UserScope(Scope): 154 | allow_api = {'v1.user+get_user'} 155 | 156 | 157 | def is_in_scope(scope, endpoint): 158 | scope = globals()[scope]() 159 | splits = endpoint.split('+') 160 | red_name = splits[0] 161 | return (endpoint in scope.allow_api) or \ 162 | (red_name in scope.allow_module) 163 | ``` 164 | 165 | #### 7.支持排除 166 | 167 | 如果一个模块又100个视图函数,UserScope需要访问98个,AdminScope需要访问所有,那么UserScope的编写就太麻烦了,我们可以让我们的Scope 168 | 支持排除操作,这样UserScope就可以添加AdminScope的全部,然后再排除掉他不能访问的两个就好了 169 | 170 | ```python 171 | class Scope: 172 | allow_api = set() 173 | allow_module = set() 174 | # 支持排除 175 | forbidden = set() 176 | 177 | def __add__(self, other): 178 | self.allow_module = self.allow_module | other.allow_module 179 | self.allow_api = self.allow_api | other.allow_api 180 | self.forbidden = self.forbidden | other.forbidden 181 | return self 182 | 183 | 184 | class SuperScope(Scope): 185 | allow_module = {'v1.user'} 186 | 187 | 188 | class UserScope(Scope): 189 | forbidden = {'v1.user+super_get_user', 'v1.user+super_delete_user'} 190 | 191 | def __init__(self): 192 | self + SuperScope() 193 | 194 | 195 | def is_in_scope(scope, endpoint): 196 | scope = globals()[scope]() 197 | splits = endpoint.split('+') 198 | red_name = splits[0] 199 | # 首先判断是否在要排除的列表里 200 | if endpoint in scope.forbidden: 201 | return False 202 | return (endpoint in scope.allow_api) or \ 203 | (red_name in scope.allow_module) 204 | ``` -------------------------------------------------------------------------------- /6. 模型对象的序列化.md: -------------------------------------------------------------------------------- 1 | # 6. 模型对象的序列化 2 | 3 | ### 1.理解序列化时的default函数 4 | 5 | 我们最想做的一件事情,就是在视图函数中,读取出模型之后,还要把他的属性读出来,转换成一个字典。我们想直接```jsonfiy(user)``` 6 | 7 | 现在jsonfiy并不能直接序列化对象,所以我们的目标就是必须想办法让jsonfiy直接序列化对象。 8 | 9 | jsonfiy在序列化对象的时候,如果不知道如何序列化当前传进来的参数,就会去调用JSONEncoder类的default函数。 10 | 11 | ```python 12 | def default(self, o): 13 | """Implement this method in a subclass such that it returns a 14 | serializable object for ``o``, or calls the base implementation (to 15 | raise a :exc:`TypeError`). 16 | 17 | For example, to support arbitrary iterators, you could implement 18 | default like this:: 19 | 20 | def default(self, o): 21 | try: 22 | iterable = iter(o) 23 | except TypeError: 24 | pass 25 | else: 26 | return list(iterable) 27 | return JSONEncoder.default(self, o) 28 | """ 29 | if isinstance(o, datetime): 30 | return http_date(o.utctimetuple()) 31 | if isinstance(o, date): 32 | return http_date(o.timetuple()) 33 | if isinstance(o, uuid.UUID): 34 | return str(o) 35 | if hasattr(o, '__html__'): 36 | return text_type(o.__html__()) 37 | return _json.JSONEncoder.default(self, o) 38 | ``` 39 | 40 | 目前的default是没有提供对对象的序列化的,所以我们这里最关键的就是要重写default方法。在重写的过程中实现对对象的序列化就可以了 41 | 42 | ### 2.不完美的对象转字典 43 | 44 | 我们首先要做到的就是让Flask可以调用到我们自己定义的default函数。要做到这一点,我们需要继承JSONEncoder,然后重写defualt方法,然后继承Flask,在子类里,替换掉Flask原有的json_encoder对象。然后,是实例化Flask核心对象的时候,使用我们的子类进行实例化 45 | 46 | ```python 47 | class JSONEncoder(_JSONEncoder): 48 | 49 | def default(self, o): 50 | # 只能转换实例变量 51 | return o.__dect__ 52 | 53 | 54 | class Flask(_Flask): 55 | json_encoder = JSONEncoder() 56 | ``` 57 | 58 | 上面的写法o.\_\_dect__只能转换实例变量,不能讲类变量也转换成字典。 59 | 60 | 61 | ### 3.深入理解dict机制 62 | 63 | 在Python中创建一个dict有很多种方式: 64 | 65 | 1. 直接定义一个字典 66 | ```python 67 | r = { 68 | 'name': 'gwf' 69 | } 70 | ``` 71 | 72 | 2. 使用dict函数 73 | ```python 74 | r = dict(name='gwf') 75 | ``` 76 | 77 | 3. 将一个对象传入dict函数 78 | 值得研究的是这第三种方法,当将一个对象传入dict函数的时候,他会去调用keys函数 79 | ![image.png](https://upload-images.jianshu.io/upload_images/7220971-4a99949baa0f298e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 80 | 81 | keys 方法的目的就是为了拿到字典里所有的键,至于说这些键有那么,完全有我们自己来定义。keys 返回的必须是一个元组或者列表来声明要序列化的键。 82 | 83 | 而dict会以中括号的形式来拿到对应的值,如o["name"],但是默认是不能这么访问的,我们需要编写\_\_getitem__函数 84 | 85 | 86 | ```python 87 | class Person: 88 | name = 'gwf' 89 | age = 18 90 | 91 | def __init__(self): 92 | self.gender = 'male' 93 | 94 | def keys(self): 95 | return ('name', 'age', 'gender') 96 | 97 | def __getitem__(self, item): 98 | return getattr(self, item) 99 | 100 | 101 | o = Person() 102 | print(dict(o)) 103 | # {'name': 'gwf', 'age': 18, 'gender': 'male'} 104 | ``` 105 | 106 | 这样我们就成功的讲一个对象转化成了字典的形式,并且无论类变量和实例变量,都可以转化,更加灵活的是,我们可以自己控制,那些变量需要转化,哪些变量不需要转化 107 | 108 | > 注意: 109 | 如果我们只想序列化一个元素 110 | ``` 111 | def keys(self): 112 | return ('name') 113 | ``` 114 | 这样是不行的,因为只有一个元素的元素不是这样定义的,我们需要在后面加上一个逗号 115 | ``` 116 | def keys(self): 117 | return ('name',) 118 | ``` 119 | 120 | ### 4.序列化SQLALChemy模型 121 | 122 | 有了之前的基础,我们就知道怎么序列化user对象了,我们只需要在User类中定义keys和getitem方法,然后在default函数中使用dict()函数即可 123 | 124 | ```python 125 | class JSONEncoder(_JSONEncoder): 126 | 127 | def default(self, o): 128 | return dict(o) 129 | 130 | 131 | class Flask(_Flask): 132 | json_encoder = JSONEncoder 133 | ``` 134 | 135 | models/user.py 136 | ```python 137 | class User(Base): 138 | id = Column(Integer, primary_key=True) 139 | email = Column(String(50), unique=True, nullable=False) 140 | auth = Column(SmallInteger, default=1) 141 | nickname = Column(String(24), nullable=False) 142 | _password = Column('password', String(128)) 143 | 144 | # SQLALChemy的实例化是不会调用__init__函数的,要想让他调用就需要 145 | # @orm.reconstructor这个装饰器 146 | @orm.reconstructor 147 | def __init__(self): 148 | self.fields = ['id', 'email', 'nickname'] 149 | 150 | def keys(self): 151 | return self.fields 152 | 153 | # 支持隐藏字段 154 | def hide(self, *keys): 155 | [self.fields.remove(key) for key in keys] 156 | 157 | # 支持添加字段 158 | def append(self, *keys): 159 | [self.fields.append(key) for key in keys] 160 | ``` 161 | 162 | ### 5.完善序列化 163 | 164 | 优化1:每一个模型如果需要序列化,都要有getitem方法,可以放到基类里面去 165 | 166 | 优化2:default函数,是递归调用的,只要遇到不能序列化的对象,就会调用default函数。所以如果有其他类型,我们需要修改完善我们的default函数 167 | 168 | 优化3:我们的default函数需要增加容错性 169 | 170 | ```python 171 | class JSONEncoder(_JSONEncoder): 172 | 173 | def default(self, o): 174 | if hasattr(o, 'keys') and hasattr(o, '__getitem__'): 175 | return dict(o) 176 | # 兼容其他的序列化 177 | if isinstance(o, date): 178 | return o.strftime('%Y-%m-%d') 179 | raise ServerError() 180 | ``` 181 | 优化4:之前编写的新的Flask类,JsonEncoder类都是不会轻易改变的,但是app.py中的一些其他方法,却是 经常改变的,应该把他们放在init文件中 182 | 183 | 184 | ### 6.ViewModel对于API有意义吗? 185 | 186 | viewmodel对于API来说,特别是内部开发来说非常有意义 187 | 188 | viewmodel是为了我们的视图层,提供个性化的试图模型。SQLALChemy返回的模型是原始模型(格式和数据库中存储的一模一样)。 189 | 而前端可能需要我们返回一个意义更加明确的字段。 190 | 191 | 原始模型是根据数据库来生成的,他的格式是一定的,但是我们在视图层中或者API的返回中,要根据业务去具体的个性化一个个属性的 192 | 格式,这就必然存在一个由原始模型向视图模型转换的过程,这个过程最适合的是在View_model中进行一个转换。 193 | 194 | 我们在视图层写转换的代码,一来会污染视图层的代码,二来会难以复用 195 | 并且有的试图模型可能会比较复杂,设计到多个原始模型,这个代码必定会比较复杂,写在视图函数中就会非常不合适 196 | 197 | 对于完全严格意义上的RESTFul,viewmodel的意义并不大,因为完全资源意义的RESTFul是不考虑业务逻辑的 198 | 199 | 200 | 201 | 202 | 203 | 204 | --------------------------------------------------------------------------------