API安全大揭秘:认证与授权的双面舞会


title: API安全大揭秘:认证与授权的双面舞会

date: 2025/05/28 12:14:35

updated: 2025/05/28 12:14:35

author: cmdragon

excerpt:

API安全的核心需求包括认证与授权机制。认证验证用户身份,如用户名密码登录;授权验证用户是否有权限执行特定操作,如管理员删除数据。典型安全威胁包括未授权访问、凭证泄露和权限提升。FastAPI通过OpenAPI规范支持OAuth2、HTTP Basic等安全方案,依赖注入系统实现灵活验证。OAuth2协议通过授权请求、授权许可、访问令牌等步骤确保安全访问。FastAPI实现OAuth2密码流程示例包括环境准备、核心代码实现和运行测试,确保用户身份验证和权限控制。

categories:

  • 后端开发
  • FastAPI

tags:

  • API安全
  • 认证与授权
  • OAuth2协议
  • FastAPI
  • 安全威胁
  • 依赖注入
  • 访问令牌

扫描二维码

关注或者微信搜一搜:编程智域 前端至全栈交流与成长

探索数千个预构建的 AI 应用,开启你的下一个伟大创意https://tools.cmdragon.cn/

第一章:理解API安全的基本需求

为什么需要认证与授权机制

认证(Authentication)与授权(Authorization)的区别

  • 认证 :验证用户身份的过程(例如:用户名密码登录)。 类比:进入公司大楼时出示工牌(证明你是员工)
  • 授权 :验证用户是否有权限执行特定操作(例如:管理员删除数据)。 类比:不同工牌对应不同的门禁权限(普通员工不能进入机房)

典型安全威胁场景

  1. 未授权访问 :攻击者直接调用/admin/delete-data接口删除数据
  2. 凭证泄露:用户密码在传输过程中被窃取
  3. 权限提升:普通用户越权访问管理员接口

FastAPI的安全设计原则

  • 内置支持OpenAPI规范的安全方案(OAuth2、HTTP Basic等)
  • 通过依赖注入系统实现灵活的安全验证逻辑
  • 自动生成交互式API文档中的安全测试界面

OAuth2协议在Web服务中的应用场景

OAuth2核心概念图解

复制代码
+--------+                               +---------------+
|        |--(A) 授权请求 ->---------------|  资源所有者    |
|        |                               | (用户)       |
|        |<-(B) 授权许可 ----------------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C) 授权许可 ->---------------| 授权服务器     |
| 客户端  |                               | (签发令牌)   |
|        |<-(D) 访问令牌 ----------------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E) 访问令牌 ->---------------| 资源服务器     |
|        |                               | (存储数据)   |
|        |<-(F) 受保护资源 --------------|               |
+--------+                               +---------------+

FastAPI实现OAuth2密码流程示例

环境准备

bash 复制代码
# 安装依赖库(指定版本保证兼容性)
pip install fastapi==0.68.0 uvicorn==0.15.0 
pip install python-jose[cryptography]==3.3.0 
pip install passlib[bcrypt]==1.7.4

核心代码实现

python 复制代码
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
from datetime import datetime, timedelta

# 安全配置常量
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 密码加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2方案声明
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


# 用户数据模型
class User(BaseModel):
    username: str
    disabled: bool = False


class UserInDB(User):
    hashed_password: str


# 令牌生成函数
def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


# 认证依赖项
async def get_current_user(token: str = Depends(oauth2_scheme)):
    credential_exception = HTTPException(
        status_code=401,
        detail="无法验证凭证",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credential_exception
    except JWTError:
        raise credential_exception

    # 实际项目应查询数据库
    user = UserInDB(
        username=username,
        hashed_password="fakehash",
        disabled=False
    )
    if user.disabled:
        raise HTTPException(status_code=400, detail="用户已被禁用")
    return user


app = FastAPI()


@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    # 验证用户名密码(示例硬编码)
    if form_data.username != "testuser" or form_data.password != "testpass":
        raise HTTPException(
            status_code=401,
            detail="用户名或密码错误",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token = create_access_token(
        data={"sub": form_data.username}
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/protected/")
async def read_protected_route(current_user: User = Depends(get_current_user)):
    return {"message": "已授权访问", "user": current_user.username}

运行与测试

bash 复制代码
uvicorn main:app --reload

打开浏览器访问 http://localhost:8000/docs,在Swagger界面中:

  1. 点击 /token 端点,输入测试凭证(username: testuser, password: testpass)
  2. 复制返回的access_token
  3. 点击 /protected/ 端点,在Authorization弹窗中输入 Bearer <your-token>

课后Quiz

Q1:认证与授权的根本区别是什么?

A) 认证确认身份,授权验证权限

B) 授权在前,认证在后

C) 两者是同义词
点击查看答案 正确答案:A 解析:认证是验证用户身份的过程(如登录),授权是验证该身份是否有权限执行特定操作(如访问管理员接口)。

Q2:OAuth2的授权码流程包含哪些主要步骤?

A) 客户端直接获取访问令牌

B) 用户授权 → 获取授权码 → 交换访问令牌

C) 用户名密码直接传递给资源服务器
点击查看答案 正确答案:B 解析:完整的授权码流程需要通过授权服务器中转授权码,避免客户端直接接触用户凭证。


常见报错解决方案

报错:422 Unprocessable Entity

json 复制代码
{
  "detail": [
    {
      "loc": [
        "body",
        "password"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

原因分析

  • 请求体缺少必填字段(如password字段)
  • 字段数据类型不匹配(例如数字传入了字符串)

解决方法

  1. 检查Swagger文档中的请求体模型
  2. 使用Postman验证请求体格式:
json 复制代码
{
  "username": "testuser",
  "password": "testpass"
}
  1. 在Pydantic模型中使用...表示必填字段:
python 复制代码
class LoginRequest(BaseModel):
    username: str
    password: str  # 必填字段

预防建议

  • 启用Pydantic的严格模式:
python 复制代码
from pydantic import StrictStr


class LoginRequest(BaseModel):
    username: StrictStr
    password: StrictStr
  • 在路由中使用response_model_exclude_unset=True过滤未设置字段

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:API安全大揭秘:认证与授权的双面舞会 | cmdragon's Blog

往期文章归档: