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

4. app = Flask(__name__)
5. auth = HTTPBasicAuth()

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

  • 接着,写一个验证用户密码的回调函数
1. from werkzeug.security import generate_password_hash, check_password_hash

3. # 模拟数据库
4. books = ['The Name of the Rose', 'The Historian', 'Rebecca']
5. users = [
6. {'username': 'ethan', 'password': generate_password_hash('6666')},
7. {'username': 'peter', 'password': generate_password_hash('4567')}
8. ]

10. # 回调函数
11. @auth.verify_password
12. def verify_password(username, password):
13. user = filter(lambda user: user['username'] == username, users)

15. if user and check_password_hash(user[0]['password'], password):
16. g.user = username
17. return True
18. return False

上面,为了对密码进行加密以及认证,我们使用 werkzeug.security 包提供的 generate_password_hash 和 check_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>'

9. books.append(title)
10. flash("add book successfully!")
11. return redirect(url_for('index'))

上面完整的代码如下:

1. $ cat app.py

3. # -*- coding: utf-8 -*-

5. from flask import Flask, url_for, render_template, request, flash, \
6. redirect, make_response, jsonify, g
7. from werkzeug.security import generate_password_hash, check_password_hash
8. from flask_httpauth import HTTPBasicAuth

11. app = Flask(__name__)
12. app.config['SECRET_KEY'] = 'secret key'

14. auth = HTTPBasicAuth()

16. # 模拟数据库
17. books = ['The Name of the Rose', 'The Historian', 'Rebecca']
18. users = [
19. {'username': 'ethan', 'password': generate_password_hash('6666')},
20. {'username': 'peter', 'password': generate_password_hash('4567')}
21. ]

23. # 回调函数
24. @auth.verify_password
25. def verify_password(username, password):
26. user = filter(lambda user: user['username'] == username, users)

28. if user and check_password_hash(user[0]['password'], password):
29. g.user = username
30. return True
31. return False

33. # 不需认证,可直接访问
34. @app.route('/', methods=['GET'])
35. def index():
36. return render_template(
37. 'book.html',
38. books=books
39. )

41. # 需要认证
42. @app.route('/', methods=['POST'])
43. @auth.login_required
44. def add_book():
45. _form = request.form
46. title = _form["title"]
47. if not title:
48. return '<h1>invalid request</h1>'

50. books.append(title)
51. flash("add book successfully!")
52. return redirect(url_for('index'))

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

58. if __name__ == '__main__':
59. app.run(host='127.0.0.1', port=5206, debug=True)
1. $ cat templates/layout.html

3. <!doctype html>
4. <title>Hello Sample</title>

6. <div class="page">
7. {% block body %} {% endblock %}
8. </div>

10. {% for message in get_flashed_messages() %}
11. {{ message }}
12. {% endfor %}
1. $ cat templates/book.html

3. {% extends "layout.html" %}
4. {% block body %}
5. {% if books %}
6. {% for book in books %}
7. <ul>
8. <li> {{ book }} </li>
9. </ul>
10. {% endfor %}
11. {% else %}
12. <p> The book doesn't exists! </p>
13. {% endif %}

15. <form method="post" action="{{ url_for('add_book') }}">
16. <input id="title" name="title" placeholder="add book" type="text">
17. <button type="submit">Submit</button>
18. </form>

20. {% endblock %}

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

auth

基于 token 的认证

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

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

1. from flask import Flask, g
2. from flask_httpauth import HTTPTokenAuth

4. app = Flask(__name__)
5. auth = HTTPTokenAuth(scheme='Token')

7. tokens = {
8. "secret-token-1": "john",
9. "secret-token-2": "susan"
10. }

12. # 回调函数,验证 token 是否合法
13. @auth.verify_token
14. def verify_token(token):
15. if token in tokens:
16. g.current_user = tokens[token]
17. return True
18. return False

20. # 需要认证
21. @app.route('/')
22. @auth.login_required
23. def index():
24. return "Hello, %s!" % g.current_user

26. if __name__ == '__main__':
27. 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 -*-

3. from flask import Flask, g
4. from flask_httpauth import HTTPTokenAuth
5. from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

7. app = Flask(__name__)
8. app.config['SECRET_KEY'] = 'secret key here'

10. auth = HTTPTokenAuth(scheme='Token')

12. # 实例化一个签名序列化对象 serializer,有效期 10 分钟
13. serializer = Serializer(app.config['SECRET_KEY'], expires_in=600)

15. users = ['john', 'susan']

17. # 生成 token
18. for user in users:
19. token = serializer.dumps({'username': user})
20. print('Token for {}: {}\n'.format(user, token))

22. # 回调函数,对 token 进行验证
23. @auth.verify_token
24. def verify_token(token):
25. g.user = None
26. try:
27. data = serializer.loads(token)
28. except:
29. return False
30. if 'username' in data:
31. g.user = data['username']
32. return True
33. return False

35. # 对视图进行认证
36. @app.route('/')
37. @auth.login_required
38. def index():
39. return "Hello, %s!" % g.user

41. if __name__ == '__main__':
42. app.run()

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

1. $ python app.py
2. Token for John: eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ3NjY5NzE0NCwiaWF0IjoxNDc2Njk1MzQ0fQ.eyJ1c2VybmFtZSI6IkpvaG4ifQ.vQu0z0Pos2Tgt5jBYMY5IYWUkTK9k3wE_RqvYHDqtyM

4. Token for Susan: eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ3NjY5NzE0NCwiaWF0IjoxNDc2Njk1MzQ0fQ.eyJ1c2VybmFtZSI6IlN1c2FuIn0.rk8JaTRwag0qiF9_KuRodhw6wx2ZWkOEhFln9hzOLP0

6. * 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/

3. # 结果
4. $ Hello, john!

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

更多阅读