Skip to content

Restful API with Flask

8dspaces edited this page Dec 26, 2014 · 5 revisions

一个简单实用的构建restful API 的例子, 把代码敲了下做了下测试

基本功能(实现GET/POST/PUT/DELETE)

代码如下:

    ## file restapp.py
    #!flask/bin/python
    from flask import Flask, jsonify, abort, make_response,request

    app = Flask(__name__)

    tasks = [
        {
            'id': 1,
            'title': u'Buy groceries',
            'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 
            'done': False
        },
        {
            'id': 2,
            'title': u'Learn Python',
            'description': u'Need to find a good Python tutorial on the web', 
            'done': False
        }
    ]

    @app.route('/todo/api/v1.0/tasks', methods=['GET'])
    def get_tasks():
        return jsonify({'tasks': tasks})

    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
    def get_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        return jsonify({'task': task[0]})

    @app.errorhandler(404)
    def page_not_found(e):
        return make_response(jsonify({'error': 'Not found'}), 404)

    @app.route('/todo/api/v1.0/tasks', methods=['POST'])
    def create_task():
        if not request.json or not 'title' in request.json:
            abort(400)
        task = {
            'id': tasks[-1]['id'] + 1,
            'title': request.json['title'],
            'description': request.json.get('description', ""),
            'done': False
        }
        tasks.append(task)
        return jsonify({'task': task}), 201

    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
    def update_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        if not request.json:
            abort(400)
        if 'title' in request.json and type(request.json['title']) != unicode:
            abort(400)
        if 'description' in request.json and type(request.json['description']) is not unicode:
            abort(400)
        if 'done' in request.json and type(request.json['done']) is not bool:
            abort(400)
        task[0]['title'] = request.json.get('title', task[0]['title'])
        task[0]['description'] = request.json.get('description', task[0]['description'])
        task[0]['done'] = request.json.get('done', task[0]['done'])
        return jsonify({'task': task[0]})

    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE'])
    def delete_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        tasks.remove(task[0])
        return jsonify({'result': True})

    if __name__ == '__main__':
        app.run(debug=True)

安全认证

我们已经完成了整个功能,但是我们还有一个问题。web service任何人都可以访问的,这不是一个好主意。

当前service是所有客户端都可以连接的,如果有别人知道了这个API就可以写个客户端随意修改数据了。 大多数教程没有与安全相关的内容, 这是个十分严重的问题。

最简单的办法是在web service中,只允许用户名和密码验证通过的客户端连接。在一个常规的web应用中,应该有登录表单提交去认证,同时 服务器会创建一个会话过程去进行通讯。这个会话过程id会被存储在客户端的cookie里面。不过这样就违返了我们REST中无状态的规则,因此, 我们需求客户端每次都将他们的认证信息发送到服务器

为此我们有两种方法表单认证方法去做,分别是 Basic 和 Digest。 这里有有个小Flask extension可以轻松做到。首先需要安装 Flask-HTTPAuth:

pip install flask-httpauth

假设web service只有用户 ok 和密码为 python 的用户接入。下面就设置了一个Basic HTTP认证:

from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

@auth.get_password
def get_password(username):
    if username == 'ok':
        return 'python'
    return None

@auth.error_handler
def unauthorized():
    return make_response(jsonify({'error': 'Unauthorized access'}), 401)

        if __name__ == '__main__':
            app.run(debug=True)

然后修改上面的例子

@app.route('/todo/api/v1.0/tasks', methods=['GET'])
@auth.login_required
def get_tasks():
    return jsonify({'tasks': tasks})

用curl 做测试, 未通过,因为缺少验证信息

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 401 UNAUTHORIZED
Content-Type: application/json
Content-Length: 36
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:41:14 GMT

{
  "error": "Unauthorized access"
}

然后加入用户信息,再试一次

$ curl -u ok:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 316
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:46:45 GMT

{
  "tasks": [
    {
      "title": "Buy groceries",
      "done": false,
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    },
    {
      "title": "Learn Python",
      "done": false,
      "description": "Need to find a good Python tutorial on the web",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
    }
  ]
}

这个认证extension十分灵活,可以随指定需要验证的APIs。
为了确保登录信息的安全,最好的办法还是使用https加密的通讯方式,客户端与服务器端传输认证信息都是加密过的,防止第三方的人去看到。
当使用浏览器去访问这个接口,会弹出一个丑丑的登录对话框,如果密码错误就回返回401的错误代码。为了防止浏览器弹出验证对话框,客户 端应该处理好这个登录请求。
有一个小技巧可以避免这个问题,就是修改返回的错误代码401。例如修改成403(”Forbidden“)就不会弹出验证对话框了。

@auth.error_handler
def unauthorized():
    return make_response(jsonify({'error': 'Unauthorized access'}), 403)

当然,同时也需要客户端知道这个403错误的意义。