Flask 構建自定義RETful API

一、目錄結構

Flask 構建自定義RETful API 目錄結構

1.1、項目入口文件

如何開始一個項目?

  1. 創建啓動文件。
  2. 實例化 flask ,app = Flask(__name__)
  3. 定義視圖函數,@app.route(’’,methods=[‘GET’])
# ginger.py
# _*_ coding:utf-8 _*_
from werkzeug.exceptions import HTTPException
from app import create_app
from app.libs.error import APIException
from app.libs.error_code import ServerError

app = create_app()

"""
	解決未知異常類型的方法:未知異常都是由框架返回的錯誤,是我們意識不到得,所以不確定。
    通過flask 提供的 errorhandler 捕獲所有的異常,必須在入口文件才能捕獲到所有錯誤類型。
    Exception 表示python異常基類
"""

@app.errorhandler(Exception)
def framework_error(e):
    """
        e表示所有異常類型:
            APIException
            HTTPException
            Exception
    """
    # 判斷 e 的類型是不是 APIEception
    if isinstance(e, APIException):
        return e
    if isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return APIException(msg, code, error_code)
    else:
        # 這裏可以記錄集體錯誤信息,並記錄到日誌中
        # 基類異常不需要返回具體的異常,可以通過定義一個統一的異常類型返回就可以了。
        # 判斷 debug 來處理異常返回的格式,如果是調試模式,直接返回錯誤信息,不是調試模式,返回自定義錯誤信息。
        if not app.config['DEBUG']:
            return ServerError()
        else:
            raise e

if __name__ == "__main__":
    app.run()

1.2、創建數據庫表、創建Flask app項目、把藍圖註冊到Flask中

app/__init__.py 文件

# app/__init__.py
# _*_ coding:utf-8 _*_
import pymysql

from .app import Flask

pymysql.install_as_MySQLdb()


# 藍圖管理函數
def create_blueprint(app):
    from .api.v1 import Create_blue_v1
    # 使用url_prefix 必須加上 “/”
    app.register_blueprint(Create_blue_v1(), url_prefix='/v1') # 把視圖模塊掛載到 Flask 核心對象中

# 創建所有數據表
def register_plugin(app):
    from app.models.base import db
    db.init_app(app)  # 註冊插件
    with app.app_context():  # 把 db.create_all() 推入Flask上下文環境中
        db.create_all()  # 必須在 Flask 的上下文環境中才能完成 create_all 操作

# 創建app項目函數
def create_app():
    app = Flask(__name__)
    # 把配置文件加載到 Flask 中
    app.config.from_pyfile('./config/secure.py')
    app.config.from_object("app.config.setting")

    create_blueprint(app)
    register_plugin(app)
    return app

1.3、如何讓jsonify()返回數據模型對象?

重寫 Flask.json 中 JSONEncode 下 default 函數,讓其支持返回數據模型對象。

jsonify 什麼情況下執行 default 函數?理解 jsonify() 序列化時的default函數?
當 flask 知道 jsonify 接收的參數是可被python序列化時,不會執行 default 函數,default 可以遞歸調用。

  1. 類.__dict__ 只能把類中的實例變量序列化爲字典,類變量不能序列化。
  2. 這裏通過 dict() 來創建字典。
  3. dict() 序列化原理:
    在執行dict(o)時,會先執行 Dictlist 類中的 keys 方法,在keys中指定返回的key值,通過keys指定我們可以靈活的返回需要序列化內容。
    默認情況python是不支持 類[‘變量名’] 訪問的,但是在內部添加 __getitem__ 方法後將支持,
    hasattr(對象,屬性) 判斷對象中是否有屬性。
    isinstance(對象,類型) 判斷對象是否爲這個類型。
    Python 面向對象
    Python Class 繼承
class DictList:
    # 類變量
    name = '八月'
    age = 30

    def __init__(self):
        self.sex = '男'  # 實例化變量

    def keys(self):
        return ('name', 'age', 'sex',) # 當返回只有一個元素的元祖時,必須這樣寫('name',)

    def __getitem__(self, item):
        # item 是keys指定返回變量的key,通過getattr()獲取對象相應變量的值
        return getattr(self, item)


o = DictList()
print(dict(o))

# 輸出:{'name': '八月', 'age': 30, 'sex': '男'}

app/app.py 文件

# app/app.py
# _*_ coding:utf-8 _*_
from datetime import date
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder
from app.libs.error_code import ServerError

# 重寫 Flask 中 JSONEncode 下 default 函數
# 只要 jsonify 傳遞的參數,Flask不能被序列化都會調用default
class JSONEncoder(_JSONEncoder):
	# o 就是jsonify()接收的參數
    def default(self, o):
    	# 判斷傳入的對象 o 中是否有 keys 和 __getitem__
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date): # 判斷對象 o 是否是 date 類型
            return o.strftime('%Y-%m-%d')
        raise ServerError()

class Flask(_Flask):
    json_encoder = JSONEncoder

二、自定義 Redprint 對象

2.1、自定義Redprint對象掛載 Flask 架構圖

自定義Redprint對象掛載 Flask 架構圖

Redprint實現思路?

  1. v1藍圖是所有視圖函數公用的。
  2. 視圖函數向Redprint對象註冊。
  3. Redprint 註冊到 Blueprint v1中。
  4. Redprint -> register 完成 Redprint 註冊到Blueprint v1中的任務

2.2、爲什麼要自定義Redprint?

  1. Redprint 功能與Blueprint 一樣。
  2. Flask 中的 Blueprint (藍圖)最好用來定義模塊級別的視圖請求
  3. 統一管理URL請求
  4. 簡化視圖函數傳入的URL

2.2.1、Redprint 代碼實現

定義 Redprint 對象
app/libs/redprint.py 文件功能,重寫 route 裝飾器,構建Redprint對象。

# app/libs/redprint.py 文件
# _*_ coding:utf-8 _*_
"""
 自定義路由管理 Redprint 對象:
    要完成與 flask 中的 Blueprint 相同的功能,
    必須把 Redprint 註冊到Blueprint 中。
"""

class Redprint:
    def __init__(self, name):
        self.name = name
        self.mound = []

    # 構建 route 裝飾器
    def route(self, rule, **options):
        def decorator(f):
            # 將傳入的內容保存到一個列表中,等待向flask的Blueprint裏面註冊
            self.mound.append((f, rule, options))
            return f
        return decorator

    # 登記函數,通過 register 把視圖函數註冊到 Blueprint 中
    # bp 表示 flask 中的 Blueprint
    def register(self, bp, url_prefix=None):
        # 判斷是否傳入 url_prefix ,不傳就等於視圖名稱
        if url_prefix is None:
            url_prefix = '/' + self.name
        for f, rule, options in self.mound:
            """
                options.pop('endpoint',f.__name__) 
                如果路由傳入endpoint參數:
                從 options 字典找到key爲 endpoint 的參數,將其刪除,並返回 endpoint 對應的 value,
                如果沒有 key 爲 endpoint,返回第二參數,也就是裝飾器對應函數的函數名。
                Redprint 中 endpoint 包含 藍圖.模塊+視圖函數 ==> 文件夾.視圖文件+文件中的視圖函數
            """
            endpoint = self.name + '+' + options.pop('endpoint', f.__name__)
            # 通過 Blueprint中add_url_rule方法註冊route接收到的信息。
            bp.add_url_rule(url_prefix + rule, endpoint, f, **options)

2.2.2、.pop() 一些理解

	a = {
		'b': 'jsom',
		# 'f':'age'
		'c': 'name'
	}
	# 如果a字典中有‘b’這個key,返回‘b’這個key的value,如果沒有則返回第二參數‘e’
	d = a.pop('b','e')
	print('d = >', d)
	print('a = >', a)
	輸出:
	d = > jsom
	a = > {'c': 'name'}
	沒有’b‘輸出:
	d = > e
	a = > {'f': 'age', 'c': 'name'}

api/v1/__init__.py 通過 Redprint.register 對象把視圖函數註冊到 Blueprint 對象中

# _*_ coding:utf-8 _*_
from flask import Blueprint
from app.api.v1 import user, book, client, token


# 藍圖與自定義 redprint 的連接函數
def Create_blue_v1():
    # 定義藍圖爲 v1
    blue = Blueprint('v1', __name__)
    # 通過 redprint.register把Redprint 對象註冊到 Blueprint v1 中
    user.api.register(blue)
    book.api.register(blue)
    client.api.register(blue)
    token.api.register(blue)
    return blue

