一、目錄結構
1.1、項目入口文件
如何開始一個項目?
- 創建啓動文件。
- 實例化 flask ,app = Flask(__name__)
- 定義視圖函數,@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 可以遞歸調用。
- 類.__dict__ 只能把類中的實例變量序列化爲字典,類變量不能序列化。
- 這裏通過 dict() 來創建字典。
- 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實現思路?
- v1藍圖是所有視圖函數公用的。
- 視圖函數向Redprint對象註冊。
- Redprint 註冊到 Blueprint v1中。
- Redprint -> register 完成 Redprint 註冊到Blueprint v1中的任務
2.2、爲什麼要自定義Redprint?
- Redprint 功能與Blueprint 一樣。
- Flask 中的 Blueprint (藍圖)最好用來定義模塊級別的視圖請求
- 統一管理URL請求
- 簡化視圖函數傳入的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異常處理,通過繼承的方式,自定義符合前端使用的異常處理。
自定義錯誤處理實現步驟:
- 繼承 HTTPException 錯誤對象,讓其處理自定義錯誤信息;
- 編寫構造函數,傳入自定義類參數;
- 調用父類的構造函數,把 msg 傳入,通過父類的response處理後返回;
- 重寫 get_body、get_headers 讓其返回JSON格式的錯誤信息。
# 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這個基類?
- 增加代碼的複用性,減少代碼量;
- 通過面向對象的繼承特性,適用多種用戶註冊方式,比如email,小程序等等;
- 驗證共有的參數,比如不管通過什麼方式註冊用戶都必須傳入 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的基本規則:
- token必須得有有效期時間;
- token中必須存儲用戶的登錄信息;
- token必須加密;
- 登錄就是獲取token的過程;
- 通過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]
八、訪問權限管理
權限管理對應表寫在什麼地方?
- 寫到數據庫中,mysql,redis等;
- 寫在代碼中,我們這裏寫在代碼中。
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