加强 RESTful web service 的安全性

我们已经完成了我们 web service 的大部分功能,但是仍然有一个问题。我们的 web service 对任何人都是公开的,这并不是一个好主意。

我们有一个可以管理我们的待办事项完整的 web service,但在当前状态下的 web service 是开放给所有的客户端。 如果一个陌生人弄清我们的 API 是如何工作的,他或她可以编写一个客户端访问我们的 web service 并且毁坏我们的数据。

大部分初级的教程会忽略这个问题并且到此为止。在我看来这是一个很严重的问题,我必须指出。

确保我们的 web service 安全服务的最简单的方法是要求客户端提供一个用户名和密码。在常规的 web 应用程序会提供一个登录的表单用来认证,并且服务器会创建一个会话为登录的用户以后的操作使用,会话的 id 以 cookie 形式存储在客户端浏览器中。然而 REST 的规则之一就是 “无状态”, 因此我们必须要求客户端在每一次请求中提供认证的信息。

我们一直试着尽可能地坚持 HTTP 标准协议。既然我们需要实现认证我们需要在 HTTP 上下文中去完成,HTTP 协议提供了两种认证机制: Basic 和 Digest

有一个小的 Flask 扩展能够帮助我们,我们可以先安装 Flask-HTTPAuth:

  1. $ flask/bin/pip install flask-httpauth

比方说,我们希望我们的 web service 只让访问用户名 miguel 和密码 python 的客户端访问。 我们可以设置一个基本的 HTTP 验证如下:

  1. from flask.ext.httpauth import HTTPBasicAuth
  2. auth = HTTPBasicAuth()
  3.  
  4. @auth.get_password
  5. def get_password(username):
  6. if username == 'miguel':
  7. return 'python'
  8. return None
  9.  
  10. @auth.error_handler
  11. def unauthorized():
  12. return make_response(jsonify({'error': 'Unauthorized access'}), 401)

get_password 函数是一个回调函数,Flask-HTTPAuth 使用它来获取给定用户的密码。在一个更复杂的系统中,这个函数是需要检查一个用户数据库,但是在我们的例子中只有单一的用户因此没有必要。

error_handler 回调函数是用于给客户端发送未授权错误代码。像我们处理其它的错误代码,这里我们定制一个包含 JSON 数据格式而不是 HTML 的响应。

随着认证系统的建立,所剩下的就是把需要认证的函数添加 @auth.login_required 装饰器。例如:

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

如果现在要尝试使用 curl 调用这个函数我们会得到:

  1. $ curl -i http://localhost:5000/todo/api/v1.0/tasks
  2. HTTP/1.0 401 UNAUTHORIZED
  3. Content-Type: application/json
  4. Content-Length: 36
  5. WWW-Authenticate: Basic realm="Authentication Required"
  6. Server: Werkzeug/0.8.3 Python/2.7.3
  7. Date: Mon, 20 May 2013 06:41:14 GMT
  8.  
  9. {
  10. "error": "Unauthorized access"
  11. }

为了能够调用这个函数我们必须发送我们的认证凭据:

  1. $ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks
  2. HTTP/1.0 200 OK
  3. Content-Type: application/json
  4. Content-Length: 316
  5. Server: Werkzeug/0.8.3 Python/2.7.3
  6. Date: Mon, 20 May 2013 06:46:45 GMT
  7.  
  8. {
  9. "tasks": [
  10. {
  11. "title": "Buy groceries",
  12. "done": false,
  13. "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
  14. "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
  15. },
  16. {
  17. "title": "Learn Python",
  18. "done": false,
  19. "description": "Need to find a good Python tutorial on the web",
  20. "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
  21. }
  22. ]
  23. }

认证扩展给予我们很大的自由选择哪些函数需要保护,哪些函数需要公开。

为了确保登录信息的安全应该使用 HTTP 安全服务器(例如: https://…),这样客户端和服务器之间的通信都是加密的,以防止传输过程中第三方看到认证的凭据。

让人不舒服的是当请求收到一个 401 的错误,网页浏览都会跳出一个丑陋的登录框,即使请求是在后台发生的。因此如果我们要实现一个完美的 web 服务器的话,我们就需要禁止跳转到浏览器显示身份验证对话框,让我们的客户端应用程序自己处理登录。

一个简单的方式就是不返回 401 错误。403 错误是一个令人青睐的替代,403 错误表示 “禁止” 的错误:

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