2.2.3、Redprint 使用

api/v1/user.py 視圖文件
我們需要把從數據模型中獲取到的數據序列化爲字典,但是jsonify() 不支持數據模型的序列化。在app/app.py重寫JSONEncode 中的default屬性來實現對數據模型序列化。

# _*_ coding:utf-8 _*_
# from flask import Blueprint
# user = Blueprint('User',__name__)
from flask import jsonify, g

from app.libs.error_code import DeleteSuccess, AuthFailed
from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.base import db
from app.models.user import User

api = Redprint('user')

"""
    view_model 視圖層 返回對前端友好的個性化視圖模型
    比如 User模型是數據庫存儲的原始模型,對前端不友好
"""


# 管理員可以獲取所有用戶信息
@api.route('', methods=['GET'])
@auth.login_required
def super_get_user():
    # 通過uid 獲取用戶
    user = User.query.filter_by(status=1).all()
    # 如何返回數據模型對象?
    # jsonify() 方法重寫 default 函數,讓其支持返回數據模型對象
    # jsonify 什麼情況下執行 default 函數?
    # 當 flask 知道 jsonify 接收的參數是可被序列化時,不會執行 default 函數
    return jsonify(user)


# 普通用戶、管理員都能使用id獲取信息
@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.filter_by(id=uid).first_or_404()
    return jsonify(user)


# 管理員權限刪除可以刪除所有用戶
@api.route('/<int:uid>', methods=['DELETE'])
@auth.login_required
def super_delete_user(uid):
    with db.auto_commit():
        if uid is g.user.uid:
            return NotFound('非法請求!')
        user = User.query.filter_by(id=uid).first_or_404()
        user.delete()
    return DeleteSuccess()


# 普通用戶只能刪除自己,現實應用是不允許刪除自己的。
@api.route('', methods=['DELETE'])
@auth.login_required
def delete_user():
    uid = g.user.uid
    # g 變量被線程隔離,多個用戶請求時,不會衝突
    with db.auto_commit():
        user = User.query.filter_by(id=uid).first_or_404()
        user.delete()
    return DeleteSuccess()

api/v1/user.py 視圖文件

# _*_ coding:utf-8 _*_
# from flask import Blueprint
# book = Blueprint('Book',__name__)

# 自定義URL管理類
from flask import jsonify
from sqlalchemy import or_
from app.libs.redprint import Redprint
from app.models.book import Book
from app.validators.forms import BookSearchForm

api = Redprint('book')


@api.route('/search')
def search():
    form = BookSearchForm().validate_for_api()
    q = '%' + form.q.data + '%'
    books = Book.query.filter(
                or_(Book.title.like(q), Book.publisher.like(q))
            ).all()
    books = [book.hide('summary').append('pages') for book in books]
    return jsonify(books)


@api.route('/<isbn>/detail')
def detail(isbn):
    book = Book.query.filter_by(isbn=isbn).first_or_404()
    return jsonify(book)

三、數據返回處理

數據返回處理

四、重寫 werkzeug.exceptions --> HTTPException 異常處理

4.1、爲什麼要自定義異常處理?

爲什麼要自定義異常處理?
因爲原有的異常處理返回的是HTML格式的錯誤信息,前端獲取到這樣的錯誤信息不好處理,對於API開發來說,需要返回統一JSON格式錯誤信息。
利用werkzeug.exceptions中HTTPException異常處理,通過繼承的方式,自定義符合前端使用的異常處理。

自定義錯誤處理實現步驟:

  1. 繼承 HTTPException 錯誤對象,讓其處理自定義錯誤信息;
  2. 編寫構造函數,傳入自定義類參數;
  3. 調用父類的構造函數,把 msg 傳入,通過父類的response處理後返回;
  4. 重寫 get_body、get_headers 讓其返回JSON格式的錯誤信息。

Python Web 框架工具包 werkzeug

# app/libs/error.py 文件
# werkzeug.exceptions 異常處理
from flask import request, json
from werkzeug.exceptions import HTTPException


class APIException(HTTPException):
    code = 500
    msg = 'sorry,we make a mistake (* _^_ )!'
    error_code = 999

    # 自定義異常處理類的構造函數
    def __init__(self, msg=None, code=None, error_code=None, headers=None):
        if code:
            self.code = code
        if error_code:
            self.error_code = error_code
        if msg:
            self.msg = msg
        # 調用 HTTPException 中的構造函數,第一個參數爲錯誤提示信息,必須傳;
        # 第二個參數爲 response,不傳會根據 HTTPException 接收到的 code、description 自動生成 response。
        super(APIException, self).__init__(msg, None)

    # 重寫HTTPException 中個get_body,讓其返回json格式錯誤信息
    def get_body(self, environ=None):
        body = dict(
            msg=self.msg,
            error_cod=self.error_code,
            # request 返回請求信息,請求方法,發生錯誤的URL
            request=request.method + ' ' + self.get_url_no_param()
        )
        return json.dumps(body)

    # 重寫HTTPException 中個get_headers,改變返回請求頭的返回信息。
    def get_headers(self, environ=None):
        return [("Content-Type", "application/json")]

    # 處理返回 get_body 中 request url
    @staticmethod
    def get_url_no_param():
        full_path = str(request.full_path)
        main_path = full_path.split('?')
        return main_path[0]

4.2、自定返回異常

# app/libs/error_code.py 文件
# _*_ coding:utf-8 _*_
# 自定義異常提示對象
# 導入自定義異常返回處理對象
"""
    異常的類型:
        已知錯誤類型:提前知道的類型
        未知錯誤類型:錯誤不固定,隨時可能會報錯
    解決未知異常的方法:
        不管未知異常在什麼地方出現,在項目啓動文件捕獲所有的異常,
        捕獲到異常後再進行格式化處理後統一拋出。
"""
from app.libs.error import APIException


class Success(APIException):
    code = 201
    msg = 'OK'
    error_code = 0


# 刪除成功
class DeleteSuccess(Success):
    code = 202
    error_code = -1


# 未知錯誤
class ServerError(APIException):
    code = 500
    msg = 'sorry,we make a mistake (* _^_ )!'
    error_code = 999


class ClientTypeError(APIException):
    code = 400
    msg = 'Client Type Error'
    error_code = 1006


class ParameterException(APIException):
    code = 400
    msg = 'Invalid paramter'
    error_code = 1000


class NotFound(APIException):
    code = 404
    msg = 'the resource are not_found O_O'
    error_code = 1001


class AuthFailed(APIException):
    code = 401
    error_code = 1005
    msg = 'authorization failed'


class Forbidden(APIException):
    code = 403
    error_code = 1004
    msg = 'forbidden, not in scope'

五、枚舉模塊

app/libs/enums.py 文件定義用戶通過什麼平臺登錄。

# app/libs/enums.py 文件
# 枚舉模塊
from enum import Enum  # 引入python枚舉模塊

class ClientTypeEnum(Enum):
    USER_EMAIL = 100  # email 登錄
    USER_MOBILE = 101  # 手機登錄
    USER_MINA = 200  # 微信小程序登錄
    USER_WX = 201  # 微信公衆號登錄

六、數據模型基類

爲什麼需要基類模型?
基類模型定義每個模型中相同的公共數據模型字段,也就是每個數據模型中都會出現的數據表字段。

數據模型基類 app/models/base.py

# _*_ coding:utf-8 _*_
# 重寫 SQLAlchemy 中的方法,定義數據模型基類
from contextlib import contextmanager # 導入 Flask 的上下文管理器
from datetime import datetime
from flask_sqlalchemy import BaseQuery, SQLAlchemy as _SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger, orm, inspect
from app.libs.error_code import NotFound


class SQLAlchemy(_SQLAlchemy):
    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            db.session.rollback()
            raise e


# 重寫 Query 中的一些方法
class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)

    # 重寫 get_or_404 first_or_404 讓其返回json格式的錯誤代碼
    def get_or_404(self, ident):
        rv = self.get(ident)
        if rv is None:
            raise NotFound()
        return rv

    def first_or_404(self):
        rv = self.first()
        if rv is None:
            raise NotFound(msg='user not found')
        return rv


db = SQLAlchemy(query_class=Query)

