既然我們已經(jīng)有了所有的安全流程,就讓我們來使用 JWT 令牌和安全哈希密碼讓應(yīng)用程序真正地安全吧。
你可以在應(yīng)用程序中真正地使用這些代碼,在數(shù)據(jù)庫中保存密碼哈希值,等等。
我們將從上一章結(jié)束的位置開始,然后對示例進行擴充。
JWT 表示 「JSON Web Tokens」。
它是一個將 JSON 對象編碼為密集且沒有空格的長字符串的標準。字符串看起來像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
它沒有被加密,因此任何人都可以從字符串內(nèi)容中還原數(shù)據(jù)。
但它經(jīng)過了簽名。因此,當你收到一個由你發(fā)出的令牌時,可以校驗令牌是否真的由你發(fā)出。
通過這種方式,你可以創(chuàng)建一個有效期為 1 周的令牌。然后當用戶第二天使用令牌重新訪問時,你知道該用戶仍然處于登入狀態(tài)。
一周后令牌將會過期,用戶將不會通過認證,必須再次登錄才能獲得一個新令牌。而且如果用戶(或第三方)試圖修改令牌以篡改過期時間,你將因為簽名不匹配而能夠發(fā)覺。
如果你想上手體驗 JWT 令牌并了解其工作方式,可訪問 https://jwt.io。
我們需要安裝 python-jose 以在 Python 中生成和校驗 JWT 令牌:
pip install python-jose[cryptography]
████████████████████████████████████████ 100%
Python-jose 需要一個額外的加密后端。
這里我們使用的是推薦的后端:pyca/cryptography。
Tip
本教程曾經(jīng)使用過 PyJWT。
但是后來更新為使用 Python-jose,因為它提供了 PyJWT 的所有功能,以及之后與其他工具進行集成時你可能需要的一些其他功能。
「哈?!沟囊馑际牵簩⒛承﹥?nèi)容(在本例中為密碼)轉(zhuǎn)換為看起來像亂碼的字節(jié)序列(只是一個字符串)。
每次你傳入完全相同的內(nèi)容(完全相同的密碼)時,你都會得到完全相同的亂碼。
但是你不能從亂碼轉(zhuǎn)換回密碼。
如果你的數(shù)據(jù)庫被盜,小偷將無法獲得用戶的明文密碼,只能拿到哈希值。
因此,小偷將無法嘗試在另一個系統(tǒng)中使用這些相同的密碼(由于許多用戶在任何地方都使用相同的密碼,因此這很危險)。
PassLib 是一個用于處理哈希密碼的很棒的 Python 包。
它支持許多安全哈希算法以及配合算法使用的實用程序。
推薦的算法是 「Bcrypt」。
因此,安裝附帶 Bcrypt 的 PassLib:
pip install passlib[bcrypt]
████████████████████████████████████████ 100%
Tip
使用 passlib,你甚至可以將其配置為能夠讀取 Django,F(xiàn)lask 的安全擴展或許多其他工具創(chuàng)建的密碼。
因此,你將能夠,舉個例子,將數(shù)據(jù)庫中來自 Django 應(yīng)用的數(shù)據(jù)共享給一個 FastAPI 應(yīng)用?;蛘呤褂猛粩?shù)據(jù)庫但逐漸將應(yīng)用從 Django 遷移到 FastAPI。
而你的用戶將能夠同時從 Django 應(yīng)用或 FastAPI 應(yīng)用登錄。
從 passlib 導(dǎo)入我們需要的工具。
創(chuàng)建一個 PassLib 「上下文」。這將用于哈希和校驗密碼。
Tip
PassLib 上下文還具有使用不同哈希算法的功能,包括僅允許用于校驗的已棄用的舊算法等。
例如,你可以使用它來讀取和校驗由另一個系統(tǒng)(例如Django)生成的密碼,但是使用其他算法例如 Bcrypt 生成新的密碼哈希值。
并同時兼容所有的這些功能。
創(chuàng)建一個工具函數(shù)以哈希來自用戶的密碼。
然后創(chuàng)建另一個工具函數(shù),用于校驗接收的密碼是否與存儲的哈希值匹配。
再創(chuàng)建另一個工具函數(shù)用于認證并返回用戶。
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
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")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_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):
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)):
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
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}]
Note
如果你查看新的(偽)數(shù)據(jù)庫 fake_users_db,你將看到哈希后的密碼現(xiàn)在的樣子:"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"。
導(dǎo)入已安裝的模塊。
創(chuàng)建一個隨機密鑰,該密鑰將用于對 JWT 令牌進行簽名。
要生成一個安全的隨機密鑰,可使用以下命令:
openssl rand -hex 32
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
然后將輸出復(fù)制到變量 「SECRET_KEY」 中(不要使用示例中的這個)。
創(chuàng)建用于設(shè)定 JWT 令牌簽名算法的變量 「ALGORITHM」,并將其設(shè)置為 "HS256"。
創(chuàng)建一個設(shè)置令牌過期時間的變量。
定義一個將在令牌端點中用于響應(yīng)的 Pydantic 模型。
創(chuàng)建一個生成新的訪問令牌的工具函數(shù)。
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
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")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_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):
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)):
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
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}]
更新 get_current_user 以接收與之前相同的令牌,但這次使用的是 JWT 令牌。
解碼接收到的令牌,對其進行校驗,然后返回當前用戶。
如果令牌無效,立即返回一個 HTTP 錯誤。
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
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")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_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):
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)):
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
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}]
使用令牌的過期時間創(chuàng)建一個 timedelta 對象。
創(chuàng)建一個真實的 JWT 訪問令牌并返回它。
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
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")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_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):
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)):
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
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}]
JWT 的規(guī)范中提到有一個 sub 鍵,值為該令牌的主題。
使用它并不是必須的,但這是你放置用戶標識的地方,所以我們在示例中使用了它。
除了識別用戶并允許他們直接在你的 API 上執(zhí)行操作之外,JWT 還可以用于其他事情。
例如,你可以識別一個 「汽車」 或 「博客文章」。
然后你可以添加關(guān)于該實體的權(quán)限,比如「駕駛」(汽車)或「編輯」(博客)。
然后,你可以將 JWT 令牌交給用戶(或機器人),他們可以使用它來執(zhí)行這些操作(駕駛汽車,或編輯博客文章),甚至不需要有一個賬戶,只需使用你的 API 為其生成的 JWT 令牌。
使用這樣的思路,JWT 可以用于更復(fù)雜的場景。
在這些情況下,幾個實體可能有相同的 ID,比如說 foo(一個用戶 foo,一輛車 foo,一篇博客文章 foo)。
因此,為了避免 ID 沖突,當為用戶創(chuàng)建 JWT 令牌時,你可以在 sub 鍵的值前加上前綴,例如 username:。所以,在這個例子中,sub 的值可以是:username:johndoe。
要記住的重點是,sub 鍵在整個應(yīng)用程序中應(yīng)該有一個唯一的標識符,而且應(yīng)該是一個字符串。
運行服務(wù)器并訪問文檔: http://127.0.0.1:8000/docs。
你會看到如下用戶界面:
像以前一樣對應(yīng)用程序進行認證。
使用如下憑證:
用戶名: johndoe 密碼: secret
Check
請注意,代碼中沒有任何地方記錄了明文密碼 「secret」,我們只保存了其哈希值。
訪問 /users/me/ 端點,你將獲得如下響應(yīng):
{
"username": "johndoe",
"email": "johndoe@example.com",
"full_name": "John Doe",
"disabled": false
}
如果你打開開發(fā)者工具,將看到數(shù)據(jù)是如何發(fā)送的并且其中僅包含了令牌,只有在第一個請求中發(fā)送了密碼以校驗用戶身份并獲取該訪問令牌,但之后都不會再發(fā)送密碼:
Note
注意請求中的 Authorization 首部,其值以 Bearer 開頭。
OAuth2 具有「作用域」的概念。
你可以使用它們向 JWT 令牌添加一組特定的權(quán)限。
然后,你可以將此令牌直接提供給用戶或第三方,使其在一些限制下與你的 API 進行交互。
你可以在之后的進階用戶指南中了解如何使用它們以及如何將它們集成到 FastAPI 中。
通過目前你所看到的,你可以使用像 OAuth2 和 JWT 這樣的標準來構(gòu)建一個安全的 FastAPI 應(yīng)用程序。
在幾乎所有的框架中,處理安全性問題都很容易成為一個相當復(fù)雜的話題。
許多高度簡化了安全流程的軟件包不得不在數(shù)據(jù)模型、數(shù)據(jù)庫和可用功能上做出很多妥協(xié)。而這些過于簡化流程的軟件包中,有些其實隱含了安全漏洞。
FastAPI 不對任何數(shù)據(jù)庫、數(shù)據(jù)模型或工具做任何妥協(xié)。
它給了你所有的靈活性來選擇最適合你項目的前者。
你可以直接使用許多維護良好且使用廣泛的包,如 passlib 和 python-jose,因為 FastAPI 不需要任何復(fù)雜的機制來集成外部包。
但它為你提供了一些工具,在不影響靈活性、健壯性和安全性的前提下,盡可能地簡化這個過程。
而且你可以用相對簡單的方式使用和實現(xiàn)安全、標準的協(xié)議,比如 OAuth2。
你可以在進階用戶指南中了解更多關(guān)于如何使用 OAuth2 「作用域」的信息,以實現(xiàn)更精細的權(quán)限系統(tǒng),并同樣遵循這些標準。帶有作用域的 OAuth2 是很多大的認證提供商使用的機制,比如 Facebook、Google、GitHub、微軟、Twitter 等,授權(quán)第三方應(yīng)用代表用戶與他們的 API 進行交互。
更多建議: