有一個家用需求:

以rest api的形式釋出一個服務(函式)

訪問這個服務需要驗證,確切說是token(JWT)認證

訪問這個服務的request/response資料需要被校驗

自帶如何使用這個api的文件

簡單點

官方文件詳細豐富

效能最不重要

python實現的

就功能實現上,任何一個主流web後端框架都具備,但自動生成api使用文件,對資料校驗方便直觀,整體使用簡單等,FastAPI就具備這些。

FastAPI是一個基於python的web後端框架,使用起來開發效率高,更少的程式碼量;開發完畢後,會

自動生成API使用文件

,扔給對方就可以了;基於pydantic,方便的對

資料校驗

;更高的效能,

支援ASGI規範

,也就是支援非同步,

支援WebSocket

等等還有一些其他web後端框架,例如

Django: 重型web框架,功能強大,官方文件豐富,很適合開發正兒八經的web應用

Flask:中型web框架,web應用,rest api用途,都可以

Bottle: 微型web框架,用來做個rest api用途,再合適不過了,這個框架本身就一個單檔案,沒有任何其他依賴,也就是說在安裝了python直譯器的環境,把這個單檔案複製過來就可以使用了,在一些Network OS裡面,就是使用的這個框架對外提供簡單的服務。

下面給出在官方的demo的基礎上完善的程式碼,有需要拿過去再改改就可以跑生產:

from

datetime

import

datetime

timedelta

from

typing

import

Optional

from

fastapi

import

Depends

FastAPI

HTTPException

status

Body

from

fastapi。security

import

OAuth2PasswordBearer

OAuth2PasswordRequestForm

from

jose

import

JWTError

jwt

from

passlib。context

import

CryptContext

from

pydantic

import

BaseModel

# https://fastapi。tiangolo。com/tutorial/security/oauth2-jwt/

# to get a string like this run in linux:

# openssl rand -hex 32

SECRET_KEY

=

“c4af8692b37bcf2d575c5958254eee21a049cf01925207beb8e4f02a5c0c9593”

ALGORITHM

=

“HS256”

ACCESS_TOKEN_EXPIRE_MINUTES

=

30

# secret01

fake_users_db

=

{

“johndoe”

{

“username”

“johndoe”

“full_name”

“John Doe”

“email”

“johndoe@example。com”

“hashed_password”

“$2b$12$X8w2pubd67dP8JsAijamkejzK1LIiY。JMTh7qAgscb9TVkSHkT0sy”

“disabled”

False

}

}

class

Test01_data_model

BaseModel

):

infor

Optional

str

=

None

class

Test02_data_model

BaseModel

):

username

str

MFAtoken

str

addtion

Optional

str

=

None

class

Token

BaseModel

):

access_token

str

token_type

str

class

TokenData

BaseModel

):

username

Optional

str

=

None

class

User

BaseModel

):

username

str

email

Optional

str

=

None

full_name

Optional

str

=

None

disabled

Optional

bool

=

None

class

UserInDB

User

):

hashed_password

str

pwd_context

=

CryptContext

schemes

=

“bcrypt”

],

deprecated

=

“auto”

# state this url-“\token” is used for get token only

oauth2_scheme

=

OAuth2PasswordBearer

tokenUrl

=

“token”

app

=

FastAPI

()

def

verify_password

plain_password

hashed_password

):

return

pwd_context

verify

plain_password

hashed_password

# used for generating hash_password by plain_password

def

get_password_hash

password

):

return

pwd_context

hash

password

def

get_user

db

username

str

):

if

username

in

db

user_dict

=

db

username

return

UserInDB

**

user_dict

def

authenticate_user

fake_db

username

str

password

str

):

user

=

get_user

fake_db

username

if

not

user

return

False

if

not

verify_password

password

user

hashed_password

):

# the former password is a plain,yes!!

return

False

return

user

def

create_access_token

data

dict

expires_delta

Optional

timedelta

=

None

):

to_encode

=

data

copy

()

if

expires_delta

expire

=

datetime

utcnow

()

+

expires_delta

else

expire

=

datetime

utcnow

()

+

timedelta

minutes

=

15

to_encode

update

({

“exp”

expire

})

encoded_jwt

=

jwt

encode

to_encode

SECRET_KEY

algorithm

=

ALGORITHM

return

encoded_jwt

async

def

get_current_user

token

str

=

Depends

oauth2_scheme

)):

‘’‘

this function is used for verifying the token’s expire time and get the user‘s infor

’‘’

print

token

credentials_exception

=

HTTPException

status_code

=

status

HTTP_401_UNAUTHORIZED

detail

=

“Could not validate credentials”

headers

=

{

“WWW-Authenticate”

“Bearer”

},

try

payload

=

jwt

decode

token

SECRET_KEY

algorithms

=

ALGORITHM

])

‘’‘

# A token can be decode to be as the data was encoded before

if the token has expired then it can not be decode and raise a

’ExpiredSignatureError(‘Signature has expired。’)‘ error。

payload = {’sub‘: ’johndoe‘, ’exp‘: 1634789405}

’‘’

print

payload

username

str

=

payload

get

“sub”

if

username

is

None

raise

credentials_exception

token_data

=

TokenData

username

=

username

except

Exception

as

e

print

repr

e

))

raise

credentials_exception

user

=

get_user

fake_users_db

username

=

token_data

username

if

user

is

None

raise

credentials_exception

return

user

async

def

get_current_active_user

current_user

User

=

Depends

get_current_user

)):

if

current_user

disabled

raise

HTTPException

status_code

=

400

detail

=

“Inactive user”

return

current_user

@app。post

“/token”

response_model

=

Token

async

def

login_for_access_token

form_data

OAuth2PasswordRequestForm

=

Depends

()):

user

=

authenticate_user

fake_users_db

form_data

username

form_data

password

if

not

user

raise

HTTPException

status_code

=

status

HTTP_401_UNAUTHORIZED

detail

=

“Incorrect username or password”

headers

=

{

“WWW-Authenticate”

“Bearer”

},

access_token_expires

=

timedelta

minutes

=

ACCESS_TOKEN_EXPIRE_MINUTES

access_token

=

create_access_token

data

=

{

“sub”

user

username

},

expires_delta

=

access_token_expires

return

{

“access_token”

access_token

“token_type”

“bearer”

}

@app。get

“/users/me/”

response_model

=

User

async

def

read_users_me

current_user

User

=

Depends

get_current_active_user

)):

return

current_user

@app。get

“/users/me/items/”

async

def

read_own_items

current_user

User

=

Depends

get_current_active_user

)):

return

[{

“item_id”

“Foo”

“owner”

current_user

username

}]

@app。get

“/test01/”

response_model

=

Test01_data_model

dependencies

=

Depends

get_current_active_user

)])

async

def

test01_app

():

return

{

“infor”

“tokenttttttttt”

}

@app。post

“/test02/”

dependencies

=

Depends

get_current_active_user

)])

async

def

test02_app

user_infor

Test02_data_model

=

Body

。。。

)):

print

user_infor

dict

())

return

{

“infor”

user_infor

}

if

__name__

==

‘__main__’

pass

#import uvicorn

#uvicorn。run(app=‘fastapi_token_demo:app’, host=“127。0。0。1”, port=8000, reload=True, debug=True)

#python -m uvicorn fastapi_token_demo:app ——host ‘127。0。0。1’ ——port 8000 ——reload

一些說明如下:

關於Token認證的流程:

當去訪問一些服務時,如果直接使用使用者憑據,當憑據洩露時,攻擊者便可以登入你主頁,擁有全部控制權,token認證是指先用使用者憑據認證一次,獲取到一個字串,其中被

編碼了使用者資訊sub

和該字串

超時資訊exp

,後續訪問服務時,只需攜帶token即可,即使token洩露,影響是可控的,不同服務類別,獲取不同的token,並且token超時會自動失效,這個也是目前對API類應用訪問的主流認證方式。

需要安裝的庫

pip install fastapi

pip install “uvicorn[standard]” //work as the ASGI server that runs your code

pip install python-multipart //get values from html form data

pip install “python-jose[cryptography]” //generate and verify the JWT tokens

pip install “passlib[bcrypt]” //handle password hashes

生成一個隨機字串,SECRET_KEY ,用於後續JWT的encode 和decode

openssl rand -hex 32

關於 ACCESS_TOKEN_EXPIRE_MINUTES ,由於JWT沒有回收機制,每次生成token後,只有等待超時才能過期,實際使用許謹慎設定過長的token超期時間,當然可以更改SECRET_KEY強制所有token報廢。

fake_users_db ,實際使用可以從資料庫select使用者表資訊

關於使用者的hashed_password,可以由下面定義的get_password_hash 函式生產

get_current_user函式中,呼叫了jwt。decode方法,用於解碼JWT字串,如果不是由同一個例項,jwt。encode方法產生的,或者exp超時,會raise錯誤,這個decode的過程實際是完成了對JWT的認證過程。

關於oauth2_scheme = OAuth2PasswordBearer(tokenUrl=“token”),當被呼叫時,會檢查request的

Authorization

header,如果該頭部值是Bearer+str(token),會取出該值,如果不符合條件,會向客戶端返回404error 和 UNAUTHORIZED資訊, oauth2_scheme是包含token的字串值,可以被其他需要JWT的函式Depends。

fastapi中引入Depends(),讓函式之間的依賴更直觀,邏輯清晰;當A函式被宣告依賴另一個B函式,A執行時,會先執行B,只有當B成功執行,為True時,A函式才會繼續,那麼在所有需要token認證的函式下,宣告依賴get_current_active_user即可,也即是依賴get_current_user函式的jwt。decode方法。

dependencies=[B,C] 用於宣告多個依賴的語法

關於async, 如果你定義的函式下有IO的操作,並且你

不清楚是否支援非同步,請不要用async關鍵字

,沒有async,fastapi會自動判斷是否函式阻塞,如果阻塞,會呼叫執行緒池,但是如果在async函式下,引入了不支援非同步的第三方IO庫,妥妥變成同步!!

關於request 傳參,如果同時存在 fastapi會依據以下特點判斷對應關係

路徑引數:url路徑,函式透過 引數名字識別,對應

查詢引數:例如get方法,宣告為

singular type(

int

float

str

bool

的都為查詢引數

request Body: 對該引數定義了

Pydantic model

的,為Body資料,送到事先定義好的資料模型校驗和轉換,轉成json後賦值給引數,不滿足資料模型校驗會返回客戶端錯誤

from

typing

import

Optional

from

fastapi

import

FastAPI

from

pydantic

import

BaseModel

class

Item

BaseModel

):

name

str

description

Optional

str

=

None

price

float

tax

Optional

float

=

None

app

=

FastAPI

()

@app。put

“/items/{item_id}”

async

def

create_item

item_id

int

item

Item

q

Optional

str

=

None

):

result

=

{

“item_id”

item_id

**

item

dict

()}

if

q

result

update

({

“q”

q

})

return

result

再給個例子:

from

typing

import

Optional

from

fastapi

import

Body

FastAPI

from

pydantic

import

BaseModel

app

=

FastAPI

()

class

Item

BaseModel

):

name

str

description

Optional

str

=

None

price

float

tax

Optional

float

=

None

class

User

BaseModel

):

username

str

full_name

Optional

str

=

None

@app。put

“/items/{item_id}”

async

def

update_item

item_id

int

item

Item

user

User

importance

int

=

Body

。。。

):

results

=

{

“item_id”

item_id

“item”

item

“user”

user

“importance”

importance

}

return

results

需要傳入的json格式

{

“item”: {

“name”: “Foo”,

“description”: “The pretender”,

“price”: 42。0,

“tax”: 3。2

},

“user”: {

“username”: “dave”,

“full_name”: “Dave Grohl”

},

“importance”: 5

}

部署+測試

python -m uvicorn fastapi_token_demo:app ——host ‘127。0。0。1’ ——port 8000 ——reload

啟動後,會自動生成API相關文件

http://

x。x。x。x:8000/docs

這裡使用用Talend(postman)測試

http://

x。x。x。x/token

發post請求,表單資料為

username:johndoe

password:secret01 //取決你實際的hashed_password的明文