# 定義基類數據模型
class Base(db.Model):
    __abstract__ = True
    create_time = Column(Integer)
    status = Column(SmallInteger, default=1)

    def __init__(self):
        self.create_time = int(datetime.now().timestamp())

    def __getitem__(self, item):
        return getattr(self, item)

    @property
    def create_datetime(self):
        if self.create_time:
            return datetime.fromtimestamp(self.create_time)
        else:
            return None

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
        	# hasattr() 判斷 self 是否包含 key 的屬性
        	# setattr() 對 self 中的 key 屬相賦值 value
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

    def delete(self):
        self.status = 0

    # 序列化模型
    def keys(self):
        return self.fields

	# 隱藏數據模型字段
    def hide(self, *keys):
        for key in keys:
            self.fields.remove(key)
        return self

	# 添加數據模型字段
    def append(self, *keys):
        for key in keys:
            self.fields.append(key)
        return self


# 第二種序列化模型類方法
class MixinJSONSerializer:
    @orm.reconstructor
    def init_on_load(self):
        self._fields = []
        # self._include = []
        self._excludes = []

        self._set_fields()
        self.__prune_fields()

    def _set_fields(self):
        pass

    def __prune_fields(self):
        columns = inspect(self.__class__).columns
        if not self._fields:
            all_columns = set(columns.keys())
            self._fields = list(all_columns) - set(self._excludes)
	
    def hide(self, *args):
        for key in args:
            self._fields.remove(key)
        return self

    def keys(self):
        return self._fields

    def __getitem__(self, key):
        return getattr(self, key)

user.py用戶數據模型。

# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, SmallInteger
from werkzeug.security import generate_password_hash, check_password_hash
from app.libs.error_code import NotFound, AuthFailed
from app.models.base import Base, db


# User 表
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)  # 用戶暱稱
    auth = Column(SmallInteger, default=1)  # 權限的標識
    _password = Column('password', String(100))

    # 第一種通過 keys和 base.py中的__getitem__ 聯合實現數據模型的序列化
    def keys(self):
        return ('id', 'email', 'nickname', 'auth',)

    # 通過@property 預處理 password
    # 獲取 數據模型中 _password 返回
    @property
    def password(self):
        return self._password

    # 獲取 form 表單提交過來的 password,加密後再設置模型中的_password
    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)  # 給密碼加密

    # 在對象下創建對象本身是不合理的,通過靜態方法或者類方法創建。
    @staticmethod
    def register_by_email(nickname, account, secret):
        with db.auto_commit():
            user = User()
            user.nickname = nickname
            user.email = account
            user.password = secret
            db.session.add(user)
            db.session.commit()

    @staticmethod
    def verify(email, password):
        user = User.query.filter_by(email=email).first_or_404()
        if not user.check_password(password):
            raise AuthFailed()
        scope = 'AdminScope' if user.auth == 2 else 'UserScope'
        return {'uid': user.id, 'scope': scope}

    # 比較密碼是否一致
    def check_password(self, raw):
        if not self._password:
            return False
        return check_password_hash(self._password, raw) # 驗證密碼是否一致

book.py書籍數據模型。

# _*_ coding:utf-8 _*_
from sqlalchemy import Column, Integer, String, orm
from app.models.base import Base


class Book(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))

    @orm.reconstructor
    def __init__(self):
        self.fields = ['id', 'title', 'author', 'binding',
                       'publisher', 'price',  'pubdate',
                       'isbn', 'summary', 'image'
                       ]

@orm.reconstructor
sqlalchemy 在通過元類實例化模型對象時,不會執行__init__
可以通過 orm.reconstructor 裝飾器來執行__init__

七、用戶知識

7.1、重新Form中的一些方法

form.validate() 方法,遇到錯誤是不會拋出錯誤信息的,只會把錯誤信息保存在form.erroes 屬性中,既然這樣我們只能手動讓它拋出錯誤異常信息。

# _*_ coding:utf-8 _*_
from flask import request
from wtforms import Form
from app.libs.error_code import ParameterException

class BaseForm(Form):
    def __init__(self):
       '''
        request.json 把表單的提交數據轉換爲json
        request.args.to_dict()
        data = request.json
        request.json 調用的是 request.get_json(self, force=False, silent=False, cache=True) 這個函數
       '''
        data = request.get_json(silent=True)  # silent 保持靜默,不會報錯
        args = request.args.to_dict()
        super(BaseForm, self).__init__(data=data, **args) # 參數爲json的時必須爲data=json值,要不然不能解析

    # 重寫 validate 方法,把不拋出異常修改爲拋出異常
    def validate_for_api(self):
        # 調用父類的validate方法並返回出現的異常
        valid = super(BaseForm, self).validate()
        if not valid:
            raise ParameterException(msg=self.errors)
        return self

