传送门:系列教程前言及整理贴 测试开发——Flask 入门教程系列 (整理及前言)
上一章我们为 TODO APP 引入了数据库,若我们开启服务至外网,任何人都可以操作,这就引入了用户登录概念。目前几乎所有网站、APP 都需要注册登录。所以,如何加入注册登录是我们必须要了解及掌握的。
本章将讲解如何使用 Flask 的又一扩展 Flask-Login 来进行访问控制。可查阅:Flask-Login 官方文档 。
我们先不用急于学习 Flask-Login,而是回顾上一章的内容,创建下用户模型。
养成好习惯,先设想下用户的数据,从而设计用户数据结构:
"user": {
"user_id": 1,
"name": "Mike",
"email": "Mike@gmail.com",
"pwd": "123456ab",
"createtime": 2018-02-17 17:02:19.15
}
!注意:这边 pwd 未加密存储,实际生产中加密是必须的,至于如何加密这边就不深入了。
这边先写两个简单的 API 用以 获取用户信息以及 新增用户(后续会改写这两个 API),API 设计如下:
POST /user 新增用户
请求传参:
Headers Content-Type: application/json
请求体为 Json 格式,name、pwd 为必传项,email 为选传项;若已存在同名的用户,返回错误
示例:
{
"name": "Mike",
"email": "Mike@gmail.com",
"pwd": "123456ab"
}
返回示例:
{
"status": 0,
"user_id": 1
}
若传参错误,则返回:
{
"err": "Request not Json or miss name/pwd."
}
若 name 已存在,则返回:
{
"err": "Name is already existed."
}
若数据存储错误,则返回:
{
"err": "Save Error. Please check your input length: pwd>6, name<100, email<200."
}
GET /user/user_id 获取指定用户的信息
请求传参:URL 中需带入 user_id
返回示例:
{
"status": 0,
"user": {
"email": "Mike@gmail.com",
"name": "Mike",
"user_id": 1
}
}
若对应 id 的 user 不存在,则返回:
{
"err": "Not found."
}
若已掌握上一章内容,代码并不难写(可先不要看,而是试着自己写下,就当回顾上节学习内容了):
#!/usr/bin/env python3
from flask import Flask, jsonify, request
from flask_mongoengine import MongoEngine
from datetime import datetime
app = Flask(__name__)
app.config['MONGODB_SETTINGS'] = {
'db': 'todo',
'host': 'localhost',
'port': 27017
}
db = MongoEngine()
db.init_app(app)
class User(db.Document):
user_id = db.IntField(required=True)
name = db.StringField(required=True, max_length=100)
email = db.StringField(max_length=200)
pwd = db.StringField(requied=True, min_length=6)
createtime = db.DateTimeField(required=True)
def to_json(self):
return {
"user_id": self.user_id,
"name": self.name,
"email": self.email
}
@app.route('/user', methods=['POST'])
def postUser():
if not request.json or not 'name' in request.json or not 'pwd' in request.json:
return jsonify({'err': 'Request not Json or miss name or pwd'})
elif User.objects(name=request.json['name']).first():
return jsonify({'err': 'Name is already existed.'})
else:
try:
user = User(
user_id=User.objects().count() + 1,
name=request.json['name'],
email=request.json['email'] if 'email' in request.json else "",
pwd=request.json['pwd'],
createtime=datetime.now()
)
user.save()
except Exception as e:
return jsonify({'err': 'Save Error. Please check your input length: pwd>6, name<100, email<200.'})
return jsonify({'status': 0, 'user_id': user['user_id']})
@app.route('/user/<int:user_id>', methods=['GET'])
def getUser(user_id):
user = User.objects(user_id=user_id).first()
if not user:
return jsonify({'err': 'Not found.'})
else:
return jsonify({'status': 0, 'user': user.to_json()})
if __name__ == '__main__':
app.run(debug=True)
简单的注册登录功能应该都不陌生,我们设计注册、登录及用户信息等基础功能如下:
实际生产中,注册、登录的涉及内容很多,且大多涉及账户安全,如验证码、密码加密、邮箱找回密码等,这边就不深究了。(有兴趣的可以 Google 自学,目前已有不少成熟的解决方案)
总之,我们设计 API 如下:
POST /register 用户注册
请求传参:
Headers Content-Type: application/json
请求体为 Json 格式,name、pwd 为必传项,email 为选传项
示例:
{
"name": "Mike",
"email": "Mike@gmail.com",
"pwd": "123456ab"
}
返回示例:
{
"status": 0,
"user_id": 1,
"msg": "Register success."
}
若传参缺省错误,则返回:
{
"err": "Request not Json or miss name/pwd."
}
若 name 已存在,则返回:
{
"err": "Name is already existed."
}
若数据存储错误,则返回:
用户输入数据长度不满足数据模型要求也会返回该错,数据长度的限制前后端均都要有。
{
"err": "Register error."
}
POST /login 用户登录
请求传参:
Headers Content-Type: application/json
请求体为 Json 格式,name、pwd 为必传项
示例:
{
"name": "Mike",
"pwd": "123456ab"
}
返回示例:
{
"status": 0,
"user_id": 1,
"msg": "Login success."
}
若登录失败,则返回:
{
"err": "Login fail."
}
POST /logout 用户登出
返回示例:
{
"status": 0,
"msg": "Logout success."
}
GET /user 获取当前登录的用户信息
返回示例:
{
"status": 0,
"user": {
"email": "Mike@gmail.com",
"name": "Mike",
"user_id": 1
}
}
若用户未登录,则返回:
{
"err": "Not login."
}
PUT /user/email 当前登录的用户更改邮箱
请求传参:email 必传
{
"email": "Mike@163.com"
}
返回示例:
{
"msg": "Email has been modified.",
"status": 0,
"user": {
"email": "Mike@163.com",
"name": "Mike",
"user_id": 1
}
}
PUT /user/pwd 当前登录的用户更改密码
请求传参:current_pwd - 原密码;new_pwd - 新密码;
{
"current_pwd": "123456ab",
"new_pwd": "123456ba"
}
返回示例:
{
"status": 0,
"user_id": 1,
"msg": "PWD has been modified."
}
若用户输入的原密码错误,则返回:
{
"err": "current_pwd is not right."
}
如同使用 Flask-MongoEngine 一样,Flask-Login 也需要导入以及和 app 服务器进行初始化绑定关联,如下:
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
这样就将 Flask-Login 和服务器绑定起来了。但是,Flask-Login 怎么才知道登录的 URL 的是哪个?怎么验证我们的账号密码?怎么才能知道登陆的用户是谁?这些都是关键的问题,别急,慢慢一一道来。
注册 API 逻辑不涉及登录态,但我们需要改写下之前 postUser() 的部分名称及文字:
@app.route('/register', methods=['POST'])
def registerUser():
if not request.json or not 'name' in request.json or not 'pwd' in request.json:
return jsonify({'err': 'Request not Json or miss name/pwd'})
elif User.objects(name=request.json['name']).first():
return jsonify({'err': 'Name is already existed.'})
else:
user_id = User.objects().count() + 1
user = User(
uuid=shortuuid.uuid(),
user_id=user_id,
name=request.json['name'],
email=request.json['email'] if 'email' in request.json else "",
pwd=request.json['pwd'],
createtime=datetime.now()
)
try:
user.save()
except Exception:
return jsonify({'err': 'Register error.'})
return jsonify({'status': 0, 'user_id': user['user_id'], 'msg': 'Register success.'})
问题还是比较多的,例如登陆的 URL 是什么?登录 API 后如何成功后保留用户登录态?先看新增的代码吧:
...
from flask.ext.login import LoginManager, login_user
...
login_manager = LoginManager()
login_manager.init_app(app)
app.secret_key = 'secret_key'
class User(db.Document):
...
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return str(self.user_id)
...
@user4oader
def load_user(user_id):
return User.objects(user_id=user_id).first()
@app.route('/login', methods=['POST'])
def login():
if not request.json or not 'name' in request.json or not 'pwd' in request.json:
return jsonify({'err': 'Request not Json or miss name/pwd'})
else:
user = User.objects(
name=request.json['name'], pwd=request.json['pwd']).first()
if user:
login_user(user)
return jsonify({'status': 0, 'user_id': user.get_id(), 'msg': 'Login success.'})
else:
return jsonify({'err': 'Login fail.'})
就以上代码加以讲解:
from flask_login import LoginManager, login_user
,LoginManager
处理 app 初始化绑定;login_user
函数:将当前用户的状态设置成已登录(flask-login.login_user)
app.secret_key = 'itsasecrectkey'
加密 session 用
class User(db.Document):
...
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return str(self.user_id)
这边我们完善了 User Class, 简单介绍如下:( flask-login user class )
python
@login_manager.user_loader
def load_user(user_id):
return User.objects(user_id=user_id).first()
需要告诉 Flask-Login 如何通过一个 id 获取用户,通过指定 user_loader,就可以查询到当前的登陆用户。(flask_login.LoginManager.user_loader)
实际用户每次登录登出都可以记录下来便于用户行为分析,像是登录设备、时间等,这边就不多加详述了。
注册成功是否保留登录态?视具体情况来定,若需要,可以在注册用户的代码里加上 login_user(user)
即可
完成了用户登录,是不是觉得 Flask-Login 很强大?用户登出就更不难理解了,仅需一个函数:
logout_user()
函数:当前登录用户退出登录状态 (flask_login.logout_user)
使用该函数,该 API 的代码就完成了:
from flask.ext.login import LoginManager, login_user, logout_user
...
@app.route('/logout', methods=['POST'])
def logout():
logout_user()
return jsonify({'status': 0, 'msg':'Logout success.'})
用户登录态保留后,并不需传递 user_id 参数,故改写之前的 getUser()。
这边再引入一个重要概念,即 current_user
该变量表示当前登录的用户:
若已登陆,那么它就是我们设置的 Model User 的对象, is_authenticated 为 True;
若未登陆,那么 is_authenticated 就为 False。 (flask_login.current_user)
代码修改如下:
from flask_login import LoginManager, login_user, logout_user, current_user
...
@app.route('/user', methods=['GET'])
def getUser():
if current_user.is_authenticated:
return jsonify({'status': 0, 'user': current_user.to_json()})
else:
return jsonify({'err': 'Not login.'})
写好了该 API,我们可以进一步验证 登录/登出 API 的功能是否正确了~(之前写的没测试总感觉没谱啊哈哈)
更改用户的密码、邮箱,此类型操作要求仅登录态才可见(未登录会报 401 错误),实际上很多 API 都有此要求。
Flask-Login 处理起来相当容易,只需要加 login_required
的装饰器即可(flask_login.login_required)
同样的,我们引入该函数并写更改用户邮箱及密码的 API 如下:
from flask_login import LoginManager, login_user, logout_user, current_user, login_required
...
@app.route('/user/pwd', methods=['PUT'])
@login_required
def putUserPWD():
if not request.json or not 'current_pwd' in request.json or not 'new_pwd' in request.json:
return jsonify({'err': 'Request not Json or miss current_pwd/new_pwd'})
else:
current_pwd = current_user.pwd
if not request.json['current_pwd'] == current_pwd:
return jsonify({'err': 'current_pwd is not right.'})
else:
current_user.pwd = request.json['new_pwd']
try:
current_user.save()
except Exception:
return jsonify({'err': 'Modify PWD error.'})
return jsonify({'status': 0, 'msg': 'PWD has been modified.', 'user_id': current_user.user_id})
完整的代码如下,基本实现了用户的注册、登录、登出,修改邮箱及密码。
#!/usr/bin/env python3
from flask import Flask, jsonify, request
from flask_mongoengine import MongoEngine
from flask_login import LoginManager, login_user, logout_user, current_user, login_required
from datetime import datetime
app = Flask(__name__)
app.config['MONGODB_SETTINGS'] = {
'db': 'user',
'host': 'localhost',
'port': 27017
}
db = MongoEngine()
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
app.secret_key = 'secret_key'
class User(db.Document):
user_id = db.IntField(required=True)
name = db.StringField(required=True, max_length=100)
email = db.StringField(max_length=200)
pwd = db.StringField(requied=True, min_length=6)
createtime = db.DateTimeField(required=True)
def to_json(self):
return {
"user_id": self.user_id,
"name": self.name,
"email": self.email
}
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return str(self.user_id)
@user11oader
def load_user(user_id):
return User.objects(user_id=user_id).first()
@app.route('/register', methods=['POST'])
def registerUser():
if not request.json or not 'name' in request.json or not 'pwd' in request.json:
return jsonify({'err': 'Request not Json or miss name/pwd'})
elif User.objects(name=request.json['name']).first():
return jsonify({'err': 'Name is already existed.'})
else:
user = User(
user_id=User.objects().count() + 1,
name=request.json['name'],
email=request.json['email'] if 'email' in request.json else "",
pwd=request.json['pwd'],
createtime=datetime.now()
)
try:
user.save()
except Exception:
return jsonify({'err': 'Register error.'})
return jsonify({'status': 0, 'user_id': user['user_id'], 'msg': 'Register success.'})
@app.route('/login', methods=['POST'])
def login():
if not request.json or not 'name' in request.json or not 'pwd' in request.json:
return jsonify({'err': 'Request not Json or miss name/pwd'})
else:
user = User.objects(
name=request.json['name'], pwd=request.json['pwd']).first()
if user:
login_user(user)
return jsonify({'status': 0, 'user_id': user.get_id(), 'msg': 'Login success.'})
else:
return jsonify({'err': 'Login fail.'})
@app.route('/logout', methods=['POST'])
def logout():
logout_user()
return jsonify({'status': 0, 'msg': 'Logout success.'})
@app.route('/user', methods=['GET'])
def getUser():
if current_user.is_authenticated:
return jsonify({'status': 0, 'user': current_user.to_json()})
else:
return jsonify({'err': 'Not login.'})
@app.route('/user/email', methods=['PUT'])
@login_required
def putUserEmail():
if not request.json or not 'email' in request.json:
return jsonify({'err': 'Request not Json or miss email'})
else:
current_user.email = request.json['email']
try:
current_user.save()
except Exception:
return jsonify({'err': 'Modify email error.'})
return jsonify({'status': 0, 'msg': 'Email has been modified.', 'user': current_user.to_json()})
@app.route('/user/pwd', methods=['PUT'])
@login_required
def putUserPWD():
if not request.json or not 'current_pwd' in request.json or not 'new_pwd' in request.json:
return jsonify({'err': 'Request not Json or miss current_pwd/new_pwd'})
else:
current_pwd = current_user.pwd
if not request.json['current_pwd'] == current_pwd:
return jsonify({'err': 'current_pwd is not right.'})
else:
current_user.pwd = request.json['new_pwd']
try:
current_user.save()
except Exception:
return jsonify({'err': 'Modify PWD error.'})
return jsonify({'status': 0, 'msg': 'PWD has been modified.', 'user_id': current_user.user_id})
if __name__ == '__main__':
app.run(debug=True)