認證成功後會返回你一個json,如下:

All in Web | Web後端框架-FastAPI

Talend(postman)工具

在後續的request中新增HTTP 認證頭部,帶上JTW

Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9。eyJzdWIiOiJqb2huZG9lIiwiZXhwIjoxNjM3Mzk4MTEwfQ。apcFjARS2StEnOum3fCJOKWZ02vvaNzE-tsOQgz0pMU

透過JWT認證如下,get

http://

x。x。x。x/users/me/

All in Web | Web後端框架-FastAPI

one more things:

這部分談談完整的部署架構

web後端框架——-WSGI/ASGI————-Ngnix/Apache————Brower

這裡區分下Ngnix/Apache 和uWSGI(Web Server Gateway Interface)的作用:

web框架只負責處理各種業務邏輯,uWSGI負責接收client的網路請求

,呼叫(函式呼叫)web框架處理請求,接收web框架的資料響應,返回資料給client,uWSGI和web框架間按照一定規則呼叫和被呼叫,共同完成對client的請求,這個規則的集合就是WSGI,具體的實現之一是uWSGI,由於標準化,

uWSGI可以對接多種web框架

All in Web | Web後端框架-FastAPI

ASGI(

Asynchronous Server Gateway Interface

)是對WSGI標準的擴充套件,包含WSGI的全部特性,新增加了,websocket協議,非同步 的支援,具體的實現有

Uvicorn,

那麼好像對於一個完整的訪問,沒有Ngnix/Apache這些元件的什麼事,確實是!

在實際生產部署中,Ngnix/Apache這類的元件可以算是對uWSGI和

Uvicorn一個助力,

例如 對uWSGI和Uvicorn server的負載;接管來自client的請求中包含的靜態資源的訪問;實現SSL offload;對Client請求的URL的攔截等等,其他‘動態’的資源,計算處理啊,資料庫查詢等請求會轉發到uWSGI和Uvicorn server。

All in Web | Web後端框架-FastAPI

All in Web | Web後端框架-FastAPI

fastapi高效能部署架構:

對於WSGI web框架,以上就是實際的部署架構了,但是對於ASGI web框架,如果要充分發揮高效能,高併發,就有一些改變了,GitHub有一個專案,把該架構整體打包,作為一個docker映象釋出,方便大家使用

All in Web | Web後端框架-FastAPI

fastapi高效能部署架構

All in Web | Web後端框架-FastAPI

fastapi——ASGI————-WSGI——-Ngnix/Apache————Brower

增加一層WSGI元件,使用的是Gunicorn,另外一個WSGI的實現,ASGI的實現是使用的

Uvicorn,

Gunicorn的作用是當作一個程序管理器,管理多個

Uvicorn程序,提高併發,

說起來很複雜,但藉助

tiangolo/uvicorn-gunicorn-fastapi-docker 這個開源專案,部署和使用起來都非常簡單

來試試看,假設你的業務程式碼已經準備好,放在本地app資料夾下

官方GitHub上提供了基於各個python版本的docker映象,這次我選擇uvicorn+gunicorn+fastapi+python3。8 docker映象,剩下的只需要在把app資料夾和程式碼需要的第三方python庫加入,重新docker build即可

假如我用fastapi釋出了一個基於selenium庫的web自動化功能,並且這個功能需要fastapi token認證,那麼我的

requirements.txt 檔案如下:

selenium==3。141。0

python-jose[cryptography]==3。3。0

passlib[bcrypt]==1。7。4

python-multipart==0。0。5

Dockerfile檔案如下:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3。8

LABEL maintainer=“Sebastian Ramirez

COPY requirements。txt /tmp/requirements。txt

RUN pip install ——no-cache-dir -r /tmp/requirements。txt

COPY 。/app /app

我的檔案目錄如下:

|—— app

| |—— main。py

| |—— selenium_timesheet。py

| `—— stealth。min。js

|—— Dockerfile

|—— README。md

`—— requirements。txt

務必把fastapi主檔案更改為main。py

docker build -t myimage 。

docker run -d ——name mycontainer_fastapi -p 8000:80 myimage

以上。