面向0基础的FastAPI教程:4.JWT 认证与权限控制 —— 给接口加 “安全锁”
上一篇我们实现了用户的 CRUD 接口,但有个大问题:任何人都能调用这些接口—— 比如随便一个人就能访问/api/users获取所有用户信息,甚至删除用户。这篇我们要解决 **“接口安全”** 问题:用 JWT(JSON Web Token)实现 “登录认证”,用权限控制实现 “不同角色访问不同接口”,让系统更符合企业级要求。
一、先搞懂:JWT 到底是啥?
小白最常问:“JWT 和密码有啥区别?”—— 举个生活中的例子:
- 你去小区健身房,第一次需要用身份证(用户名 + 密码)登记,前台给你一张电子门禁卡(JWT);
- 之后每次去,不需要再出示身份证,直接刷门禁卡就行 —— 门禁卡里面存了你的身份信息(比如 “住户小明”),还带防伪标识(别人伪造不了)。
JWT 的核心价值:
- 「一次登录,多接口复用」:登录后拿到 JWT,之后所有请求都带这个 Token,不用每次都传用户名密码;
- 「无状态」:服务器不用存用户会话,Token 本身包含了所有必要信息;
- 「安全」:Token 有签名,篡改后会失效。
JWT 的结构(3 部分用 “.” 连接)
比如一个实际的 JWT:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InhpYW9taW5nIiwiZXhwIjoxNzE2MjM5MjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c拆成 3 部分:
- Header(头部):存 Token 类型(JWT)和签名算法(比如 HS256);
{"alg": "HS256", "typ": "JWT"} - Payload(负载):存用户信息(比如用户 ID、用户名)和过期时间;
{"user_id": 1, "username": "xiaoming", "exp": 1716239200} - Signature(签名):用 Header 的算法 + 密钥(比如
secret_key)对前两部分加密,防止篡改;HMACSHA256( base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secret_key )
二、准备工作:配置 JWT 参数
先在.env文件里加 JWT 配置(避免硬编码):
# .env文件
# 之前的数据库配置...
JWT_SECRET_KEY=your-secret-key-keep-it-safe # JWT密钥(要复杂,比如用openssl rand -hex 32生成)
JWT_ALGORITHM=HS256 # 签名算法
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 # Token过期时间(30分钟)
然后在app/config.py里读取这些配置:
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# 数据库配置...
# JWT配置
JWT_SECRET_KEY: str
JWT_ALGORITHM: str
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int
class Config:
env_file = ".env"
settings = Settings()
三、实现 JWT 工具类:生成与验证 Token
写一个工具类app/utils/jwt_utils.py,封装 JWT 的生成和验证逻辑:
from datetime import datetime, timedelta
from jose import JWTError, jwt
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.config import settings
from app.models.user import User
from app.services.user_service import UserService
# 初始化UserService
user_service = UserService()
def create_access_token(data: dict) -> str:
"""生成JWT Token"""
to_encode = data.copy()
# 设置过期时间:当前时间 + 配置的过期分钟数
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) # 把过期时间加入Payload
# 生成Token:用密钥和算法加密
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
def verify_access_token(token: str, db: Session) -> User:
"""验证Token,返回对应的用户"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的Token或已过期",
headers={"WWW-Authenticate": "Bearer"}, # 告诉客户端需要用Bearer Token
)
try:
# 解码Token
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
# 从Payload中获取用户ID
user_id: int = payload.get("user_id")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# 查询用户是否存在
user = user_service.get_by_id(db, user_id)
if user is None:
raise credentials_exception
return user
四、实现登录接口:获取 JWT Token
登录接口是唯一不需要认证的接口 —— 用户用用户名 + 密码换 Token,之后用 Token 访问其他接口。
1. 写登录的 Pydantic 模型(app/schemas/auth.py)
from pydantic import BaseModel, Field
class LoginRequest(BaseModel):
"""登录请求体模型"""
username: str = Field(..., min_length=3, max_length=50, description="用户名")
password: str = Field(..., min_length=6, description="密码")
class TokenResponse(BaseModel):
"""登录响应体模型"""
access_token: str # JWT Token
token_type: str = "bearer" # Token类型(固定为bearer)
2. 写登录接口(app/routes/auth_router.py)
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db import get_db
from app.schemas.auth import LoginRequest, TokenResponse
from app.services.user_service import UserService
from app.utils.jwt_utils import create_access_token
# 创建认证路由实例
auth_router = APIRouter(prefix="/api/auth", tags=["Auth"])
# 初始化UserService
user_service = UserService()
@auth_router.post("/login", response_model=TokenResponse, status_code=status.HTTP_200_OK)
def login(login_data: LoginRequest, db: Session = Depends(get_db)):
"""登录接口:用用户名+密码换JWT Token"""
# 1. 根据用户名查询用户
user = user_service.get_by_fields(db, username=login_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
# 2. 验证密码(对比明文密码和数据库的哈希密码)
if not user_service.verify_password(login_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
# 3. 生成JWT Token(Payload中存用户ID)
access_token = create_access_token(data={"user_id": user.id})
# 4. 返回Token
return {"access_token": access_token, "token_type": "bearer"}
五、给接口加认证:只有登录用户才能访问
现在要让所有用户接口都需要认证—— 比如访问/api/users必须带 JWT Token。实现方式是依赖注入:写一个get_current_user依赖,解析请求中的 Token,获取当前用户。
1. 写认证依赖(app/dependencies/auth.py)
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.db import get_db
from app.models.user import User
from app.utils.jwt_utils import verify_access_token
# OAuth2PasswordBearer:FastAPI提供的Bearer Token验证工具
# tokenUrl:登录接口的路径(用于Swagger文档自动生成登录按钮)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> User:
"""依赖:获取当前登录用户"""
return verify_access_token(token, db)
2. 修改用户接口:添加认证依赖
回到app/routes/user_router.py,给每个接口添加current_user: User = Depends(get_current_user)依赖:
from app.dependencies.auth import get_current_user # 导入认证依赖
from app.models.user import User # 导入User模型
# 示例:修改创建用户接口
@user_router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(
user_in: UserCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user) # 添加认证依赖
):
"""创建用户接口(需要登录)"""
return user_service.create_user(db, user_in)
# 同理,修改其他接口:
# get_user、get_users、update_user、delete_user都要加current_user依赖
3. 测试认证效果
启动服务,用 Swagger 文档测试:
- 先调用
/api/auth/login接口,输入用户名 + 密码,拿到access_token; - 点击 Swagger 文档右上角的 **“Authorize”** 按钮,输入
Bearer {access_token}(比如Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...); - 之后调用
/api/users接口,就能正常返回数据 —— 如果没登录或 Token 过期,会返回 401 错误(“无效的 Token 或已过期”)。
六、实现权限控制:不同角色访问不同接口
认证解决了 “你是谁” 的问题,权限控制解决 “你能做什么” 的问题 —— 比如:
- 普通用户只能访问自己的信息(
/api/users/me); - 管理员能访问所有用户信息(
/api/users)和删除用户(/api/users/{user_id})。
1. 写权限依赖(app/dependencies/permissions.py)
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.dependencies.auth import get_current_user
from app.models.user import User
from app.services.user_service import UserService
# 初始化UserService
user_service = UserService()
def has_permission(required_perm: str):
"""依赖:检查当前用户是否有指定权限"""
def dependency(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
# 1. 获取用户的所有权限(从角色中关联)
user_permissions = set()
for role in current_user.roles:
for perm in role.permissions:
user_permissions.add(perm.code)
# 2. 检查是否有需要的权限
if required_perm not in user_permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有访问权限"
)
return current_user
return dependency
2. 修改接口:添加权限依赖
比如,删除用户接口只有管理员能调用(需要user:delete权限),修改user_router.py:
from app.dependencies.permissions import has_permission # 导入权限依赖
@user_router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(has_permission("user:delete")) # 添加权限依赖
):
"""删除用户接口(只有管理员能调用)"""
user = user_service.get_or_404(db, user_id)
user_service.logical_delete(db, user)
3. 测试权限控制效果
- 用管理员用户(比如之前创建的
xiaoming,有user:delete权限)登录,调用/api/users/1删除接口,能成功; - 用普通用户(没有
user:delete权限)登录,调用删除接口,会返回 403 错误(“没有访问权限”)。
七、常见问题排查
1. 登录接口返回 “用户名或密码错误”
- 原因:用户名不存在,或密码错误;
- 解决:检查用户名是否正确,密码是否哈希正确(比如
verify_password方法是否正确)。
2. 接口返回 401 错误(“无效的 Token 或已过期”)
- 原因:Token 过期,或 Token 被篡改,或请求没带 Token;
- 解决:重新登录获取新 Token,或检查请求头是否带
Authorization: Bearer {token}。
3. 接口返回 403 错误(“没有访问权限”)
- 原因:用户没有对应的权限;
- 解决:给用户分配对应的角色(比如给普通用户添加
user:delete权限)。
八、总结:这篇我们做了什么?
- 搞懂了 JWT 的概念(电子门禁卡),实现了 JWT 的生成与验证;
- 实现了登录接口,用用户名 + 密码换 JWT Token;
- 用依赖注入实现了认证(只有登录用户才能访问接口);
- 用权限依赖实现了权限控制(不同角色访问不同接口);
- 测试了每个接口,确保认证和权限控制生效。
更多推荐


所有评论(0)