flask 写restful api
用flask写restful api实录
什么是restful api
简单来讲, 就是通过http协议固有的方法, 如get,post,put,delete实现对数据的CRUD, 再利用http协议的状态码对数据状态进行表示。在rest的概念中,资源是核心, 所有的操作都是针对资源进行的。什么是资源?简单来讲就是名字, 要完成对资源的操作,就需要借助http提供的各种方法。这样做的好处是什么?私以为这样的好处是语义化, 每一个名词对应的url,加上操作动词,构成了整个操作逻辑。方便了前后端交互。
restful api一般用于对外服务的api设计, 内部则很少使用。
restful 设计原则
资源是核心, 每一种资源对应一种名词
请求头, 合理的请求方法
状态码, 如201, 401, 403, 在http中都有相应的含义, 配合状态码表述相应内容
版本表示, 大该有三种, url中加入版本信息(推荐), headers中加入版本信息
Accept:application/vnd.github.v3+json, 还有就是自定义headers访问频率限制, 设计api时必须要加入频率限制, 429表示请求过多, 在headers中也有必要加入相应的限制提示
信息过滤, 对于大量数据的内容, 通常采用offset和limit进行切割, 分页
api失效或者迁移, 401, 301
缓存 http提供了
last-modified``date等相关headers请求和相应一般采取
json格式
搞清问题本质
使用flask写api是件很方便的事情, flask内部的jsonify方法可以轻松的返回json数据。但我们仍需要关注几个重点问题。
-
如何应对异常, 如何将异常信息改为json格式
-
验证数据, 对客户端提交的数据进行验证
-
如何序列化,在MVC设计中, 我们无需进行序列化, 而是模板语言直接渲染, 而对于json传输我们必须关注序列化
-
如何限制访问频率, 这里有个
flask-limiter可以轻松解决我们的问题 -
身份验证问题, 在MVC中,采用session与cookie的方式不再万能, 需要采用新的验证方式
其中1,最为复杂,需要对flask异常类进行重写,默认flask异常类都是以html渲染的。
定义flask url
这里直接使用flask提供的蓝图,增加版本信息到url中
v1 = Blueprint('api_v1', __name__, url_prefix='/api_v1')
重写flask异常类
定义一个通用的异常类, 可以通过继承APIException 重写定义NotFoundException``ServerInternalExecption
这样就完成了对,异常类的重写。通过制定不同的code和msg和error_code来区分不同的异常
class APIException(HTTPException):
code = 500
msg = 'Our Server Has A Problem :( You Can Try Later'
error_code = 9999
def __init__(self, code=None, msg=None, error_code=None, headers=None, **kwargs):
if code:
self.code = code
if msg:
self.msg = msg
if error_code:
self.error_code = error_code
super(APIException, self).__init__(msg, None)
def get_body(self, environ=None):
body = dict(
msg=self.msg,
error_code=self.error_code
)
return json.dumps(body)
def get_headers(self, environ=None):
return [('Content-Type', 'application/json')]
特别注意:
我们也可以通过flask异常返回机制, 返回正确的值。当新建资源成功时,可以return成功的异常,用以表示状态。
class Success(APIException):
code = 201
msg = 'ok'
error_code = 0
全局异常处理
上面我们处理了关于HTTPException相关异常, 但如果代码中出现异常, 则仍然无法捕获, 这里需要进一步捕捉异常。
在flask1.0版本提供了全局异常捕捉。这样就可以针对代码中所有异常进行捕获了。
@app.errorhandler(Exception)
def handler_global_exceptions(e):
if isinstance(e, APIException):
return e
elif isinstance(e, HTTPException):
msg = e.description
code = e.code
error_code = 4099
app.logger.warning('status_code:'+str(code)+'msg:'+str(e))
return APIException(msg=msg, code=code, error_code=error_code)
else:
if app.config['DEBUG']:
raise e
else:
app.logger.exception(str(e)) # 可以将堆栈信息记录到日志当中
return ServerErrorException()
wtform验证客户提交的数据
有了上面的异常处理, 在验证时,将不符合的数据, 通过异常直接抛出返回json, 这里的问题是如何获取到验证错误信息
这里需要重写wtform的BaseForm, 实现相关功能。
class BaseForm(Form):
def __init__(self, data=None):
if not data:
data = request.json # 默认客户端传来的是json格式
super(BaseForm, self).__init__(data=data)
def validate_api(self):
valid = super(BaseForm, self).validate() # 返回的是true 或 false
if not valid:
raise ParametersException(msg=self.errors) # 这里使用self.errors, 因为验证的错误信息都被放入了self.erros,直接在msg中指定即可。 如果需要自定义error信息,在字段验证时, 指定具体的message=
return self
视图中使用form.validate_api
@v1.route('/user', methods=['POST'])
@limiter.limit("5 per minute") # 频率限制 稍后会介绍
def create_user():
form = SignUpUserForm().validate_api() # 如验证不通过 直接抛出异常
user = User()
ret = user.create_user(form.username.data, form.password1.data)
return Success(code=201, msg=ret) # 利用异常返回机制 可以将正确的信息进行返回
flask序列化&SQLAlchemy序列化
序列化的本质 是数据的序列化, 也就是字段的序列化。
我们可以通过建立ViewModel使用如下案例进行序列化
ret = {
'user_id': user.id,
'username': user.username,
'email': user.email,
'addr': user.addr
}
return jsonify(ret)
但这样每个模型都要写这么多重复的代码, 是很不方便的。如果可以将模型对象可以序列化, 则可以大大减少代码量。而这就需要重写flask.jsonify方法, 在调用jsonify时, 实际上flask会进行判断, 如果是基础的数据类型,则直接序列化, 如果不是, 则调用default 方法, 其中内置了几个方法, 可以将date对象进行序列化, 我们需要在此方法内,将我们的自定义的序列化方法加入进去。
from flask.json import JSONEncoder as _JSONEncoder
class JSONEncoder(_JSONEncoder):
def default(self, o):
if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
return dict(o)
if hasattr(o, datetime):
return o.strftime('%Y-%m-%d %H:%M:%S')
raise ServerErrorException()
class Flask(_Flask):
json_encoder = JSONEncoder
这里使用了对象的keys 方法和__getitem__ 方法。
在Model中, 我们定义的是类变量, 而类变量是无法通过 实例.__dict__ 进行获取的,因而我们无法得到相应的字典。看个栗子
class Kay:
name = 'kay'
age = 18
def __init__(self):
self.gender = 'male'
k = Kay()
print(k.__dict__)
#结果是:
{'gender': 'male'}
class Kay:
name = 'kay'
age = 18
def __init__(self):
self.gender = 'male'
# 调用keys dict()
def keys(self):
return ('name', 'age', 'gender')
def __getitem__(self, item):
return getattr(self, item)
k = Kay()
print(dict(k))
# {'name': 'likai', 'age': 18, 'gender': 'male'}
通过定义keys 和__getitem__ 方法, 实现了对类变量序列化。dict(实例) 得到的是基础数据类型, 就可以用于序列化。
class User(BaseModel):
id = Column(Integer, autoincrement=True, primary_key=True)
username = Column('username', String(32), unique=True)
_password = Column('password', String(128), nullable=False)
role = Column('role', Integer)
files = relationship('File', backref='user')
def keys(self):
return ('username', 'role')
def __getitem__(self, item):
return getattr(self, item)
@v1.route('/user/', methods=['GET'])
@auth.login_required # 权限验证
@limiter.exempt
def get_user(id):
user = User.query.get_or_404(id)
return jsonify(user) # 返回的json数据中就包含了username 和 role
关于访问频率限制
使用flask-limiter可以轻松实现访问频率限制
limiter = Limiter(key_func=get_remote_address, application_limits=['600 per minute'], headers_enabled=True)
# key_func 限制条件
# application_limits 这是一个全局限制, 可以设置 per minute / per hour / per day
# headers_enabled 是否将限制信息提示放在相应头上
视图函数实例
@v1.route('/user', methods=['PUT'])
@auth.login_required
@limiter.limit("20 per minute")
def update_user():
return ''
使用装饰器来对每一个视图进行具体限制
@v1.route('/user/', methods=['DELETE'])
@auth.login_required
@limiter.exempt
def delete_user(id):
user = User.query.get_or_404(id)
if user:
with db.auto_commit():
db.session.delete(user)
return Success(msg='user has been deleted success')
使用limiter.exempt 跳出全局限制, 使其不受限制
捕捉限制异常:
limiter使用的flask异常处理机制, 抛出的是html,需要进行单独捕捉, 使其变为json格式
@app.errorhandler(429)
def limiter_handler(e):
current_limit = g.view_rate_limit # 可以从g变量获得 限制频率
p = limiter.limiter.get_window_stats(*current_limit)[0] # 获得限制到什么时候
retry_after = int(p-time.time()) # 多少秒之后 限制解除
ret = {
'error': 'excess request limit!',
'msg': 'current_request:%s, wait %ss to request again' % (current_limit[0],retry_after)
}
resp = make_response(jsonify(ret), 429)
return resp
自定义限制条件:
@limiter.request_filter
def my_filter():
path_url = request.path
white_list = ['/except']
if path_url in white_list:
return True
else:
return False
自定义@limiter.request_filter 返回True或False 判断是否要进行限制
limiter基本的用法, 如上, 如需要共享限制, 根据用户身份限制, 请查阅官方文档。
身份验证
在api身份验证, 最常使用的 jwt验证方式。
jwt分为三个部分。
header + payload + signature
前两部分值进行了编码, 没有加密, 后面签名针对前两部分进行加密, 如果前两部分进行了篡改, 那么签名就会不对, 保证了jwt的有效性。 payload一般会存放用户信息, 但由于是公开的, 所以不会存放敏感信息, 只放用户标识及其他非敏感信息。
flask生成jwt的方法, 非常简单。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
def gen_access_token(uid, username, scope=None, expires_in=7200):
s = Serializer(current_app.config['SECRET_KEY'],expires_in=expires_in)
return s.dumps({
'uid': uid,
'username': username,
'scope': scope
}) # 这里相当于 payload 存放一些用户相关信息
登录过程, 就是取得token的过程.
@v1.route('/token', methods=['POST'])
@limiter.limit("10 per hour")
def create_token():
form = CheckUserForm().validate_api()
user = User.verify(form.username.data, form.password.data)
expired_in = current_app.config['ACCESS_TOKEN_EXPIRES']
token = gen_access_token(user['uid'], user['username'], scope=user['scope'], expires_in=expired_in)
return jsonify({
'access_token': token.decode(),
'expired_at': (datetime.datetime.now() + datetime.timedelta(
seconds=expired_in)).strftime('%Y-%m-%d %H:%M:%S')
})
一般情况下, 为了客户端良好的操作体验, 还会提供较长有效期的refresh_token 用来刷新access_token
客户端有了token之后, 需要将token放在哪里, 服务端才能正确接收呢? 这涉及到了服务端如何验证token的问题
这里使用了flask-httpauth
auth = HTTPTokenAuth()
# 利用这种方式实现 Bearer token # 这种方式 是从http头 Authorization中 Bearer + token的方式 取到token值
@auth.verify_token
def verify_password(token):
user_info = parse_token(token) # 解析token
if not user_info:
return False
else:
# 放入上下文全局变量g中
g.user = user_info # 将解析出的字典放入 全局变量g中
return True
接下来如何解析token?
def parse_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except BadSignature:
raise AuthFailedException(msg='access_token is invalid', error_code=4012)
except BadTimeSignature:
raise AuthFailedException(msg='token is expired', error_code=4013)
uid = data['uid']
scope = data['scope']
username = data['username']
# allowed = is_in_scope(scope, request.endpoint) # 可以根据 scope判断不同的用户, 做权限控制
# if not allowed:
# raise ForbiddenException()
return {
'uid': uid,
'username': username,
'scope': scope
}
在视图函数中 使用auth.login_required 验证是否含有正确有效的token
@v1.route('/user/', methods=['DELETE'])
@auth.login_required
@limiter.exempt
def delete_user(id):
user = User.query.get_or_404(id)
if user:
with db.auto_commit():
db.session.delete(user)
return Success(msg='user has been deleted success')
最后
以上就是flask写api的基本思路, 主要研究了几个问题, 最关键的是异常的重写, 还有序列化问题。 其实还有很多很多的细节, 在此没有一一写出来, 有时间在慢慢总结。
也可以尝试使用flask-restful 插件进行构造api, 后面也会介绍到这种方式。
初学flask, 还有很多地方不明白, 还请各位看官多多指教。

浙公网安备 33010602011771号