OAuth2 密码流程与令牌签发
实现 OAuth2PasswordBearer 方案,使用 passlib 哈希密码,并在登录时签发带签名的访问令牌。
OAuth2 密码流程与令牌签发 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
The OAuth2 Password Flow in Plain English
The OAuth2 password flow (a.k.a. the Resource Owner Password Credentials grant) is the simplest way to authenticate a first-party client: the user sends their username and password directly to your API, and the API hands back a signed access token.
- The client posts credentials once to a
/tokenendpoint. - The server verifies them against the database.
- On success it returns a short-lived JWT access token.
- Every later request carries that token in the
Authorization: Bearer <token>header.
FastAPI gives us ready-made building blocks for exactly this: OAuth2PasswordBearer and OAuth2PasswordRequestForm.
Declaring the OAuth2PasswordBearer Scheme
OAuth2PasswordBearer is a FastAPI dependency that knows how to pull a bearer token out of the Authorization header. You create one instance and point its tokenUrl at the login endpoint that issues tokens.
tokenUrlis a relative path — it tells the docs UI where clients should request a token.- Using the scheme as a dependency makes the endpoint require a token; a missing or malformed header returns 401 automatically.
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
# 'token' matches the path of our login route below
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/users/me")
async def read_me(token: str = Depends(oauth2_scheme)):
# FastAPI extracts the raw bearer token string for us
return {"token": token}Hashing Passwords with passlib
You must never store raw passwords. Hash them with a strong, salted algorithm. The passlib library wraps bcrypt behind a clean CryptContext API.
hash()produces a salted digest you store in the database.verify()compares a plaintext attempt against the stored hash in constant time.- bcrypt is deliberately slow, which frustrates brute-force attacks.
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
stored = hash_password("s3cret")
print("stored looks like:", stored[:7], "...")
print("correct ->", verify_password("s3cret", stored))
print("wrong ->", verify_password("nope", stored))Modeling Users and a Tiny Fake Database
Before issuing tokens we need somewhere to look users up. In production this is your real database; for learning we use an in-memory dict. Notice the stored field is hashed_password, never the plaintext.
- A Pydantic model gives the user a typed shape.
- A
get_user()helper centralizes lookups.
from pydantic import BaseModel
class UserInDB(BaseModel):
username: str
hashed_password: str
disabled: bool = False
fake_users_db = {
"alice": UserInDB(
username="alice",
hashed_password="$2b$12$exampleexampleexamplehashvalue",
)
}
def get_user(username: str):
return fake_users_db.get(username)Authenticating the Credentials
Authentication ties the pieces together: find the user, then verify the supplied password against the stored hash. Return the user on success, or a falsy value on failure.
- Look the user up first; if absent, fail.
- Then call
verify_password— do not short-circuit before hashing to keep timing roughly uniform. - The caller decides how to respond (usually a 401).
def authenticate_user(db, username: str, password: str):
user = db.get(username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return userWhat a JWT Actually Is
A JSON Web Token is three base64url segments joined by dots: header.payload.signature.
- The header names the algorithm, e.g.
HS256. - The payload holds claims like
sub(subject) andexp(expiry). - The signature is an HMAC of header+payload using your secret key.
JWTs are signed, not encrypted — anyone can read the payload, but nobody can forge it without the secret. Never put passwords or sensitive data in the payload.
Encoding a Signed Access Token
We sign tokens with the python-jose library (or PyJWT). Always include an exp claim so tokens expire. Store the username in the sub claim — it identifies who the token belongs to.
SECRET_KEYmust be long, random, and kept out of source control.- Set a short lifetime (e.g. 15-30 minutes) for access tokens.
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "replace-with-a-long-random-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
token = create_access_token({"sub": "alice"})
print("issued token segments:", token.count(".") + 1)The Token Response Shape
The OAuth2 spec dictates the JSON your /token endpoint returns. At minimum it must include access_token and token_type, where the type is the literal string "bearer".
- Clients read
token_typeto know how to send the credential back. - A Pydantic
Tokenmodel documents and validates the response.
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
example = Token(access_token="eyJhbGci...", token_type="bearer")
print(example.model_dump())Wiring the /token Login Endpoint
The login route depends on OAuth2PasswordRequestForm, which reads form-encoded username and password fields (not JSON) — exactly what the OAuth2 password flow requires. On success it returns the Token response.
- Failed auth raises 401 with a
WWW-Authenticate: Bearerheader. - The
subclaim carries the username forward into the token.
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
app = FastAPI()
@app.post("/token", response_model=Token)
async def login(form: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form.username, form.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token({"sub": user.username})
return Token(access_token=access_token, token_type="bearer")Decoding the Token to Find the Current User
A protected route depends on oauth2_scheme to receive the raw token, then decodes it. If the signature is invalid or the token is expired, jwt.decode raises JWTError and we return 401.
- Read the username from the
subclaim. - Re-load the user from the database to confirm they still exist and are active.
from fastapi import Depends, HTTPException, status
from jose import JWTError, jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exc = 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 = payload.get("sub")
if username is None:
raise credentials_exc
except JWTError:
raise credentials_exc
user = get_user(username)
if user is None:
raise credentials_exc
return userSecurity Practices That Matter
The mechanics work, but production hardening makes them safe:
- Secret key: load
SECRET_KEYfrom an environment variable; rotate it if leaked. - HTTPS only: tokens in headers are plaintext on the wire — TLS is mandatory.
- Short expiry: keep access tokens brief and pair them with longer-lived refresh tokens.
- Pin the algorithm: pass an explicit
algorithms=["HS256"]list tojwt.decodeto block thealg: noneattack. - Generic errors: say "Incorrect username or password", never reveal which one was wrong.
Quick Check: The /token Endpoint
Time to test your understanding of how the FastAPI /token login route consumes credentials.
Recap: From Password to Bearer Token
You implemented the full OAuth2 password flow in FastAPI:
- OAuth2PasswordBearer declares the bearer scheme and extracts tokens from the
Authorizationheader. - passlib + bcrypt hash and verify passwords so plaintext is never stored.
- authenticate_user looks up the user and verifies the hash, returning 401 on failure.
- The /token route reads form credentials via
OAuth2PasswordRequestFormand issues a signed JWT with asubclaim and anexpexpiry. - get_current_user decodes and validates the token, pinning the algorithm to block forgery.
With HTTPS, an env-loaded secret, and short token lifetimes, this is a solid, idiomatic authentication foundation.
常见问题解答
「OAuth2 密码流程与令牌签发」课时是免费的吗?
是的 — 「OAuth2 密码流程与令牌签发」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
「OAuth2 密码流程与令牌签发」这节课中我会学到什么?
实现 OAuth2PasswordBearer 方案,使用 passlib 哈希密码,并在登录时签发带签名的访问令牌。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 FastAPI Backend Development Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「OAuth2 密码流程与令牌签发」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?
能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- OAuth2 密码流程与令牌签发
- 使用 python-jose 签名与验证 JWT
- 刷新令牌与令牌轮换
- 基于作用域的授权与角色守卫