7.1、用戶註冊

登錄方式
用戶註冊的方式,通過枚舉的方式定義。
client.py用戶註冊

# app/api/v1/client.py
# _*_ coding:utf-8 _*_
# flask 中所有用戶提交的數據都在request中

from app.libs.enums import ClientTypeEnum
from app.libs.error_code import Success
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, UserEmailForm

api = Redprint('client')


# 註冊登錄
@api.route('/register', methods=['GET', 'POST'])
def create_client():
    # 考慮的問題:參數 校驗 接收參數
    # 調用重寫後驗證器,對提交的數據進行驗證並拋出異常
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: __register_user_by_email
    }
    promise[form.type.data]()
    return Success()


# email 登錄處理函數
def __register_user_by_email():
    form = UserEmailForm().validate_for_api()
    # 表單對象 ClientForm 無法獲取 nickname ?
    # 1、request.json['nickname'] 中包含了這個參數,但是 request 是沒有通過驗證的
    #    所以不建議使用這種方法獲取。
    # 2、通過 form 獲取,
    #    利用python的繼承特性,編寫一個特殊的 Form 對象 UserEmailForm ,讓其去繼承
    #    ClientForm 這個基類,在 UserEmailForm 編寫個性的 Form 屬性。
    User.register_by_email(form.nickname.data, form.account.data, form.secret.data)

7.2、用戶註冊校驗 WTForms

forms.py對form表單提交過來的數據驗證。

爲什麼設置ClientForm這個基類?

  1. 增加代碼的複用性,減少代碼量;
  2. 通過面向對象的繼承特性,適用多種用戶註冊方式,比如email,小程序等等;
  3. 驗證共有的參數,比如不管通過什麼方式註冊用戶都必須傳入 type 這個參數,所以就可以在基類中進行統一的驗證。
# _*_ coding:utf-8 _*_
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, length, Email, Regexp, ValidationError
from app.libs.enums import ClientTypeEnum
from app.models.user import User
from app.validators.base_form import BaseForm as Form


# 註冊基類,
class ClientForm(Form):
    account = StringField(validators=[DataRequired(message='用戶名不能爲空'), length(min=5, max=32)])
    secret = StringField()  # 密碼
    type = IntegerField(validators=[DataRequired()])

    # 自定義驗證器,驗證type
    def validate_type(self, value):
        try:
            # 把用戶輸入的數字轉換爲枚舉類型
            client = ClientTypeEnum(value.data)
        except ValueError as e:
            raise e
        self.type.data = client


# 繼承基類,並編寫獨立的email註冊類
class UserEmailForm(ClientForm):
    account = StringField(validators=[Email(message='invalidate email')])
    secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
    nickname = StringField(validators=[DataRequired(), length(min=2, max=22)])

    # 驗證用戶是否註冊,必須以 validate_ 開頭
    def validate_account(self, value):
        if User.query.filter_by(email=value.data).first():
            raise ValidationError()


class BookSearchForm(Form):
    q = StringField(validators=[DataRequired()])


class TokenForm(Form):
    token = StringField(validators=[DataRequired()])

7.3、通過 Token 登錄API接口

Token的基本規則:

  1. token必須得有有效期時間;
  2. token中必須存儲用戶的登錄信息;
  3. token必須加密;
  4. 登錄就是獲取token的過程;
  5. 通過POST傳入Token信息。

token.py 生成token與登錄驗證token。

# _*_ coding:utf-8 _*_
from flask import current_app, jsonify
from app.libs.enums import ClientTypeEnum
from app.libs.error_code import AuthFailed
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm, TokenForm
# token令牌生成加密模塊
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature

api = Redprint('token')


# 生成 Token 令牌
@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify # 調用User.verify獲取到用戶信息
    }
    identity = promise[ClientTypeEnum(form.type.data)](
        form.account.data,
        form.secret.data
    )
    # 生成令牌
    expiration = current_app.config['TOKEN_EXPIRATION']
    token = generate_auth_token(identity['uid'],
                                form.type.data,
                                identity['scope'],
                                expiration
                                )
    t = {
        'token': token.decode('ascii')
    }
    return jsonify(t), 201


