Flask-HTTPAuth

在 Web 应用中,我们经常需要保护我们的 api,以避免非法访问。比如,只允许登录成功的用户发表评论等。Flask-HTTPAuth 扩展可以很好地对 HTTP 的请求进行认证,不依赖于 Cookie 和 Session。本文主要介绍两种认证的方式:基于密码和基于令牌 (token)。

安装

使用 pip 安装:

  1. $ pip install Flask-HTTPAuth

基于密码的认证

为了简化代码,这里我们就不引入数据库了。

  • 首先,创建扩展对象实例
  1. from flask import Flask
  2. from flask_httpauth import HTTPBasicAuth
  3. app = Flask(__name__)
  4. auth = HTTPBasicAuth()

这里有一点需要注意的是,我们创建了一个 auth 对象,但没有传入 app 对象,这跟其他扩展初始化实例有一点区别。

  • 接着,写一个验证用户密码的回调函数
  1. from werkzeug.security import generate_password_hash, check_password_hash
  2. # 模拟数据库
  3. books = ['The Name of the Rose', 'The Historian', 'Rebecca']
  4. users = [
  5. {'username': 'ethan', 'password': generate_password_hash('6666')},
  6. {'username': 'peter', 'password': generate_password_hash('4567')}
  7. ]
  8. # 回调函数
  9. @auth.verify_password
  10. def verify_password(username, password):
  11. user = filter(lambda user: user['username'] == username, users)
  12. if user and check_password_hash(user[0]['password'], password):
  13. g.user = username
  14. return True
  15. return False

上面,为了对密码进行加密以及认证,我们使用 werkzeug.security 包提供的 generate_password_hashcheck_password_hash 方法:generate_password_hash 会对给定的字符串,生成其加盐的哈希值;check_password_hash 验证传入的明文字符串与哈希值是否一致。

  • 然后,我们在需要认证的视图函数上,加上 @auth.login_required 装饰器,比如
  1. @app.route('/', methods=['POST'])
  2. @auth.login_required
  3. def add_book():
  4. _form = request.form
  5. title = _form["title"]
  6. if not title:
  7. return '<h1>invalid request</h1>'
  8. books.append(title)
  9. flash("add book successfully!")
  10. return redirect(url_for('index'))

上面完整的代码如下:

  1. $ cat app.py
  2. # -*- coding: utf-8 -*-
  3. from flask import Flask, url_for, render_template, request, flash, \
  4. redirect, make_response, jsonify, g
  5. from werkzeug.security import generate_password_hash, check_password_hash
  6. from flask_httpauth import HTTPBasicAuth
  7. app = Flask(__name__)
  8. app.config['SECRET_KEY'] = 'secret key'
  9. auth = HTTPBasicAuth()
  10. # 模拟数据库
  11. books = ['The Name of the Rose', 'The Historian', 'Rebecca']
  12. users = [
  13. {'username': 'ethan', 'password': generate_password_hash('6666')},
  14. {'username': 'peter', 'password': generate_password_hash('4567')}
  15. ]
  16. # 回调函数
  17. @auth.verify_password
  18. def verify_password(username, password):
  19. user = filter(lambda user: user['username'] == username, users)
  20. if user and check_password_hash(user[0]['password'], password):
  21. g.user = username
  22. return True
  23. return False
  24. # 不需认证,可直接访问
  25. @app.route('/', methods=['GET'])
  26. def index():
  27. return render_template(
  28. 'book.html',
  29. books=books
  30. )
  31. # 需要认证
  32. @app.route('/', methods=['POST'])
  33. @auth.login_required
  34. def add_book():
  35. _form = request.form
  36. title = _form["title"]
  37. if not title:
  38. return '<h1>invalid request</h1>'
  39. books.append(title)
  40. flash("add book successfully!")
  41. return redirect(url_for('index'))
  42. @auth.error_handler
  43. def unauthorized():
  44. return make_response(jsonify({'error': 'Unauthorized access'}), 401)
  45. if __name__ == '__main__':
  46. app.run(host='127.0.0.1', port=5206, debug=True)
  1. $ cat templates/layout.html
  2. <!doctype html>
  3. <title>Hello Sample</title>
  4. <div class="page">
  5. {% block body %} {% endblock %}
  6. </div>
  7. {% for message in get_flashed_messages() %}
  8. {{ message }}
  9. {% endfor %}
  1. $ cat templates/book.html
  2. {% extends "layout.html" %}
  3. {% block body %}
  4. {% if books %}
  5. {% for book in books %}
  6. <ul>
  7. <li> {{ book }} </li>
  8. </ul>
  9. {% endfor %}
  10. {% else %}
  11. <p> The book doesn't exists! </p>
  12. {% endif %}
  13. <form method="post" action="{{ url_for('add_book') }}">
  14. <input id="title" name="title" placeholder="add book" type="text">
  15. <button type="submit">Submit</button>
  16. </form>
  17. {% endblock %}

