這是我在做用戶認證開發過程中看到一位大神寫的文章,不過源地址已經失效了,希望有可能未來還能看到傳送門。在此轉載是不忍心這麼好的文章絕版
我在 github
上找到了作者的源碼,有需要的可以去下載https://github.com/yaoyonstudio/flask-pyjwt-auth
在程序開發中,用戶認證授權是一個繞不過的重難點。以前的開發模式下,cookie和session認證是主流,隨着前後端分離的趨勢,基於Token的認證方式成爲主流,而JWT是基於Token認證方式的一種機制,是實現單點登錄認證的一種有效方法。
PyJWT是一個用來編碼和解碼JWT(JSON Web Tokens)的Python庫,也可以用在Flask上。本文就通過一個實例來演示Flask項目整合PyJWT來實現基於Token的用戶認證授權。
一、需求
1、程序將實現一個用戶註冊、登錄和獲取用戶信息的功能
2、用戶註冊時輸入用戶名(username)、郵箱(email)和密碼(password),用戶名和郵箱是唯一的,如果數據庫中已有則會註冊失敗;用戶註冊成功後返回用戶的信息。
3、用戶使用用戶名(username)和密碼(password)登錄,登錄成功時返回token,每次登錄都會更新一次token。
4、用戶要獲取用戶信息,需要在請求Header中傳入驗證參數和token,程序會驗證這個token的有效性並給出響應。
5、程序構建方面,將用戶和認證分列兩個模塊。
二、程序目錄結構
根據示例需求構建程序目錄結構:
三、程序實現
1、程序構建及相關文件
數據遷移配置文件:
flask-pyjwt-auth/db.py
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from run import app
from app import db
app.config.from_object('app.config')
db.init_app(app)
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
運行入口文件:
flask-pyjwt-auth/run.py
from app import create_app
app = create_app('app.config')
if __name__ == '__main__':
app.run(host=app.config['HOST'],
port=app.config['PORT'],
debug=app.config['DEBUG'])
程序初始化文件:
flask-pyjwt-auth/app/__init__.py
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config_filename):
app = Flask(__name__)
app.config.from_object(config_filename)
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
if request.method == 'OPTIONS':
response.headers['Access-Control-Allow-Methods'] = 'DELETE, GET, POST, PUT'
headers = request.headers.get('Access-Control-Request-Headers')
if headers:
response.headers['Access-Control-Allow-Headers'] = headers
return response
from app.users.model import db
db.init_app(app)
from app.users.api import init_api
init_api(app)
return app
上面代碼加入了全局HTTP請求頭配置,設置允許所有跨域請求。
配置文件:
flask-pyjwt-auth/app/config.py
DB_USER = 'root'
DB_PASSWORD = ''
DB_HOST = 'localhost'
DB_DB = 'flask-pyjwt-auth'
DEBUG = True
PORT = 3333
HOST = "192.168.1.141"
SECRET_KEY = "my blog"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = 'mysql://' + DB_USER + ':' + DB_PASSWORD + '@' + DB_HOST + '/' + DB_DB
公共文件:
flask-pyjwt-auth/app/common.py
def trueReturn(data, msg):
return {
"status": True,
"data": data,
"msg": msg
}
def falseReturn(data, msg):
return {
"status": False,
"data": data,
"msg": msg
}
2、用戶模塊
模塊入口(空)
flask-pyjwt-auth/app/users/__init__.py
#
用戶模型:
flask-pyjwt-auth/app/users/model.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import SQLAlchemyError
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(250), unique=True, nullable=False)
username = db.Column(db.String(250), unique=True, nullable=False)
password = db.Column(db.String(250))
login_time = db.Column(db.Integer)
def __init__(self, username, password, email):
self.username = username
self.password = password
self.email = email
def __str__(self):
return "Users(id='%s')" % self.id
def set_password(self, password):
return generate_password_hash(password)
def check_password(self, hash, password):
return check_password_hash(hash, password)
def get(self, id):
return self.query.filter_by(id=id).first()
def add(self, user):
db.session.add(user)
return session_commit()
def update(self):
return session_commit()
def delete(self, id):
self.query.filter_by(id=id).delete()
return session_commit()
def session_commit():
try:
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
reason = str(e)
return reason
在上面用戶模型定義中,定義了set_password和check_password方法,分別用來加密用戶註冊時填寫的密碼(將加密後的密碼寫入數據庫)和在用戶登錄時檢查用戶密碼是否正確。
用戶相關接口實現:
flask-pyjwt-auth/app/users/api.py
from flask import jsonify, request
from app.users.model import Users
from app.auth.auths import Auth
from .. import common
def init_api(app):
@app.route('/register', methods=['POST'])
def register():
"""
用戶註冊
:return: json
"""
email = request.form.get('email')
username = request.form.get('username')
password = request.form.get('password')
user = Users(email=email, username=username, password=Users.set_password(Users, password))
result = Users.add(Users, user)
if user.id:
returnUser = {
'id': user.id,
'username': user.username,
'email': user.email,
'login_time': user.login_time
}
return jsonify(common.trueReturn(returnUser, "用戶註冊成功"))
else:
return jsonify(common.falseReturn('', '用戶註冊失敗'))
@app.route('/login', methods=['POST'])
def login():
"""
用戶登錄
:return: json
"""
username = request.form.get('username')
password = request.form.get('password')
if (not username or not password):
return jsonify(common.falseReturn('', '用戶名和密碼不能爲空'))
else:
return Auth.authenticate(Auth, username, password)
@app.route('/user', methods=['GET'])
def get():
"""
獲取用戶信息
:return: json
"""
result = Auth.identify(Auth, request)
if (result['status'] and result['data']):
user = Users.get(Users, result['data'])
returnUser = {
'id': user.id,
'username': user.username,
'email': user.email,
'login_time': user.login_time
}
result = common.trueReturn(returnUser, "請求成功")
return jsonify(result)
上面用戶模塊的API實現代碼中,先從auth模塊中導入Auth類,在用戶登錄接口中,調用Auth類的authenticate方法來執行用戶認證,認證通過則返回token,認證不通過則返回錯誤信息。在獲取用戶信息的接口,首先要進行“用戶鑑權”,只有擁有權限的用戶纔有權限拿到用戶信息。
3、認證模塊
模塊入口(空)
flask-pyjwt-auth/app/auth/__init__.py
#
授權認證處理:
flask-pyjwt-auth/app/auth/auths.py
import jwt, datetime, time
from flask import jsonify
from app.users.model import Users
from .. import config
from .. import common
class Auth():
@staticmethod
def encode_auth_token(user_id, login_time):
"""
生成認證Token
:param user_id: int
:param login_time: int(timestamp)
:return: string
"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, seconds=10),
'iat': datetime.datetime.utcnow(),
'iss': 'ken',
'data': {
'id': user_id,
'login_time': login_time
}
}
return jwt.encode(
payload,
config.SECRET_KEY,
algorithm='HS256'
)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
驗證Token
:param auth_token:
:return: integer|string
"""
try:
# payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'), leeway=datetime.timedelta(seconds=10))
# 取消過期時間驗證
payload = jwt.decode(auth_token, config.SECRET_KEY, options={'verify_exp': False})
if ('data' in payload and 'id' in payload['data']):
return payload
else:
raise jwt.InvalidTokenError
except jwt.ExpiredSignatureError:
return 'Token過期'
except jwt.InvalidTokenError:
return '無效Token'
def authenticate(self, username, password):
"""
用戶登錄,登錄成功返回token,寫將登錄時間寫入數據庫;登錄失敗返回失敗原因
:param password:
:return: json
"""
userInfo = Users.query.filter_by(username=username).first()
if (userInfo is None):
return jsonify(common.falseReturn('', '找不到用戶'))
else:
if (Users.check_password(Users, userInfo.password, password)):
login_time = int(time.time())
userInfo.login_time = login_time
Users.update(Users)
token = self.encode_auth_token(userInfo.id, login_time)
return jsonify(common.trueReturn(token.decode(), '登錄成功'))
else:
return jsonify(common.falseReturn('', '密碼不正確'))
def identify(self, request):
"""
用戶鑑權
:return: list
"""
auth_header = request.headers.get('Authorization')
if (auth_header):
auth_tokenArr = auth_header.split(" ")
if (not auth_tokenArr or auth_tokenArr[0] != 'JWT' or len(auth_tokenArr) != 2):
result = common.falseReturn('', '請傳遞正確的驗證頭信息')
else:
auth_token = auth_tokenArr[1]
payload = self.decode_auth_token(auth_token)
if not isinstance(payload, str):
user = Users.get(Users, payload['data']['id'])
if (user is None):
result = common.falseReturn('', '找不到該用戶信息')
else:
if (user.login_time == payload['data']['login_time']):
result = common.trueReturn(user.id, '請求成功')
else:
result = common.falseReturn('', 'Token已更改,請重新登錄獲取')
else:
result = common.falseReturn('', payload)
else:
result = common.falseReturn('', '沒有提供認證token')
return result
認證模塊實現token的生成、解析,以及用戶的認證和鑑權。
首先要安裝PyJWT
Pip install pyjwt
認證模塊的實現主要包括下面4個部分(方法):
(1)encode_auth_token方法用來生成認證Token
要生成Token需要用到pyjwt的encode方法,這個方法可以傳入三個參數,如示例:
jwt.encode(payload, config.SECRET_KEY, algorithm=’HS256′)
上面代碼的jwt.encode方法中傳入了三個參數:第一個是payload,這是認證依據的主要信息,第二個是密鑰,這裏是讀取配置文件中的SECRET_KEY配置變量,第三個是生成Token的算法。
這裏稍微講一下payload,這是認證的依據,也是後續解析token後定位用戶的依據,需要包含特定用戶的特定信息,如本例註冊了data聲明,data聲明中包括了用戶ID和用戶登錄時間兩個參數,在“用戶鑑權”方法中,解析token完成後要利用這個用戶ID來查找並返回用戶信息給用戶。這裏的data聲明是我們自己加的,pyjwt內置註冊了以下幾個聲明:
- “exp”: 過期時間
- “nbf”: 表示當前時間在nbf裏的時間之前,則Token不被接受
- “iss”: token簽發者
- “aud”: 接收者
- “iat”: 發行時間
要注意的是”exp”過期時間是按當地時間確定,所以設置時要使用utc時間。
(2)decode_auth_token方法用於Token驗證
這裏的Token驗證主要包括過期時間驗證和聲明驗證。使用pyjwt的decode方法解析Token,得到payload。如:
jwt.decode(auth_token, config.SECRET_KEY, options={‘verify_exp’: False})
上面的options設置不驗證過期時間,如果不設置這個選項,token將在原payload中設置的過期時間後過期。
經過上面解析後,得到的payload可以跟原來生成payload進行比較來驗證token的有效性。
(3)authenticate方法用於用戶登錄驗證
這個方法進行用戶登錄驗證,如果通過驗證,先把登錄時間寫入用戶記錄,再調用上面第一個方法生成token,返回給用戶(用戶登錄成功後,據此token來獲取用戶信息或其他操作)。
(4)identify方法用於用戶鑑權
當用戶有了token後,用戶可以拿token去執行一些需要token才能執行的操作。這個用戶鑑權方法就是進一步檢查用戶的token,如果完全符合條件則返回用戶需要的信息或執行用戶的操作。
用戶鑑權的操作首先判斷一個用戶是否正確傳遞token,這裏使用header的方式來傳遞,並要求header傳值字段名爲“Authorization”,字段值以“JWT”開頭,並與token用“ ”(空格)隔開。
用戶按正確的方式傳遞token後,再調用decode_auth_token方法來解析token,如果解析正確,獲取解析出來的用戶信息(user_id)併到數據庫中查找詳細信息返回給用戶。
四、運行結果
1、註冊成功
2、註冊失敗
3、登錄成功
4、登錄失敗
5、成功獲取用戶信息
6、用戶重新登錄,token變更,原token無法獲取用戶信息
7、不帶token請求,無法獲取用戶信息
PyJWT的使用比較簡單,也比較安全,本文基本涵蓋了Flask和PyJWT的整合和使用過程,希望對大家有用。本文完。