# 獲取 Token 令牌
@api.route('/secret', methods=['POST'])
def get_token_info():
    form = TokenForm().validate_for_api()
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(form.token.data, return_header=True)
    except SignatureExpired:
        raise AuthFailed(msg='token is expired', error_code=1002)
    except BadSignature:
        raise AuthFailed(msg='token is invalid', error_code=1002)
        
	# token中包含的信息
    r = {
        'scope': data[0]['scope'],
        'create_at': data[1]['iat'],  # 令牌創建時間
        'expire_in': data[1]['exp'],  # 令牌過期時間
        'uid': data[0]['uid']
    }
    return jsonify(r)


# 生成令牌函數
def generate_auth_token(uid, ac_type, scope=None, expiration=7200):
    s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    return s.dumps({
        'uid': uid,
        'type': ac_type.value,
        'scope': scope
    })

token_auth.py 驗證token是否合法,提供視圖函數訪問保護。
通過flask_httpauth中的 HTTPBasicAuth 來爲視圖函數訪問提供保護,,驗證Token的合法性與有效時間。

# _*_ coding:utf-8 _*_
"""
HTTPBasicAuth 發送賬號和密碼的方式必須在HTTP請求頭中發送
格式:
    固定寫法 --> Authorization : basic bse64(賬號:密碼)
"""
from collections import namedtuple
from flask import current_app, g, request
from flask_httpauth import HTTPBasicAuth # 提供視圖函數訪問保護。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, \
    BadSignature, SignatureExpired

from app.libs.error_code import AuthFailed, Forbidden
from app.libs.scope import is_in_scope

auth = HTTPBasicAuth()
# 構建一個對象式結構
User = namedtuple('User', ['uid', 'ac_type', 'scope'])

"""
    在這裏獲取token的時候,只要把token通過賬號傳遞進來即可,密碼不用傳值
    因爲token中已經包含了賬號密碼信息。
"""

# user 登錄,通過 HTTPBasicAuth 驗證 token 合法函數
@auth.verify_password
def verify_password(account, password):
    # account == token
    # 驗證token是否合法
    user_info = verify_auth_token(account)
    if not user_info:
        return False
    else:
        # 將 user_info 保存到全局的 g.user 中
        g.user = user_info
    return True

# 驗證token是否合法、有效時間是否過期
# 1.驗證令牌是否合法
# 2.驗證令牌時間是否過期
def verify_auth_token(token):
    s = Serializer(current_app.config['SECRET_KEY']) # SECRET_KEY 在配置文件中必須設置
    try:
        data = s.loads(token)
    except BadSignature:  # 捕獲BadSignature,驗證 Token 是否合法
        raise AuthFailed(msg='token is invalid', error_code=1002)
    except SignatureExpired:  # 捕獲SignatureExpired,驗證 Token 是否過期
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['type']
    scope = data['scope']
    allow = is_in_scope(scope, request.endpoint) # 判斷視圖函數是否在權限中
    if not allow:
        raise Forbidden()
    # 最佳返回結果爲對象式結構
    return User(uid, ac_type, scope)
7.3.1、python中namedtuple介紹

namedtuple類位於collections模塊,namedtuple能夠用來創建類似於元祖的數據類型,除了能夠用索引來訪問數據,能夠迭代,還能夠方便的通過屬性名來訪問數據。
在python中,傳統的tuple類似於數組,只能通過下標來訪問各個元素,我們還需要註釋每個下表代表什麼數據。通過使用namedtuple,每個元素有了自己的名字。聲明namedtuple是非常簡單方便的。

from collections import namedtuple

Friend = namedtuple("Friend", ['name', 'age', 'email'])

f1 = Friend('xiaowang', 33, '[email protected]')
print('f1=>', f1)
print('f1.age=>', f1.age)
print('f1.email=>', f1.email)
f2 = Friend(name='xiaozhang', email='[email protected]', age=30)
print('f2=>', f2)

name, age, email = f2
print('name, age, email=>', name, age, email)

輸出:

