Python-FastAPI 異步框架開發博客系統(二)通訊邏輯篇


title: [Frodo-通訊篇] 模板、增刪改查與認證 layout: post date: 2020-06-09 tag: note author: BY Zhi-kai Yang

Frodo的第一個版本已經實現了,在下一個版本前,我將目前的開發思路整理成三篇文章,分別是數據篇、通訊篇、異步篇。html

項目地址前端

博客地址java

本篇就來到實現具體功能的邏輯流程了,在Web應用匯總,我我的更傾向於將業務流程成爲「通訊」。由於是整個流程就是後臺將數據組織加工發往前端,這個過程協議能夠不一樣(http(s), websocket), 方法可能不一樣(rcp, ajax, mq), 返回的內容格式不一樣(json, xml, html(templates), 早年的Flash等); 剛纔講的是先後臺通訊,其實邏輯模塊間、進程間甚至是後續的容器間都涉及到通訊的問題。本篇先介紹Web通訊的核心,先後臺通訊。python

模板技術與先後端分離

  • 模板技術: 本世紀頭十年普遍採用的Web技術,他有更有名的稱呼MVC模式。核心思想是在html模板中使用後端代碼寫入數據,模板引擎會將渲染後html返回。Django內嵌了這種技術,python其餘框架須要依賴jinjia,Mako等單獨模板。其餘語言如java的JSP也是採用此模式。他的特色是操做直接,直接在須要的地方寫對應的數據。還能夠直接使用後端語言在頁面寫邏輯,開發速度快。但缺點也很明顯,先後端嚴重耦合,維護困難,不適合大型項目。webpack

    • 協議: http
    • 方法: 都可
    • 內容: html(templates)
  • 先後端分離: 當下主流模式,當項目愈來愈大,前端工程化的需求催生了webpack工具。隨後Vue,React,Angular框架專一MVVC模式,也就是隻向後端拿數據,渲染和業務邏輯放進前端框架中。這樣先後端開發人員能夠最大程度的分離。git

    • 協議:都可
    • 方法: 都可
    • 內容: json/xml

Mako模板和他的朋友FastAPI-Mako

Frodo使用模板來作博客的展現前臺,考慮的是這部分頁面少、邏輯簡單、後端人員方便維護,模板徹底夠用。沒有過期的技術,只有合不合適的技術github

Mako是python主流模板之一,他的原生接口其實能夠直接使用,但有些重複的邏輯須要咱們包裝一下:web

  • 模板中固定須要的幾個上下文變量
    • 請求對象(後端框架使用的request對象,在Flask,Django,fastapi中都存在),模板須要用到他的一些方法和屬性,如反向尋址request.url_for(), request.host,甚至是request.Session中的內容
    • 請求上下文 context (主要指body,接觸過Web開發的朋友都能列舉出主要的請求體:Formdata, QueryParam, PathParam, 這些在模板中可能會用到)
    • 返回上下文 (不用封裝葉濤提供)
  • 模板文件自動尋址
  • 靜態文件尋址
  • 模板異常處理

Flask同樣,fastapi的路由也是函數式的,爲了將上述模板功能封裝進路由函數,直接的作法是藉助python的裝飾器。最終高達到下述的效果:ajax

from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import HTMLResponse
from models import cache, Post
from ext import mako

@router.get('/archives', name='archives', response_class=HTMLResponse)
@mako.template('archives.html') # 指定模板文件名稱
@cache(MC_KEY_ARCHIVES)
async def archives(request: Request): # 須要顯示地傳遞 Request
    post_data = await Post.async_filter(status=Post.STATUS_ONLINE)
    post_obj = [Post(**p) for p in post_data]
    post_obj = sorted(post_obj, key=lambda p: p.created_at, reverse=True)
    rv = dict()
    for year, items in groupby(post_obj, lambda x: x.created_at.year):
        if year in rv: rv[year].extend(list(items))
        else: rv[year] = list(items)
    archives = sorted(rv.items(), key=lambda x: x[0], reverse=True)
    # 只返回上下文
    return {'archives': archives}
複製代碼

其實很好理解,惟一要說明的是爲何要顯示地傳遞request, fastapi最大程度上避免傳遞request, 這一點和Flask的想法是相同的,利用Local線程棧徹底能夠作到區別不一樣請求的上下文。但模板中須要常常反向尋址,相似於:redis

% for year, posts in archives:
  <h3 class="archive-year-wrap">
      <a href="${ request.url_for('archives', year=year) }" class="archive-year">
      ${ year }
    </a>
  </h3>
% endfor
複製代碼

@mako簡單裝飾器完整在LouisYZK/FastAPI-Mako, 感興趣的朋友能夠看看。

此外還有@cached裝飾器,他是將函數的返回結果模板緩存,若是當前頁面的數據不發生變化的話下次訪問將直接從redis拿數據,詳細的邏輯將在下面CRUD邏輯中介紹。

CRUD的通訊邏輯

本節是針對全部的數據模型講述的,個別的如Posts,Activity的數據存儲方式有多重,他們須要的trick多一些。而全部數據的操做都遵循下列流程:

控制用例中的DataModel 就是在數據篇中設計的數據類,他們有若干方法處理CRUD的需求,其中最最重要的兩點:

  • 生成操做的SQL
  • 生成KV數據庫緩存使用的 key

上述兩點均使用了一些sqlalchemy和python裝飾器的一點trick。你們能夠重點參考源碼models/base.pymodels/mc.py.

很是值得一提的是更新刪除緩存的實現:

## mc.py中的clear_mc方法
async def clear_mc(*keys):
    redis = await get_redis()
    print(f'Clear cached: {keys}')
    assert redis is not None
    await asyncio.gather(*[redis.delete(k) for k in keys],
                        return_exceptions=True)

