Flask + PyJWT 實現基於Json Web Token的用戶認證授權

這是我在做用戶認證開發過程中看到一位大神寫的文章,不過源地址已經失效了,希望有可能未來還能看到傳送門。在此轉載是不忍心這麼好的文章絕版

我在 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的整合和使用過程,希望對大家有用。本文完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章