保存上面的代码,启动该应用,当我们试图提交书籍时,你会浏览器弹出了一个登录框,如下,只有输入正确的用户名和密码,书籍才会添加成功。

auth

基于 token 的认证

很多时候,我们并不直接通过密码做认证,比如当我们把 api 开放给第三方的时候,我们不可能给它们提供密码,而是对它们进行授权,还可能会有时间限制,比如半年或一年等。这时候,我们往往通过一个令牌,也就是 token 来做认证,Flask-HTTPAuth 提供了 HTTPTokenAuth 对象来做这件事。

我们用官方文档的例子来说明。

  1. from flask import Flask, g
  2. from flask_httpauth import HTTPTokenAuth
  3. app = Flask(__name__)
  4. auth = HTTPTokenAuth(scheme='Token')
  5. tokens = {
  6. "secret-token-1": "john",
  7. "secret-token-2": "susan"
  8. }
  9. # 回调函数,验证 token 是否合法
  10. @auth.verify_token
  11. def verify_token(token):
  12. if token in tokens:
  13. g.current_user = tokens[token]
  14. return True
  15. return False
  16. # 需要认证
  17. @app.route('/')
  18. @auth.login_required
  19. def index():
  20. return "Hello, %s!" % g.current_user
  21. if __name__ == '__main__':
  22. app.run()

上面,我们在初始化 HTTPTokenAuth 对象时,传入了 scheme='Token'。这个 scheme,是我们在发送请求时,在 HTTP 头 Authorization 中要用的 scheme 字段。用 curl 测试如下:

  1. $ curl -X GET -H "Authorization: Token secret-token-1" http://localhost:5000/

结果:

  1. Hello, john!

使用 itsdangerous 库来管理令牌

上面的令牌还是比较薄弱的,在实际使用中,我们需要使用加密的签名(Signature)作为令牌,它能够根据用户信息生成相关的签名,并且很难被篡改。itsdangerous 提供了上述功能,在使用之前请使用 pip 安装: $ pip install itsdangerous

改进后的代码如下:

  1. # -*- coding: utf-8 -*-
  2. from flask import Flask, g
  3. from flask_httpauth import HTTPTokenAuth
  4. from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
  5. app = Flask(__name__)
  6. app.config['SECRET_KEY'] = 'secret key here'
  7. auth = HTTPTokenAuth(scheme='Token')
  8. # 实例化一个签名序列化对象 serializer,有效期 10 分钟
  9. serializer = Serializer(app.config['SECRET_KEY'], expires_in=600)
  10. users = ['john', 'susan']
  11. # 生成 token
  12. for user in users:
  13. token = serializer.dumps({'username': user})
  14. print('Token for {}: {}\n'.format(user, token))
  15. # 回调函数,对 token 进行验证
  16. @auth.verify_token
  17. def verify_token(token):
  18. g.user = None
  19. try:
  20. data = serializer.loads(token)
  21. except:
  22. return False
  23. if 'username' in data:
  24. g.user = data['username']
  25. return True
  26. return False
  27. # 对视图进行认证
  28. @app.route('/')
  29. @auth.login_required
  30. def index():
  31. return "Hello, %s!" % g.user
  32. if __name__ == '__main__':
  33. app.run()

将上面代码保存为 app.py,在终端运行,可看到类似如下的输出:

  1. $ python app.py
  2. Token for John: eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ3NjY5NzE0NCwiaWF0IjoxNDc2Njk1MzQ0fQ.eyJ1c2VybmFtZSI6IkpvaG4ifQ.vQu0z0Pos2Tgt5jBYMY5IYWUkTK9k3wE_RqvYHDqtyM
  3. Token for Susan: eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ3NjY5NzE0NCwiaWF0IjoxNDc2Njk1MzQ0fQ.eyJ1c2VybmFtZSI6IlN1c2FuIn0.rk8JaTRwag0qiF9_KuRodhw6wx2ZWkOEhFln9hzOLP0
  4. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

使用 curl 测试如下:

  1. $ curl -X GET -H "Authorization: Token eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ3NjY5NzE0NCwiaWF0IjoxNDc2Njk1MzQ0fQ.eyJ1c2VybmFtZSI6IkpvaG4ifQ.vQu0z0Pos2Tgt5jBYMY5IYWUkTK9k3wE_RqvYHDqtyM" http://localhost:5000/
  2. # 结果
  3. $ Hello, john!

本节的代码可在这里下载。

更多阅读