一、目录结构
1.1、项目入口文件
如何开始一个项目?
- 创建启动文件。
- 实例化 flask ,app = Flask(__name__)
- 定义视图函数,@app.route(’’,methods=[‘GET’])
# ginger.py
# _*_ coding:utf-8 _*_
from werkzeug.exceptions import HTTPException
from app import create_app
from app.libs.error import APIException
from app.libs.error_code import ServerError
app = create_app()
"""
解决未知异常类型的方法:未知异常都是由框架返回的错误,是我们意识不到得,所以不确定。
通过flask 提供的 errorhandler 捕获所有的异常,必须在入口文件才能捕获到所有错误类型。
Exception 表示python异常基类
"""
@app.errorhandler(Exception)
def framework_error(e):
"""
e表示所有异常类型:
APIException
HTTPException
Exception
"""
# 判断 e 的类型是不是 APIEception
if isinstance(e, APIException):
return e
if isinstance(e, HTTPException):
code = e.code
msg = e.description
error_code = 1007
return APIException(msg, code, error_code)
else:
# 这里可以记录集体错误信息,并记录到日志中
# 基类异常不需要返回具体的异常,可以通过定义一个统一的异常类型返回就可以了。
# 判断 debug 来处理异常返回的格式,如果是调试模式,直接返回错误信息,不是调试模式,返回自定义错误信息。
if not app.config['DEBUG']:
return ServerError()
else:
raise e
if __name__ == "__main__":
app.run()
1.2、创建数据库表、创建Flask app项目、把蓝图注册到Flask中
app/__init__.py 文件
# app/__init__.py
# _*_ coding:utf-8 _*_
import pymysql
from .app import Flask
pymysql.install_as_MySQLdb()
# 蓝图管理函数
def create_blueprint(app):
from .api.v1 import Create_blue_v1
# 使用url_prefix 必须加上 “/”
app.register_blueprint(Create_blue_v1(), url_prefix='/v1') # 把视图模块挂载到 Flask 核心对象中
# 创建所有数据表
def register_plugin(app):
from app.models.base import db
db.init_app(app) # 注册插件
with app.app_context(): # 把 db.create_all() 推入Flask上下文环境中
db.create_all() # 必须在 Flask 的上下文环境中才能完成 create_all 操作
# 创建app项目函数
def create_app():
app = Flask(__name__)
# 把配置文件加载到 Flask 中
app.config.from_pyfile('./config/secure.py')
app.config.from_object("app.config.setting")
create_blueprint(app)
register_plugin(app)
return app
1.3、如何让jsonify()返回数据模型对象?
重写 Flask.json 中 JSONEncode 下 default 函数,让其支持返回数据模型对象。
jsonify 什么情况下执行 default 函数?理解 jsonify() 序列化时的default函数?
当 flask 知道 jsonify 接收的参数是可被python序列化时,不会执行 default 函数,default 可以递归调用。
- 类.__dict__ 只能把类中的实例变量序列化为字典,类变量不能序列化。
- 这里通过 dict() 来创建字典。
- dict() 序列化原理:
在执行dict(o)时,会先执行 Dictlist 类中的 keys 方法,在keys中指定返回的key值,通过keys指定我们可以灵活的返回需要序列化内容。
默认情况python是不支持 类[‘变量名’] 访问的,但是在内部添加 __getitem__ 方法后将支持,
hasattr(对象,属性) 判断对象中是否有属性。
isinstance(对象,类型) 判断对象是否为这个类型。
Python 面向对象
Python Class 继承
class DictList:
# 类变量
name = '八月'
age = 30
def __init__(self):
self.sex = '男' # 实例化变量
def keys(self):
return ('name', 'age', 'sex',) # 当返回只有一个元素的元祖时,必须这样写('name',)
def __getitem__(self, item):
# item 是keys指定返回变量的key,通过getattr()获取对象相应变量的值
return getattr(self, item)
o = DictList()
print(dict(o))
# 输出:{'name': '八月', 'age': 30, 'sex': '男'}
app/app.py 文件
# app/app.py
# _*_ coding:utf-8 _*_
from datetime import date
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
from app.libs.error_code import ServerError
# 重写 Flask 中 JSONEncode 下 default 函数
# 只要 jsonify 传递的参数,Flask不能被序列化都会调用default
class JSONEncoder(_JSONEncoder):
# o 就是jsonify()接收的参数
def default(self, o):
# 判断传入的对象 o 中是否有 keys 和 __getitem__
if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
return dict(o)
if isinstance(o, date): # 判断对象 o 是否是 date 类型
return o.strftime('%Y-%m-%d')
raise ServerError()
class Flask(_Flask):
json_encoder = JSONEncoder
二、自定义 Redprint 对象
2.1、自定义Redprint对象挂载 Flask 架构图
Redprint实现思路?
- v1蓝图是所有视图函数公用的。
- 视图函数向Redprint对象注册。
- Redprint 注册到 Blueprint v1中。
- Redprint -> register 完成 Redprint 注册到Blueprint v1中的任务
2.2、为什么要自定义Redprint?
- Redprint 功能与Blueprint 一样。
- Flask 中的 Blueprint (蓝图)最好用来定义模块级别的视图请求
- 统一管理URL请求
- 简化视图函数传入的URL
2.2.1、Redprint 代码实现
定义 Redprint 对象
app/libs/redprint.py 文件功能,重写 route 装饰器,构建Redprint对象。
# app/libs/redprint.py 文件
# _*_ coding:utf-8 _*_
"""
自定义路由管理 Redprint 对象:
要完成与 flask 中的 Blueprint 相同的功能,
必须把 Redprint 注册到Blueprint 中。
"""
class Redprint:
def __init__(self, name):
self.name = name
self.mound = []
# 构建 route 装饰器
def route(self, rule, **options):
def decorator(f):
# 将传入的内容保存到一个列表中,等待向flask的Blueprint里面注册
self.mound.append((f, rule, options))
return f
return decorator
# 登记函数,通过 register 把视图函数注册到 Blueprint 中
# bp 表示 flask 中的 Blueprint
def register(self, bp, url_prefix=None):
# 判断是否传入 url_prefix ,不传就等于视图名称
if url_prefix is None:
url_prefix = '/' + self.name
for f, rule, options in self.mound:
"""
options.pop('endpoint',f.__name__)
如果路由传入endpoint参数:
从 options 字典找到key为 endpoint 的参数,将其删除,并返回 endpoint 对应的 value,
如果没有 key 为 endpoint,返回第二参数,也就是装饰器对应函数的函数名。
Redprint 中 endpoint 包含 蓝图.模块+视图函数 ==> 文件夹.视图文件+文件中的视图函数
"""
endpoint = self.name + '+' + options.pop('endpoint', f.__name__)
# 通过 Blueprint中add_url_rule方法注册route接收到的信息。
bp.add_url_rule(url_prefix + rule, endpoint, f, **options)
2.2.2、.pop() 一些理解
a = {
'b': 'jsom',
# 'f':'age'
'c': 'name'
}
# 如果a字典中有‘b’这个key,返回‘b’这个key的value,如果没有则返回第二参数‘e’
d = a.pop('b','e')
print('d = >', d)
print('a = >', a)
输出:
d = > jsom
a = > {'c': 'name'}
没有’b‘输出:
d = > e
a = > {'f': 'age', 'c': 'name'}
api/v1/__init__.py 通过 Redprint.register 对象把视图函数注册到 Blueprint 对象中
# _*_ coding:utf-8 _*_
from flask import Blueprint
from app.api.v1 import user, book, client, token
# 蓝图与自定义 redprint 的连接函数
def Create_blue_v1():
# 定义蓝图为 v1
blue = Blueprint('v1', __name__)
# 通过 redprint.register把Redprint 对象注册到 Blueprint v1 中
user.api.register(blue)
book.api.register(blue)
client.api.register(blue)
token.api.register(blue)
return blue
2.2.3、Redprint 使用
api/v1/user.py 视图文件
我们需要把从数据模型中获取到的数据序列化为字典,但是jsonify() 不支持数据模型的序列化。在app/app.py重写JSONEncode 中的default属性来实现对数据模型序列化。
# _*_ coding:utf-8 _*_
# from flask import Blueprint
# user = Blueprint('User',__name__)
from flask import jsonify, g
from app.libs.error_code import DeleteSuccess, AuthFailed
from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.base import db
from app.models.user import User
api = Redprint('user')
"""
view_model 视图层 返回对前端友好的个性化视图模型
比如 User模型是数据库存储的原始模型,对前端不友好
"""
# 管理员可以获取所有用户信息
@api.route('', methods=['GET'])
@auth.login_required
def super_get_user():
# 通过uid 获取用户
user = User.query.filter_by(status=1).all()
# 如何返回数据模型对象?
# jsonify() 方法重写 default 函数,让其支持返回数据模型对象
# jsonify 什么情况下执行 default 函数?
# 当 flask 知道 jsonify 接收的参数是可被序列化时,不会执行 default 函数
return jsonify(user)
# 普通用户、管理员都能使用id获取信息
@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
user = User.query.filter_by(id=uid).first_or_404()
return jsonify(user)
# 管理员权限删除可以删除所有用户
@api.route('/<int:uid>', methods=['DELETE'])
@auth.login_required
def super_delete_user(uid):
with db.auto_commit():
if uid is g.user.uid:
return NotFound('非法请求!')
user = User.query.filter_by(id=uid).first_or_404()
user.delete()
return DeleteSuccess()
# 普通用户只能删除自己,现实应用是不允许删除自己的。
@api.route('', methods=['DELETE'])
@auth.login_required
def delete_user():
uid = g.user.uid
# g 变量被线程隔离,多个用户请求时,不会冲突
with db.auto_commit():
user = User.query.filter_by(id=uid).first_or_404()
user.delete()
return DeleteSuccess()
api/v1/user.py 视图文件
# _*_ coding:utf-8 _*_
# from flask import Blueprint
# book = Blueprint('Book',__name__)
# 自定义URL管理类
from flask import jsonify
from sqlalchemy import or_
from app.libs.redprint import Redprint
from app.models.book import Book
from app.validators.forms import BookSearchForm
api = Redprint('book')
@api.route('/search')
def search():
form = BookSearchForm().validate_for_api()
q = '%' + form.q.data + '%'
books = Book.query.filter(
or_(Book.title.like(q), Book.publisher.like(q))
).all()
books = [book.hide('summary').append('pages') for book in books]
return jsonify(books)
@api.route('/<isbn>/detail')
def detail(isbn):
book = Book.query.filter_by(isbn=isbn).first_or_404()
return jsonify(book)
三、数据返回处理
四、重写 werkzeug.exceptions --> HTTPException 异常处理
4.1、为什么要自定义异常处理?
为什么要自定义异常处理?
因为原有的异常处理返回的是HTML格式的错误信息,前端获取到这样的错误信息不好处理,对于API开发来说,需要返回统一JSON格式错误信息。
利用werkzeug.exceptions中HTTPException异常处理,通过继承的方式,自定义符合前端使用的异常处理。
自定义错误处理实现步骤:
- 继承 HTTPException 错误对象,让其处理自定义错误信息;
- 编写构造函数,传入自定义类参数;
- 调用父类的构造函数,把 msg 传入,通过父类的response处理后返回;
- 重写 get_body、get_headers 让其返回JSON格式的错误信息。
# app/libs/error.py 文件
# werkzeug.exceptions 异常处理
from flask import request, json
from werkzeug.exceptions import HTTPException
class APIException(HTTPException):
code = 500
msg = 'sorry,we make a mistake (* _^_ )!'
error_code = 999
# 自定义异常处理类的构造函数
def __init__(self, msg=None, code=None, error_code=None, headers=None):
if code:
self.code = code
if error_code:
self.error_code = error_code
if msg:
self.msg = msg
# 调用 HTTPException 中的构造函数,第一个参数为错误提示信息,必须传;
# 第二个参数为 response,不传会根据 HTTPException 接收到的 code、description 自动生成 response。
super(APIException, self).__init__(msg, None)
# 重写HTTPException 中个get_body,让其返回json格式错误信息
def get_body(self, environ=None):
body = dict(
msg=self.msg,
error_cod=self.error_code,
# request 返回请求信息,请求方法,发生错误的URL
request=request.method + ' ' + self.get_url_no_param()
)
return json.dumps(body)
# 重写HTTPException 中个get_headers,改变返回请求头的返回信息。
def get_headers(self, environ=None):
return [("Content-Type", "application/json")]
# 处理返回 get_body 中 request url
@staticmethod
def get_url_no_param():
full_path = str(request.full_path)
main_path = full_path.split('?')
return main_path[0]
4.2、自定返回异常
# app/libs/error_code.py 文件
# _*_ coding:utf-8 _*_
# 自定义异常提示对象
# 导入自定义异常返回处理对象
"""
异常的类型:
已知错误类型:提前知道的类型
未知错误类型:错误不固定,随时可能会报错
解决未知异常的方法:
不管未知异常在什么地方出现,在项目启动文件捕获所有的异常,
捕获到异常后再进行格式化处理后统一抛出。
"""
from app.libs.error import APIException
class Success(APIException):
code = 201
msg = 'OK'
error_code = 0
# 删除成功
class DeleteSuccess(Success):
code = 202
error_code = -1
# 未知错误
class ServerError(APIException):
code = 500
msg = 'sorry,we make a mistake (* _^_ )!'
error_code = 999
class ClientTypeError(APIException):
code = 400
msg = 'Client Type Error'
error_code = 1006
class ParameterException(APIException):
code = 400
msg = 'Invalid paramter'
error_code = 1000
class NotFound(APIException):
code = 404
msg = 'the resource are not_found O_O'
error_code = 1001
class AuthFailed(APIException):
code = 401
error_code = 1005
msg = 'authorization failed'
class Forbidden(APIException):
code = 403
error_code = 1004
msg = 'forbidden, not in scope'
五、枚举模块
app/libs/enums.py 文件定义用户通过什么平台登录。
# app/libs/enums.py 文件
# 枚举模块
from enum import Enum # 引入python枚举模块
class ClientTypeEnum(Enum):
USER_EMAIL = 100 # email 登录
USER_MOBILE = 101 # 手机登录
USER_MINA = 200 # 微信小程序登录
USER_WX = 201 # 微信公众号登录
六、数据模型基类
为什么需要基类模型?
基类模型定义每个模型中相同的公共数据模型字段,也就是每个数据模型中都会出现的数据表字段。
数据模型基类 app/models/base.py
# _*_ coding:utf-8 _*_
# 重写 SQLAlchemy 中的方法,定义数据模型基类
from contextlib import contextmanager # 导入 Flask 的上下文管理器
from datetime import datetime
from flask_sqlalchemy import BaseQuery, SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger, orm, inspect
from app.libs.error_code import NotFound
class SQLAlchemy(_SQLAlchemy):
@contextmanager
def auto_commit(self):
try:
yield
self.session.commit()
except Exception as e:
db.session.rollback()
raise e
# 重写 Query 中的一些方法
class Query(BaseQuery):
def filter_by(self, **kwargs):
if 'status' not in kwargs.keys():
kwargs['status'] = 1
return super(Query, self).filter_by(**kwargs)
# 重写 get_or_404 first_or_404 让其返回json格式的错误代码
def get_or_404(self, ident):
rv = self.get(ident)
if rv is None:
raise NotFound()
return rv
def first_or_404(self):
rv = self.first()
if rv is None:
raise NotFound(msg='user not found')
return rv
db = SQLAlchemy(query_class=Query)
# 定义基类数据模型
class Base(db.Model):
__abstract__ = True
create_time = Column(Integer)
status = Column(SmallInteger, default=1)
def __init__(self):
self.create_time = int(datetime.now().timestamp())
def __getitem__(self, item):
return getattr(self, item)
@property
def create_datetime(self):
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
# hasattr() 判断 self 是否包含 key 的属性
# setattr() 对 self 中的 key 属相赋值 value
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
def delete(self):
self.status = 0
# 序列化模型
def keys(self):
return self.fields
# 隐藏数据模型字段
def hide(self, *keys):
for key in keys:
self.fields.remove(key)
return self
# 添加数据模型字段
def append(self, *keys):
for key in keys:
self.fields.append(key)
return self
# 第二种序列化模型类方法
class MixinJSONSerializer:
@orm.reconstructor
def init_on_load(self):
self._fields = []
# self._include = []
self._excludes = []
self._set_fields()
self.__prune_fields()
def _set_fields(self):
pass
def __prune_fields(self):
columns = inspect(self.__class__).columns
if not self._fields:
all_columns = set(columns.keys())
self._fields = list(all_columns) - set(self._excludes)
def hide(self, *args):
for key in args:
self._fields.remove(key)
return self
def keys(self):
return self._fields
def __getitem__(self, key):
return getattr(self, key)
user.py用户数据模型。
# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, SmallInteger
from werkzeug.security import generate_password_hash, check_password_hash
from app.libs.error_code import NotFound, AuthFailed
from app.models.base import Base, db
# User 表
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
email = Column(String(24), unique=True, nullable=False)
nickname = Column(String(24), unique=True) # 用户暱称
auth = Column(SmallInteger, default=1) # 权限的标识
_password = Column('password', String(100))
# 第一种通过 keys和 base.py中的__getitem__ 联合实现数据模型的序列化
def keys(self):
return ('id', 'email', 'nickname', 'auth',)
# 通过@property 预处理 password
# 获取 数据模型中 _password 返回
@property
def password(self):
return self._password
# 获取 form 表单提交过来的 password,加密后再设置模型中的_password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw) # 给密码加密
# 在对象下创建对象本身是不合理的,通过静态方法或者类方法创建。
@staticmethod
def register_by_email(nickname, account, secret):
with db.auto_commit():
user = User()
user.nickname = nickname
user.email = account
user.password = secret
db.session.add(user)
db.session.commit()
@staticmethod
def verify(email, password):
user = User.query.filter_by(email=email).first_or_404()
if not user.check_password(password):
raise AuthFailed()
scope = 'AdminScope' if user.auth == 2 else 'UserScope'
return {'uid': user.id, 'scope': scope}
# 比较密码是否一致
def check_password(self, raw):
if not self._password:
return False
return check_password_hash(self._password, raw) # 验证密码是否一致
book.py书籍数据模型。
# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, orm
from app.models.base import Base
class Book(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(50), nullable=False)
author = Column(String(30), default='未名')
binding = Column(String(20))
publisher = Column(String(50))
price = Column(String(20))
pages = Column(Integer)
pubdate = Column(String(20))
isbn = Column(String(15), nullable=False, unique=True)
summary = Column(String(1000))
image = Column(String(50))
@orm.reconstructor
def __init__(self):
self.fields = ['id', 'title', 'author', 'binding',
'publisher', 'price', 'pubdate',
'isbn', 'summary', 'image'
]
@orm.reconstructor
sqlalchemy 在通过元类实例化模型对象时,不会执行__init__
可以通过 orm.reconstructor 装饰器来执行__init__
七、用户知识
7.1、重新Form中的一些方法
form.validate() 方法,遇到错误是不会抛出错误信息的,只会把错误信息保存在form.erroes 属性中,既然这样我们只能手动让它抛出错误异常信息。
# _*_ coding:utf-8 _*_
from flask import request
from wtforms import Form
from app.libs.error_code import ParameterException
class BaseForm(Form):
def __init__(self):
'''
request.json 把表单的提交数据转换为json
request.args.to_dict()
data = request.json
request.json 调用的是 request.get_json(self, force=False, silent=False, cache=True) 这个函数
'''
data = request.get_json(silent=True) # silent 保持静默,不会报错
args = request.args.to_dict()
super(BaseForm, self).__init__(data=data, **args) # 参数为json的时必须为data=json值,要不然不能解析
# 重写 validate 方法,把不抛出异常修改为抛出异常
def validate_for_api(self):
# 调用父类的validate方法并返回出现的异常
valid = super(BaseForm, self).validate()
if not valid:
raise ParameterException(msg=self.errors)
return self
7.1、用户注册
用户注册的方式,通过枚举的方式定义。
client.py用户注册
# app/api/v1/client.py
# _*_ coding:utf-8 _*_
# flask 中所有用户提交的数据都在request中
from app.libs.enums import ClientTypeEnum
from app.libs.error_code import Success
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, UserEmailForm
api = Redprint('client')
# 注册登录
@api.route('/register', methods=['GET', 'POST'])
def create_client():
# 考虑的问题:参数 校验 接收参数
# 调用重写后验证器,对提交的数据进行验证并抛出异常
form = ClientForm().validate_for_api()
promise = {
ClientTypeEnum.USER_EMAIL: __register_user_by_email
}
promise[form.type.data]()
return Success()
# email 登录处理函数
def __register_user_by_email():
form = UserEmailForm().validate_for_api()
# 表单对象 ClientForm 无法获取 nickname ?
# 1、request.json['nickname'] 中包含了这个参数,但是 request 是没有通过验证的
# 所以不建议使用这种方法获取。
# 2、通过 form 获取,
# 利用python的继承特性,编写一个特殊的 Form 对象 UserEmailForm ,让其去继承
# ClientForm 这个基类,在 UserEmailForm 编写个性的 Form 属性。
User.register_by_email(form.nickname.data, form.account.data, form.secret.data)
7.2、用户注册校验 WTForms
forms.py对form表单提交过来的数据验证。
为什么设置ClientForm这个基类?
- 增加代码的复用性,减少代码量;
- 通过面向对象的继承特性,适用多种用户注册方式,比如email,小程序等等;
- 验证共有的参数,比如不管通过什么方式注册用户都必须传入 type 这个参数,所以就可以在基类中进行统一的验证。
# _*_ coding:utf-8 _*_
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, length, Email, Regexp, ValidationError
from app.libs.enums import ClientTypeEnum
from app.models.user import User
from app.validators.base_form import BaseForm as Form
# 注册基类,
class ClientForm(Form):
account = StringField(validators=[DataRequired(message='用户名不能为空'), length(min=5, max=32)])
secret = StringField() # 密码
type = IntegerField(validators=[DataRequired()])
# 自定义验证器,验证type
def validate_type(self, value):
try:
# 把用户输入的数字转换为枚举类型
client = ClientTypeEnum(value.data)
except ValueError as e:
raise e
self.type.data = client
# 继承基类,并编写独立的email注册类
class UserEmailForm(ClientForm):
account = StringField(validators=[Email(message='invalidate email')])
secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
nickname = StringField(validators=[DataRequired(), length(min=2, max=22)])
# 验证用户是否注册,必须以 validate_ 开头
def validate_account(self, value):
if User.query.filter_by(email=value.data).first():
raise ValidationError()
class BookSearchForm(Form):
q = StringField(validators=[DataRequired()])
class TokenForm(Form):
token = StringField(validators=[DataRequired()])
7.3、通过 Token 登录API接口
Token的基本规则:
- token必须得有有效期时间;
- token中必须存储用户的登录信息;
- token必须加密;
- 登录就是获取token的过程;
- 通过POST传入Token信息。
token.py 生成token与登录验证token。
# _*_ coding:utf-8 _*_
from flask import current_app, jsonify
from app.libs.enums import ClientTypeEnum
from app.libs.error_code import AuthFailed
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, TokenForm
# token令牌生成加密模块
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature
api = Redprint('token')
# 生成 Token 令牌
@api.route('', methods=['POST'])
def get_token():
form = ClientForm().validate_for_api()
promise = {
ClientTypeEnum.USER_EMAIL: User.verify # 调用User.verify获取到用户信息
}
identity = promise[ClientTypeEnum(form.type.data)](
form.account.data,
form.secret.data
)
# 生成令牌
expiration = current_app.config['TOKEN_EXPIRATION']
token = generate_auth_token(identity['uid'],
form.type.data,
identity['scope'],
expiration
)
t = {
'token': token.decode('ascii')
}
return jsonify(t), 201
# 获取 Token 令牌
@api.route('/secret', methods=['POST'])
def get_token_info():
form = TokenForm().validate_for_api()
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(form.token.data, return_header=True)
except SignatureExpired:
raise AuthFailed(msg='token is expired', error_code=1002)
except BadSignature:
raise AuthFailed(msg='token is invalid', error_code=1002)
# token中包含的信息
r = {
'scope': data[0]['scope'],
'create_at': data[1]['iat'], # 令牌创建时间
'expire_in': data[1]['exp'], # 令牌过期时间
'uid': data[0]['uid']
}
return jsonify(r)
# 生成令牌函数
def generate_auth_token(uid, ac_type, scope=None, expiration=7200):
s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
return s.dumps({
'uid': uid,
'type': ac_type.value,
'scope': scope
})
token_auth.py 验证token是否合法,提供视图函数访问保护。
通过flask_httpauth中的 HTTPBasicAuth 来为视图函数访问提供保护,,验证Token的合法性与有效时间。
# _*_ coding:utf-8 _*_
"""
HTTPBasicAuth 发送账号和密码的方式必须在HTTP请求头中发送
格式:
固定写法 --> Authorization : basic bse64(账号:密码)
"""
from collections import namedtuple
from flask import current_app, g, request
from flask_httpauth import HTTPBasicAuth # 提供视图函数访问保护。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, \
BadSignature, SignatureExpired
from app.libs.error_code import AuthFailed, Forbidden
from app.libs.scope import is_in_scope
auth = HTTPBasicAuth()
# 构建一个对象式结构
User = namedtuple('User', ['uid', 'ac_type', 'scope'])
"""
在这里获取token的时候,只要把token通过账号传递进来即可,密码不用传值
因为token中已经包含了账号密码信息。
"""
# user 登录,通过 HTTPBasicAuth 验证 token 合法函数
@auth.verify_password
def verify_password(account, password):
# account == token
# 验证token是否合法
user_info = verify_auth_token(account)
if not user_info:
return False
else:
# 将 user_info 保存到全局的 g.user 中
g.user = user_info
return True
# 验证token是否合法、有效时间是否过期
# 1.验证令牌是否合法
# 2.验证令牌时间是否过期
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY']) # SECRET_KEY 在配置文件中必须设置
try:
data = s.loads(token)
except BadSignature: # 捕获BadSignature,验证 Token 是否合法
raise AuthFailed(msg='token is invalid', error_code=1002)
except SignatureExpired: # 捕获SignatureExpired,验证 Token 是否过期
raise AuthFailed(msg='token is expired', error_code=1003)
uid = data['uid']
ac_type = data['type']
scope = data['scope']
allow = is_in_scope(scope, request.endpoint) # 判断视图函数是否在权限中
if not allow:
raise Forbidden()
# 最佳返回结果为对象式结构
return User(uid, ac_type, scope)
7.3.1、python中namedtuple介绍
namedtuple类位于collections模块,namedtuple能够用来创建类似于元祖的数据类型,除了能够用索引来访问数据,能够迭代,还能够方便的通过属性名来访问数据。
在python中,传统的tuple类似于数组,只能通过下标来访问各个元素,我们还需要注释每个下表代表什么数据。通过使用namedtuple,每个元素有了自己的名字。声明namedtuple是非常简单方便的。
from collections import namedtuple
Friend = namedtuple("Friend", ['name', 'age', 'email'])
f1 = Friend('xiaowang', 33, '[email protected]')
print('f1=>', f1)
print('f1.age=>', f1.age)
print('f1.email=>', f1.email)
f2 = Friend(name='xiaozhang', email='[email protected]', age=30)
print('f2=>', f2)
name, age, email = f2
print('name, age, email=>', name, age, email)
输出:
f1=> Friend(name='xiaowang', age=33, email='[email protected]')
f1.age=> 33
f1.email=> [email protected]
f2=> Friend(name='xiaozhang', age=30, email='[email protected]')
name, age, email=> xiaozhang 30 [email protected]
八、访问权限管理
权限管理对应表写在什么地方?
- 写到数据库中,mysql,redis等;
- 写在代码中,我们这里写在代码中。
scope.py权限管理文件
# _*_ coding:utf-8 _*_
# 权限管理
class Scope:
allow_api = [] # 设置视图函数的访问权限
allow_module = [] # 设置模块级别的访问权限
forbidden = [] # 设置需要排除的视图函数
# 通过__add__ 完成 “+” 操作,利用类的继承性,让每个继承了Scope 都可以使用 “+” 操作
def __add__(self, other):
# 处理视图函数访问权限
self.allow_api = self.allow_api + other.allow_api
# 利用 set 集合类型不重复特性,去除重复
self.allow_api = list(set(self.allow_api))
# 处理模块级别
self.allow_module = self.allow_module + other.allow_module
self.allow_module = list(set(self.allow_module))
# 处理排除
self.forbidden = self.forbidden + other.forbidden
self.forbidden = list(set(self.forbidden))
return self
class AdminScope(Scope):
# allow_api = ['v1.user+get_user', 'v1.user+super_get_user', 'v1.user+delete_user']
allow_module = ['v1.user']
def __init__(self):
self + UserScope()
class UserScope(Scope):
forbidden = ['v1.user+delete_user', 'v1.user+super_get_user']
# allow_api = ['v1.user+get_user', 'v1.user+delete_user']
# class SuperScope(Scope):
# allow_api = ['v1.C', 'v1.D']
# allow_module = ['v1.user']
#
# def __init__(self):
# self + UserScope() + AdminScope()
# 判断视图函数是否在权限中
# endpoint 接收的是 Redprint 对象中 endpoint ,我们将其改写为了包含 蓝图.模块+视图函数 ==> 文件夹.视图文件+文件中的视图函数 这样的形式,方便处理。
def is_in_scope(scope, endpoint):
# 利用 globals() 实现反射
scope = globals()[scope]()
splits = endpoint.split('+')
red_name = splits[0]
if endpoint in scope.forbidden:
return False
if endpoint in scope.allow_api:
return True
if red_name in scope.allow_module:
return True
else:
return False
8.1、globals() 实现反射
globals() 的定义:返回一个字典, 表示当前位置的所有全局符号表。 这个符号表始终针对当前模块(对函数或方法来说, 是指定义它们的模块, 而不是调用它们的模块)。
现在有个问题,如果只想调用函数名以 _super 结尾的函数,但是又不确定具体方法名和有几个函数,该怎么办呢?这时候我们可以用 globals() 函数来实现,注意示例中 main 函数中的代码部分
def zero_super():
return 0
def one_super():
return 1
def two_super():
return 2
def hello():
print("Hello")
if __name__ == '__main__':
promos = [name for name in globals()]
print(promos)
输出:
['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__','zero _super', 'one _super', 'two _super','hello']
前面打印的都是一些内置函数,我们不关注,我们只关注自己定义的函数,那么只想调用某一类型的函数,比如后缀是 _super 结尾的怎么办呢?加上一个个过滤条件:
if __name__ == '__main__':
promos = [name for name in globals() if name.endswith(" _super")]
print(promos)
输出:
['zero_promo', 'one_promo', 'two_promo']
现在得到的是一个字符串,肯定是不能执行的,那该如何是好呢?这里就可以利用 globals() [name] 来获取函数体。
if __name__ == '__main__':
promos = [globals()[name] for name in globals() if name.endswith(" _super")]
print(promos)
print(promos[0]())
输出:
[<function zero_promo at 0x0000013872FE2E18>, <function one_promo at 0x00000138735D5730>, <function two_promo at 0x00000138735D57B8>]
获得函数体后就可以加‘()’调用函数。
promos[0]()
输出:
0