使用 Flask-Login 注册登录

在我们的前几章中,围绕着要讲解的内容持续得再丰富一个 REST 服务。但是,截止到目前为止,我们这个 REST 服务都是没有权限控制的,也就是说,如果将这个 REST 服务发布到外网上去,那么将可以被任何人操作,增删改查都不是问题。

作为我们的重要服务(真的很重要:-D),我们怎么能让别人随便操作我们的数据呢,所以这一章就讲解一下如何使用 Flask 的又一扩展 Flask-Login 来进行访问控制。

安装 Flask-Login

根据在 《本书概述》中列举的那样,我们使用的 Flask-Login 的版本是

  1. Flask-Login==0.3.2

所以安装的话直接使用 pip 安装即可:

  1. pip install Flask-Login==0.3.2

初始化 Flask-Login

和我们在上一章使用 Flask-MongoEngine 一样,使用 Flask-Login 还是依赖于 Flask,所以我们还是需要和 app 这样服务器绑定起来,所以我们一开始还是需要这样和服务器绑定的:

  1. from flask.ext.login import LoginManager
  2. login_manager = LoginManager()
  3. login_manager.init_app(app)

这样就将 Flask-Login 和服务器绑定起来了。但是,这好像没有什么作用啊,我们要怎么登陆呢?Flask-Login 怎么才知道登录的 URL 的是哪个?怎么验证我们的账号密码?怎么才能知道登陆的用户是谁?这些都是关键的问题啊。

设置 Flask-Login

对于前面提到的问题,我们一一解决,解决完之后我们的 Flask-Login 就差不多算是会使用了。

首先是登陆的 URL 是什么?这个在 Flask-Login 中是没有默认的登陆 URL 的,所以需要我们指定:

  1. from flask.ext.login import login_user
  2. login_manager.login_view = 'login'
  3. @app.route('/login', methods=['POST'])
  4. def login():
  5. info = json.loads(request.data)
  6. username = info.get('username', 'guest')
  7. password = info.get('password', '')
  8. user = User.objects(name=username,
  9. password=password).first()
  10. if user:
  11. login_user(user)
  12. return jsonify(user.to_json())
  13. else:
  14. return jsonify({"status": 401,
  15. "reason": "Username or Password Error"})

这里其实就做了两件事:

  1. 指定了 login_view 为 ‘login’
  2. 编写的登陆的代码逻辑

那我们来看第一点,指定 login_view,也就是告诉 Flask 我们的处理的登陆的 URL 是哪个。这里我们发现是 ‘login’,那么 Flask 是怎么根据 login 找到我们的登陆逻辑所在的位置的呢?这里除了 ‘login’ 我们还能填写其他的字符串吗?

这里先给出答案,是不能的,也就是说,在我们这段代码中,必须指定为 ‘login’,这里的 ‘login’ 的意思就是在当前文件找到

  1. def login(self, xxx)

这个函数,然后它就是我们处理登陆逻辑代码所在的地方。

假如说我们处理登陆逻辑的代码没有放在这个文件,而是放在了其他文件,例如 auth.py 里面的 login 函数里面,那么我们就需要指定为:

  1. login_view = 'auth.login'

登陆逻辑

还是看回上一段代码,我们发现这是一个普通的 Flask 处理请求的函数,说普通在于:

  • 从客户端的请求中获得参数,和之前的 CRUD 一样
  • 无论是登陆成功还是失败都返回 json 串给客户端

那么凭什么这段代码就能胜任登陆用户的职责呢?问题的关键就在于

  1. login_user(user)

这一句,仅仅是通过这简单的一句,就将当前用户的状态设置成已登录。这里不做过深入的讲解,只需要知道当这个函数被调用之后,用户的状态就是登陆状态了。

那现在问题是,下次有请求过来,我们怎么知道是不是有用户登陆了,怎么知道是哪个用户?这时我们就会发现我们的 Model 还不够完善,需要完善一下 Model。具体应该这样完善一下:

  1. class User(db.Document):
  2. name = db.StringField()
  3. password = db.StringField()
  4. email = db.StringField()
  5. def to_json(self):
  6. return {"name": self.name,
  7. "email": self.email}
  8. def is_authenticated(self):
  9. return True
  10. def is_active(self):
  11. return True
  12. def is_anonymous(self):
  13. return False
  14. def get_id(self):
  15. return str(self.id)

我们可以看到,这里增加了两个方法,分别是:

  • is_authenticated:当前用户是否被授权,因为我们登陆了就可以操作,所以默认都是被授权的
  • is_anonymous: 用于判断当前用户是否是匿名用户,很明显,如果这个用户登陆了,就必须不是
  • is_active: 用于判断当前用户是否已经激活,已经激活的用户才能登陆
  • get_id: 获取改用户的唯一标示

这里,我们仅仅可以通过 is_authenticated 来判断用户时候有权限操作我们的 API,但是,我们还不能知道当前的登陆用户是谁,所以我们还需要告诉 Flask-Login 如何通过一个 id 获取到用户的方法:

  1. @login_manager.user_loader
  2. def load_user(user_id):
  3. return User.objects(id=user_id).first()

通过指定 user_loader,我们就可以查询到当前的登陆用户是谁了。这样我们就将登陆、判断用户是否登陆都完善起来了。

登陆可见

既然都登陆了,我们就需要控制登陆的权限了,我们设置增加、删除和修改的 REST API 为登陆才能使用,唯有查询的 API 才能随便可见。

