创建博客-使用REST Web服务

来源:互联网 发布:freebsd 查看安装软件 编辑:程序博客网 时间:2024/05/23 23:47

使用Flask创建REST Web服务很简单,使用熟悉的route()装饰器及其methods可选参数可以声明服务所提供资源URL的路由,处理JSON数据同样简单,因为请求中包含的JSON数据可通过request.json这个Python字典获取,并且需要包含JSON的响应可以使用Flask提供的辅助函数jsonify()从Python字典中生成

创建API蓝本

REST API相关的路由是一个自成一体的程序子集,所以为了更好的组织代码,我们最好把这些路由放到独立的蓝本中,这个程序API蓝本的基本结构如下:

|-flasky  |-app/    |-api_1_0      |-__init__.py      |-users.py      |-posts.py      |-comments.py      |-authentication.py      |-errors.py      |-decorators.py      

注意,API包的名字中有个版本号,如果需要创建一个向前兼容的API版本,可以添加一个版本号不同的包,让程序同时支持两个版本的API

在这个API蓝本中,各资源分别在不同的模块中实现,蓝本中还包含处理认证、错误以及提供自定义装饰器的模块,蓝本的构造文件如下所示:

# app/api_1_0/__init__.pyfrom flask import Blueprintapi = Blueprint('api', __name__)from . import authentication, posts, users, comments, errors#...

注册API蓝本的代码如下:

# app/__init__.pydef create_app(config_name):    #...    from .api_1_0 import api as api_1_0_blueprint    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')    return app

错误处理

REST Web服务将请求的状态告知客户端时,会在响应中发送适当的HTTP状态码,并将额外信息放入响应主体,客户端能从Web服务得到的常见状态码如下表

HTTP状态码 名称 说明 200 OK(成功) 请求成功完成 201 Created(已创建) 请求成功完成并创建了一个新资源 400 Bad request(坏请求) 请求不可用或不一致 401 Unauthorized(未授权) 请求中为包含认证信息 403 Forbidden(禁止) 请求中发送的认证密令无权访问目标 404 Notfound(未找到) URL对应资源不存在 405 Methods not allowed(不允许使用的方法) 指定资源不支持请求使用方法 500 Internal server error(内部服务器错误) 处理请求的过程中发生意外错误

处理404和500状态码时会有点小麻烦,因为这两个错误是由Flask自己生成的,而且一般会返回HTML响应,这很可能会让API客户端困惑

为所有客户端生成适当相应的一种方式是,在错误处理程序中根据客户端请求的格式改写响应,这种技术成为内容协商, 下例是改进后的404错误处理程序,它向Web服务客户端发送JSON格式响应,除此之外都发送HTML格式响应,500错误处理程序的写法类似

app/main/errors.py@main.app_errorhandler(404)def page_not_found(e):    if request.accept_mimetypes.accept_json and \            not request.accept_mimetypes.accept_html:        response = jsonify({'error': 'not found'})        response.status_code = 404        return response    return render_template('404.html'), 404

这个新版错误处理程序检查Accept请求首部(Werkzeug将其编码为request.accept_mimetypes),根据首部的值决定客户端期望接受的响应格式,浏览器一般不限制响应的格式,所以只为接受JSON格式而不接受HTML格式的客户端生成JSON响应

其他状态码都是由Web服务生成,因此可在蓝本的errors.py模块作为辅助函数实现,下例是403错误的处理程序,其他错误处理程序的写法类似

# app/api_1_0/errors.pydef forbidden(message):    response = jsonify({'error':'forbidden', 'message': message})    response.status_code = 403    return response

现在,Web服务的视图函数可以调用这些辅助函数生成错误响应了

使用Flask-HTTPAuth认证用户

和普通的Web程序一样,Web服务也需要保护信息,确保未经授权的用户无法访问,为此RIA必须询问用户的登录密令,并将其传给服务器进行验证

REST Web服务的特征之一是无状态,即在服务器在两次请求之间不能“记住”客户端的任何信息,客户端必须在发出的请求中包含所有必要信息,因此所有请求都必须包含用户密令

程序当前的登录功能是在Flask-Login的帮助下实现的,可以把数据存储在用户会话中,默认情况下,Flask把会话保存在客户端cookie中,因此服务器没有保存任何用户相关的信息,都转交给客户端保存,这种实现方式看起来遵守了REST架构的无状态要求,但在REST Web服务中使用cookie有点不现实,因为Web浏览器之外的客户端很难提供对cookie的支持,鉴于此,使用cookie并不是一个很好的设计选择

REST架构的无状态看起来似乎过于严格,但这并是不随意提出的要求,无状态的服务器伸缩起来更加简单,如果服务器保存了客户端的相关信息,就必须提供一个所有服务器都能访问的共享缓存,这样才能保证一直使用同一台服务器处理特定客户端的请求,这样的需求很难实现

因为REST架构基于HTTP协议,所以发送密令的最佳方式是使用HTTP认证,基本认证和摘要认证都可以,在HTTP认证中,用户密令包含在请求的Authorization首部中

HTTP认证协议很简单,可以直接实现,不过Flask-HTTPAuth拓展提供了一个便利的包装,可以把协议的细节隐藏在装饰器之中,类似于Flask-Login提供的login_required装饰器

Flask-HTTPAuth使用pip安装,在将HTTP基本认证的扩展进行初始化之前,我们先要创建一个HTTPBasicAuth类对象,和Flask-Login一样,Flask-HTTPAuth不对验证用户命令所需的步骤做任何假设,因此所需的信息在回调函数中提供,下例展示了如何初始化Flask-HTTPAuth扩展,以及如何在回调函数中验证密令

# app/api_1_0/authentication.pyfrom flask_httpauth import HTTPBasicAuthauth = HTTPBasicAuth()@auth.verify_passworddef verify_password(email, password):    if email == '':        g.current_user = AnonymousUser()        return True    user = User.query.filter_by(email = email).first()    if not user:        return False    g.current_user = user    return user.verify_password(password)

由于这种用户认证方法只在API蓝本中使用,所以Flask-HTTPAuth扩展只能在蓝本包中初始化,而不像其他扩展那样要在程序包中初始化

电子邮件和密码使用User模型中现有的方法验证,如果登录密令正确,这个验证回调函数就返回True,否则返回False,API蓝本也支持匿名用户访问,此时客户端发送的电子邮件字段必须为空

验证回调函数把通过认证的用户保存在Flask的全局对象g中,这样一来,视图函数便能进行访问,注意在匿名登录时,这个函数返回True并把Flask-Login提供的AnonymousUser类实例赋值给g.current_user

由于每次请求时都要传送用户密令,所以API路由最好通过安全的HTTP提供,加密所有的请求和响应

如果认证密令不正确,服务器向客户端返回401错误,默认情况下,Flask-HTTPAuth自动生成这个状态码,但为了和API返回的其他错误保持一致,我们可以自定义这个错误响应:

#app/api_1_0/authentication.py#...@auth.error_headlerdef auth_error():    return unauthorized('Invalid credentials')

为了保护路由,可使用装饰器auth.login_required

@api.route('/posts')@auth.login_requireddef get_posts():    pass

不过,这个蓝本中的所有路由都要使用相同的方式进行保护,所以我们可以在before_request处理程序中使用一次login_required装饰器,应用到整个蓝本,如下例所示:

#app/api_1_0/authentication.pyfrom .errors import forbidden@api.before_request@auth.login_requireddef before_request():    if not g.current_user.is_anonymous and \            not g.current_user.comfirmed:        return forbidden('Uncofirmed account')

现在,API蓝本中的所有路由都能进行自动认证,而且作为附加认证,before_request处理程序还会拒绝已通过认证但没有确认账户的用户

基于令牌的认证

每次请求时,客户端都要发送认证密令,为了避免总是发送敏感信息,我们可以提供一种基于令牌的认证方案

使用基于令牌的认证方案时,客户端要先把登录密令发送给一个特殊的URL,从而生成认证令牌,一旦客户端获得令牌,就可用令牌代替登录密令认证请求,处于安全考虑,令牌有过期时间,令牌过期后,客户端必须重新发送登陆密令以生成新令牌,令牌落入他人之手所带来的安全隐患受限于令牌的短暂使用期限,为了生成和验证认证令牌,我们要在User模型中定义两个新方法,这两个新方法用到了itsdangerous包,如下

# app/models.pyclass User(db.Model):   #....    def generate_auth_token(self, expiration):        s = Serializer(current_app.config['SECRET_KEY'],                       expires_in=expiration)        return s.dumps({'id': self.id})    @staticmethod    def verify_auth_token(token):        s = Serializer(current_app.config['SECRET_KEY'])        try:            data = s.loads(token)        except:            return None        return User.query.get(data['id'])

generate_auth_token()方法使用编码后的用户id字段值生成一个签名令牌,还指定了以秒为单位的过期时间,verify_auth_token()方法接受的参数是一个令牌,如果令牌可用就返回对应的对象,verify_auth_token()是静态方法,因为只有解码令牌后才能知道用户是谁

为了能够认证包含令牌的请求,我们必须修改Flask-HTTPAuth提供的verify_password回调,除了普通的密令之外,还要接受令牌,修改后的回调函数如下:

# app/api_1_0/authentication.py@auth.verify_passworddef verify_password(email_or_token, password):    if email_or_token == '':        g.current_user = AnonymousUser()        return True    if password == '':        g.current_user = User.verify_auth_token(email_or_token)        g.token_used = True        return g.current_user is not None    user = User.query.filter_by(email = email_or_token).first()    if not user:        return False    g.current_user = user    g.token_used = False    return user.verify_password(password)

在这个新版本中,第一个认证参数可以是电子邮件地址或认证令牌,如果这个参数为空,那就和之前一样,假定是匿名用户,如果密码为空,那就假定email_or_token参数提供的是令牌,按照令牌的方式进行认证,如果两个参数都不为空,假定使用常规的邮件地址和密码进行认证,在这种实现方式中,基于令牌的认证是可选的,由客户端决定是否使用,为了让视图函数能区分这两种认证方式,我们添加了g.token_used变量

把认证令牌发送给客户端的路由也要添加到API蓝本中,具体实现如下:

# app/api_1_0/authentication.py#...@api.route('/token')def get_token():    if g.current_user.is_anonymous() or g.token_used:        return unauthorized("Invalid credentials")    return jsonify({'token': g.current_user.generate_auth_token(        expiration=3600), 'expiration': 3600})

这个路由也在蓝本中,所以添加到before_request处理程序上的认证机制也会用在这个路由上,为了避免客户端使用旧令牌申请新令牌,要在视图函数中检查g.token_used变量的值,如果使用令牌进行认证就拒绝请求,这个视图函数返回JSON格式的响应,其中包含了过期时间为1小时的令牌,JSON格式的响应也包含过期时间

0 0
原创粉丝点击