f1=> Friend(name='xiaowang', age=33, email='[email protected]')
f1.age=> 33
f1.email=> [email protected]
f2=> Friend(name='xiaozhang', age=30, email='[email protected]')
name, age, email=> xiaozhang 30 [email protected]

八、訪問權限管理

權限管理設計實例方案

權限管理對應表寫在什麼地方?

  1. 寫到數據庫中,mysql,redis等;
  2. 寫在代碼中,我們這裏寫在代碼中。

scope.py權限管理文件

# _*_ coding:utf-8 _*_
# 權限管理


class Scope:
    allow_api = []  # 設置視圖函數的訪問權限
    allow_module = []  # 設置模塊級別的訪問權限
    forbidden = []  # 設置需要排除的視圖函數
	
	# 通過__add__ 完成 “+” 操作,利用類的繼承性,讓每個繼承了Scope 都可以使用 “+” 操作
    def __add__(self, other):
        # 處理視圖函數訪問權限
        self.allow_api = self.allow_api + other.allow_api
        # 利用 set 集合類型不重複特性,去除重複
        self.allow_api = list(set(self.allow_api))

        # 處理模塊級別
        self.allow_module = self.allow_module + other.allow_module
        self.allow_module = list(set(self.allow_module))

        # 處理排除
        self.forbidden = self.forbidden + other.forbidden
        self.forbidden = list(set(self.forbidden))
        return self


class AdminScope(Scope):
    # allow_api = ['v1.user+get_user', 'v1.user+super_get_user', 'v1.user+delete_user']

    allow_module = ['v1.user']

    def __init__(self):
        self + UserScope()


class UserScope(Scope):
    forbidden = ['v1.user+delete_user', 'v1.user+super_get_user']
    # allow_api = ['v1.user+get_user', 'v1.user+delete_user']


# class SuperScope(Scope):
#     allow_api = ['v1.C', 'v1.D']
#     allow_module = ['v1.user']
#
#     def __init__(self):
#         self + UserScope() + AdminScope()

# 判斷視圖函數是否在權限中
# endpoint 接收的是 Redprint 對象中 endpoint ,我們將其改寫爲了包含 藍圖.模塊+視圖函數 ==> 文件夾.視圖文件+文件中的視圖函數 這樣的形式,方便處理。
def is_in_scope(scope, endpoint):
    # 利用 globals() 實現反射
    scope = globals()[scope]()
    splits = endpoint.split('+')
    red_name = splits[0]
    if endpoint in scope.forbidden:
        return False
    if endpoint in scope.allow_api:
        return True
    if red_name in scope.allow_module:
        return True
    else:
        return False

8.1、globals() 實現反射

globals() 的定義:返回一個字典, 表示當前位置的所有全局符號表。 這個符號表始終針對當前模塊(對函數或方法來說, 是指定義它們的模塊, 而不是調用它們的模塊)。
現在有個問題,如果只想調用函數名以 _super 結尾的函數,但是又不確定具體方法名和有幾個函數,該怎麼辦呢?這時候我們可以用 globals() 函數來實現,注意示例中 main 函數中的代碼部分

def zero_super():
    return 0

def one_super():
    return 1

def two_super():
    return 2

def hello():
    print("Hello")

if __name__ == '__main__':
    promos = [name for name in globals()]
    print(promos)

輸出:

['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__','zero _super', 'one _super', 'two _super','hello'] 

前面打印的都是一些內置函數,我們不關注,我們只關注自己定義的函數,那麼只想調用某一類型的函數,比如後綴是 _super 結尾的怎麼辦呢?加上一個個過濾條件:

if __name__ == '__main__':
    promos = [name for name in globals() if name.endswith(" _super")]
    print(promos)

輸出:

['zero_promo', 'one_promo', 'two_promo']

現在得到的是一個字符串,肯定是不能執行的,那該如何是好呢?這裏就可以利用 globals() [name] 來獲取函數體。

if __name__ == '__main__':
    promos = [globals()[name] for name in globals() if name.endswith(" _super")]
    print(promos)
	print(promos[0]())

輸出:

[<function zero_promo at 0x0000013872FE2E18>, <function one_promo at 0x00000138735D5730>, <function two_promo at 0x00000138735D57B8>]

獲得函數體後就可以加‘()’調用函數。

	promos[0]()

輸出:

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