控制登陆可用的方法比较简单,只需要加一个 login_required 的装饰器即可。我们还是以之前那些章节的 REST DEMO 为例进行改写:

  1. from flask.ext.login import login_required
  2. @app.route('/', methods=['PUT'])
  3. @login_required
  4. def create_record():
  5. ......
  6. @app.route('/', methods=['POST'])
  7. @login_required
  8. def update_record():
  9. ......
  10. @app.route('/', methods=['DELETE'])
  11. @login_required
  12. def delte_record():
  13. ......

这样我们就限制了增加、修改和删除操作必须登陆用户才能操作,而我们也能记录是哪个用户做的操作了。

用户信息

既然服务器提供了登陆的支持,那么肯定少不了退出登陆的支持;同时,作为客户端,可能关注的是想知道到底有没有登陆?

对于退出登陆,很简单,都根本不需要使用到 User 的这个 Model 了。代码如下:

  1. from flask.ext.login import logout_user
  2. @app.route('/logout', methods=['POST'])
  3. def logout():
  4. logout_user()
  5. return jsonify(**{'result': 200,
  6. 'data': {'message': 'logout success'}})

这里就调用了一个 logout_user 的方法就退出了登陆。

然而即使退出了登陆客户端也不知道,除非尝试请求一下新增、修改或者删除的操作,发现无法操作了,这时就知道了我已经退出登陆了,这样明显不合理!所以,这里再增加一个获取当前登陆用户信息的接口:

  1. from flask.ext.login import current_user
  2. @app.route('/user_info', methods=['POST'])
  3. def user_info():
  4. if current_user.is_authenticated:
  5. resp = {"result": 200,
  6. "data": current_user.to_json()}
  7. else:
  8. resp = {"result": 401,
  9. "data": {"message": "user no login"}}
  10. return jsonify(**resp)

这里一个重要的点就是第一句,这里有一个成员叫做 current_user,这个变量表示的是当前请求的登陆用户,如果登陆了,那么它就是我们设置的 Model User 的对象,根据我们的 Model 定义, is_authenticated 一直为 True,表示登陆了;如果没有登陆,那么它就是默认的匿名用户 AnonymousUserMixin 的对象,is_authenticated 就为 False,就表示没有登陆。

如果登陆的话,那么 current_user 就是 User 的对象了,那么 to_json 方法就可以返回当前登陆用户的用户信息了,这样的话,我们就可以编写获取用户信息的 API 了。

本章的完整代码为:

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import json
  4. from flask import Flask, request, jsonify
  5. from flask.ext.login import (current_user, LoginManager,
  6. login_user, logout_user,
  7. login_required)
  8. from flask_mongoengine import MongoEngine
  9. app = Flask(__name__)
  10. app.config['MONGODB_SETTINGS'] = {
  11. 'db': 'the_way_to_flask',
  12. 'host': 'localhost',
  13. 'port': 27017
  14. }
  15. app.secret_key = 'youdontknowme'
  16. db = MongoEngine()
  17. login_manager = LoginManager()
  18. db.init_app(app)
  19. login_manager.init_app(app)
  20. login_manager.login_view = 'login'
  21. @login_manager.user_loader
  22. def load_user(user_id):
  23. return User.objects(id=user_id).first()
  24. @app.route('/login', methods=['POST'])
  25. def login():
  26. info = json.loads(request.data)
  27. username = info.get('username', 'guest')
  28. password = info.get('password', '')
  29. user = User.objects(name=username,
  30. password=password).first()
  31. if user:
  32. login_user(user)
  33. return jsonify(user.to_json())
  34. else:
  35. return jsonify({"status": 401,
  36. "reason": "Username or Password Error"})
  37. @app.route('/logout', methods=['POST'])
  38. def logout():
  39. logout_user()
  40. return jsonify(**{'result': 200,
  41. 'data': {'message': 'logout success'}})
  42. @app.route('/user_info', methods=['POST'])
  43. def user_info():
  44. if current_user.is_authenticated:
  45. resp = {"result": 200,
  46. "data": current_user.to_json()}
  47. else:
  48. resp = {"result": 401,
  49. "data": {"message": "user no login"}}
  50. return jsonify(**resp)
  51. class User(db.Document):
  52. name = db.StringField()
  53. password = db.StringField()
  54. email = db.StringField()
  55. def to_json(self):
  56. return {"name": self.name,
  57. "email": self.email}
  58. def is_authenticated(self):
  59. return True
  60. def is_active(self):
  61. return True
  62. def is_anonymous(self):
  63. return False
  64. def get_id(self):
  65. return str(self.id)
  66. @app.route('/', methods=['GET'])
  67. def query_records():
  68. name = request.args.get('name')
  69. user = User.objects(name=name).first()
  70. if not user:
  71. return jsonify({'error': 'data not found'})
  72. else:
  73. return jsonify(user.to_json())
  74. @app.route('/', methods=['PUT'])
  75. @login_required
  76. def create_record():
  77. record = json.loads(request.data)
  78. user = User(name=record['name'],
  79. password=record['password'],
  80. email=record['email'])
  81. user.save()
  82. return jsonify(user.to_json())
  83. @app.route('/', methods=['POST'])
  84. @login_required
  85. def update_record():
  86. record = json.loads(request.data)
  87. user = User.objects(name=record['name']).first()
  88. if not user:
  89. return jsonify({'error': 'data not found'})
  90. else:
  91. user.update(email=record['email'],
  92. password=record['password'])
  93. return jsonify(user.to_json())
  94. @app.route('/', methods=['DELETE'])
  95. @login_required
  96. def delte_record():
  97. record = json.loads(request.data)
  98. user = User.objects(name=record['name']).first()
  99. if not user:
  100. return jsonify({'error': 'data not found'})
  101. else:
  102. user.delete()
  103. return jsonify(user.to_json())
  104. if __name__ == "__main__":
  105. app.run(port=8080, debug=True)