## base類中的__flush__方法
from models.mc import clear_mc
@classmethod
async def __flush__(cls, target):
    await asyncio.gather(
        clear_mc(MC_KEY_ITEM_BY_ID % (target.__class__.__name__, target.id)),
        target.clear_mc(), return_exceptions=True
    )

## target是具體數據實例,他們重寫clear_mc()方法,用於刪除指定不一樣的key, 例以下面的Post類的重寫:
async def clear_mc(self):
    keys = [
        MC_KEY_FEED, MC_KEY_SITEMAP, MC_KEY_SEARCH, MC_KEY_ARCHIVES,
        MC_KEY_TAGS, MC_KEY_RELATED % (self.id, 4),
        MC_KEY_POST_BY_SLUG % self.slug,
        MC_KEY_ARCHIVE % self.created_at.year
    ]
    for i in [True, False]:
        keys.append(MC_KEY_ALL_POSTS % i)
    for tag in await self.tags:
        keys.append(MC_KEY_TAG % tag.id)
    await clear_mc(*keys)
複製代碼

這樣就確保每次的建立、更新、刪除數據能把相關的緩存都刪除,保持數據的一致性。你可能注意到了,刪除緩存的操做是可等待的(awaitable),這意味着異步能夠在這裏發揮優點實現併發。所以咱們看到了asyncio.gather(*coros)的使用,他能夠併發地刪除多個key,由於redis建立了鏈接池,這樣不使用多線程,asyncio就是這樣實現io併發的。(其實這點應該在異步 篇介紹的,不過這點很重要)。

認證

認證的需求來自兩個:

  • 內容管理系統後臺只能由博客擁有者進行數據操做,如博客的發佈、密碼的修改等。

  • 訪問者評論須要驗證身份。

管理員的認證--使用JWT

JWT是目前普遍使用的驗證方式之一,他比cookie的優點能夠參考相關文章。而fastapi已經內嵌了對於JWT的支持,咱們使用他來驗證很是方便。

在講具體實現前,仍是得先想明白他的通訊邏輯:

上述流程表示login的邏輯以及訪問須要驗證API的通常邏輯。你們發現問題了嘛?Token在哪裏存儲呢?

Token 存在哪裏呢? 服務器生成Token客戶端接收,下次請求要帶上他。這種常用且小體積的數據直接存儲在內存最合適。放在程序語言中少不了要共享全局變量,例如multiprocess.Value就是解決此問題的。但異步是針對事件循環來研究的,沒有線程進程的概念,此時contextvar是專門解決異步的變量共享問題的,須要python大於3.7

fastapi 幫咱們維護此Token,只須要簡單的定義以下:

from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth')
複製代碼

意思是Token的生成路徑爲/auth,同時oauth2_scheme形就做爲全局依賴的token來源, 每當接口須要使用Token時僅需:

@router.get('/users')
async def list_users(token: str = Depends(oauth2_scheme)) -> schemas.CommonResponse:
    users: list = await User.async_all()
    users = [schemas.User(**u) for u in users]
    return {'items': users, 'total': len(users)}
複製代碼

Depends 是fastapi的特點,直接寫在接口函數的參數裏,能夠在請求前執行一些邏輯,相似中間件。這裏的邏輯就是檢查請求頭是否帶有Auth: Bear+Token, 如沒有就不能發出此請求。

Token的生成邏輯在login接口完成,這差很少是Frodo最複雜的邏輯了:

@app.post('/auth')
async def login(req: Request, username: str=Form(...), password: str=Form(...)):
    user_auth: schemas.User = \
            await user.authenticate_user(username, password)
    if not user_auth:
        raise HTTPException(status_code=400, 
                            detail='Incorrect User Auth.')
    access_token_expires = timedelta(
        minutes=int(config.ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    access_token = await user.create_access_token(
                        data={'sub': user_auth.name},
                        expires_delta=access_token_expires)
    return {
        'access_token': access_token,
        'refresh_token': access_token,
        'token_type': 'bearer'
    }
複製代碼

基本按照本節時序圖執行。

訪問認證--使用Session

訪問認證有不少,受博客內容所限,訪問Frodo的應該都有Github, 所以採用他認證,邏輯以下:

整個邏輯很簡單,遵循Github的認證邏輯,換種方式好比微信掃碼就要換一套。注意一下跳轉的url便可。同時存儲訪客的信息就不使用JWT了,由於不限制過期等,session的cookie最直接。

@router.get('/oauth')
async def oauth(request: Request):
    if 'error' in str(request.url):
        raise HTTPException(status_code=400)
    client = GithubClient()
    rv = await client.get_access_token(code=request.query_params.get('code'))
    token = rv.get('access_token', '')
    try:
        user_info = await client.user_info(token)
    except:
        return RedirectResponse(config.OAUTH_REDIRECT_PATH)
    rv = await create_github_user(user_info)
    ## 使用session存儲
    request.session['github_user'] = rv
    return RedirectResponse(request.session.get('post_url'))
複製代碼

注意fastapi 開啓session須要加入中間件

from starlette.middleware.sessions import SessionMiddleware

app.add_middleware(SessionMiddleware, secret_key='YOUR KEY')
複製代碼

starlette是什麼東西? 不是fastapi嗎? 簡單講starlettefastapi的關係就跟werkzurgFlask的關係同樣,WSGI 和 ASGI的區別,如今ASGI的思路就是超越WSGI,固然本身也要搞一套基本標準和工具庫。

Fine, 通訊邏輯基本上就是這些,Frodo使用到的通訊模型仍是不多的。下一篇「異步篇」跟通訊和數據都有關係,異步博客與通常python實現的博客就區別在這裏。

相關文章
相關標籤/搜索