读后笔记 -- FastAPI 构建Python微服务 Chapter7:保护 REST API 的安全

7.3 基于密码的验证 OAuth2 规范

1. 项目结构

## 2. 使用 OAuth2 进行身份验证
### 需要安装的包
```bash
pip install python-multipart
```

### 项目结构
```
The Application
├── api/                                # 接口包                  
│   └── __init__.py         
│   └── login.py                        # router,接口定义,运行事务,使用自定义表单提交
│   └── setup.py                        # router,接口定义,运行事务,使用 OpenAPI 提供的 OAuth2 表单
├── db_config/                          # 数据库配置                
│   └── __init__.py              
│   └── sqlalchemy_connect.py           # 设置数据库连接  
├── models/                             # 模型层                 
│   |── data
│   │   └── __init__.py         
│   │   └── sqlalchemy_models.py        # 定义表及关联              
│   |── requests
│   │   └── __init__.py         
│   │   └── login.py                    # 定义请求结构
│   │   └── signup.py                   # 定义请求结构                                               
│   └── __init__.py                                           
├── repository/                         # 存储库包                 
│   └── __init__.py                   
│   └── login.py                        # 创建连接 CRUD    
│   └── signup.py                       # 创建连接 CRUD   
├── security/                           # 安全层               
│   └── __init__.py                   
│   └── secure.py                       # 验证处理
│── __init__.py
└── main.py                             # 生成 app,app.include_router(api 的 router)                                 
```

### 运行
1. 启动 main.py

 

2. 重要的几处代码:

2.1 secure 部分

# content of security\secure.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Login
from repository.login import LoginRepository

# CryptContext: passlib 库中的一个类,提供了一个用于密码哈希的上下文。允许指定支持的哈希算法列表、设置默认配置,并能轻松地对密码进行哈希和验证,而不需要管理每个单独算法的具体细节
# schemes: 传递给CryptContext构造函数的一个参数,是一个列表,指定了该上下文应支持的哈希算法
crypt_context = CryptContext(schemes=["sha256_crypt", "md5_crypt"])

# OAuth2PasswordBearer:实现了OAuth2的资源所有者密码凭证授予类型(Resource Owner Password Credentials Grant)。
#   这种授权类型允许用户通过提供他们的用户名和密码直接从客户端获取访问令牌(access token),而不是先获取授权码。
#   这种方式通常用于信任的客户端或者原生应用,因为它涉及到了用户的敏感信息(密码)。
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/ch07/login/token")

def verify_password(plain_password, hashed_password):
    return crypt_context.verify(plain_password, hashed_password)

def authenticate(username, password, account: Login):
    try:
        password_check = verify_password(password, account.passphrase)
        return password_check
    except Exception as e:
        print(e)
        return False

def get_current_user(token: str = Depends(oauth2_scheme), sess: Session = Depends(sess_db)):
    loginrepo = LoginRepository(sess)
    user = loginrepo.get_all_login_username(token)
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Invalid authentication credentials",
                            headers={"WWW-Authenticate": "Bearer"},
                            )
    return user

解释:

"""
    实际上,当你定义 token: str = Depends(oauth2_scheme) 时,FastAPI中的OAuth2PasswordBearer并不会直接从你提到的响应
        {"access_token": "bruce", "token_type": "bearer"} 中提取出仅"access_token"的值"bruce"。相反,整个token字符串,
        通常是包含前缀"Bearer "的部分,会作为参数传递给你的get_current_user函数。

    对于响应中的 {"access_token": "bruce", "token_type": "bearer"},在标准的OAuth2流程中,客户端会将token类型和访问令牌拼接成
        Authorization头部,格式如:Authorization: Bearer bruce。当FastAPI接收到带有此头部的请求时,oauth2_scheme依赖项实际上会提取
        完整的token字符串"Bearer bruce",而不仅仅是"bruce"。

    所以在你的get_current_user函数里,token实际上会接收到来自请求的完整的认证头信息,即 "Bearer bruce"。但实际操作中,为了使
        OAuth2PasswordBearer正常工作,客户端只需发送"Bearer "后面的部分,即"bruce",而FastAPI的OAuth2PasswordBearer机制恰能妥善
        处理这种情况,它明白需要的是"bruce"这一部分来进行后续验证。

    debug 发现,这里的 token 的值就是页面的 "bruce"
    """

 

2.2.1 通过自定义表单实现验证

# content of api\login.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from db_config.sqlalchemy_connect import sess_db
from repository.login import LoginRepository
from security.secure import authenticate

router = APIRouter()


@router.post("/login/token")
def login(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(sess_db)):
    """
    通过表单方式传递
    :param form_data:
    :param sess:
    :return:
    """
    username = form_data.username
    password = form_data.password
    loginrepo = LoginRepository(sess)
    account = loginrepo.get_all_login_username(username)
    if authenticate(username, password, account) and account is not None:
        return {"access_token": form_data.username, "token_type": "bearer"}
    else:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

2.2.2 使用 OpenAPI 框架 提供的 OAuth2 表单

# content of api\admin.py
from typing import List

from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Signup, Login
from models.request.signup import SignupReq
from repository.signup import SignupRepository
from security.secure import get_current_user

router = APIRouter()

@router.post("/signup/add")
def add_signup(req: SignupReq, sess: Session = Depends(sess_db)):
    """
    没有调用 Depends get_user,所以 Swagger 页面接口后面没有 授权 Icon
    :param req:
    :param sess:
    :return:
    """
    repo: SignupRepository = SignupRepository(sess)
    signup = Signup(password=req.password, username=req.username, id=req.id)
    result = repo.insert_signup(signup)
    if result is True:
        return signup
    else:
        return JSONResponse(content={'message': 'create signup problem encountered'}, status_code=500)

@router.get("/signup/list", response_model=List[SignupReq])
def list_signup(current_user: Login = Depends(get_current_user), sess: Session = Depends(sess_db)):
    """
    通过这里的 Depends get_user 实现调用 框架的 OAuth2 表单,所以 Swagger 页面接口后面有 授权 Icon
    :param current_user:
    :param sess:
    :return:
    """
    repo: SignupRepository = SignupRepository(sess)
    result = repo.get_all_signup()
    return result

 


7.4 基于 JWT 的身份验证

1. 项目结构

## 1. description
使用 token 进行身份验证, 基于 python-jose 生成令牌,但还不是很安全


## 2. 使用 token 进行身份验证
### 需要安装的包
```bash
pip install python-jose
```

### 生成密钥
```bash
openssl rand -hex 32
```

### 项目结构
```
The Application
├── api/                                # 接口包                  
│   └── __init__.py   
│   └── admin.py                        # router,接口定义,运行事务,基于 JWT    
│   └── login.py                        # router,接口定义,运行事务,还是基于 自定义表单提交验证信息
├── db_config/                          # 数据库配置                
│   └── __init__.py              
│   └── sqlalchemy_connect.py           # 设置数据库连接  
├── models/                             # 模型层                 
│   |── data
│   │   └── __init__.py         
│   │   └── sqlalchemy_models.py        # 定义表及关联              
│   |── requests
│   │   └── __init__.py         
│   │   └── login.py                    # 定义请求结构
│   │   └── signup.py                   # 定义请求结构                                               
│   └── __init__.py                                           
├── repository/                         # 存储库包                 
│   └── __init__.py                   
│   └── login.py                        # 创建连接 CRUD    
│   └── signup.py                       # 创建连接 CRUD   
├── security/                           # 安全层               
│   └── __init__.py                   
│   └── secure.py                       # 验证处理
│── __init__.py
└── main.py                             # 生成 app,app.include_router(api 的 router)                             
```

### 运行
1. 启动 main.py
2. 在 swagger 页面,有接口验证的 API 后面,点击解锁 Icon 进行身份验证。对应的接口有调用 Depends(get_current_user),
   调用了 OpenAPI 的 OAuth2,并调用了 "ch07/login/token"
3. "/ch07/login/token" 是通过 api\login.py 接口的端点 "/login/token" 实现的
4. Execute API 接口时,验证传入的JWT令牌有效性,然后从数据库中查找对应的用户,用于后续的请求处理中确定用户身份

 

2. 身份验证部分

# content of securiy\secure.py
from datetime import timedelta, datetime
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Login
from models.request.tokens import TokenData
from repository.login import LoginRepository

crypt_context = CryptContext(schemes=["sha256_crypt", "md5_crypt"])

# 在 ps console 处,通过 "openssl rand -hex 32" 生成 SECRET_KEY
SECRET_KEY = "882fdc269930e113056ec3f630be0c84214c0d0ed477075f0207369548e513a9"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 通过调用 \api\login.py 的端点 "/login/token" 实现 oauth2_scheme
= OAuth2PasswordBearer(tokenUrl="ch07/login/token") def verify_password(plain_password, hashed_password): return crypt_context.verify(plain_password, hashed_password) def authenticate(username, password, account: Login): try: password_check = verify_password(password, account.passphrase) return password_check except Exception as e: print(e) return False def create_access_token(data: dict, expires_after: timedelta): plain_text = data.copy() expire = datetime.utcnow() + expires_after plain_text.update({"exp": expire}) encoded_jwt = jwt.encode(plain_text, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def get_current_user(token: str = Depends(oauth2_scheme), sess: Session = Depends(sess_db)): """ 目的是根据提供的JWT令牌验证用户身份,并从数据库中获取当前用户的信息 :param token: 从请求中提取的JWT令牌(默认通过 oauth2_scheme 依赖项注入) :param sess: 数据库会话对象(由 sess_db 依赖项提供) :return: """ credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}) try: # 尝试使用 jwt.decode 解码 JWT 令牌,其中 SECRET_KEY 和 ALGORITHM 分别是解码所需的密钥和算法 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception # 成功解码后,创建一个 TokenData 实例存储用户名 token_data = TokenData(username=username) except JWTError: raise credentials_exception loginrepo = LoginRepository(sess) user = loginrepo.get_all_login_username(token_data.username) if user is None: raise credentials_exception return user

 

3. 事务

# content of api\login.py
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from repository.login import LoginRepository
from security.secure import authenticate, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES

router = APIRouter()

@router.post("/login/token")
def login(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(sess_db)):
    """
    还是基于 自定义表单提交验证信息
    :param form_data:
    :param sess:
    :return:
    """
    username = form_data.username
    password = form_data.password
    loginrepo = LoginRepository(sess)
    account = loginrepo.get_all_login_username(username)
    if authenticate(username, password, account):
        access_token = create_access_token(data={"sub": username},
                                           expires_after=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
        return {"access_token": access_token, "token_type": "bearer"}
    else:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

 

# content of api\admin.py
from typing import List
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Signup, Login
from models.request.signup import SignupReq
from repository.signup import SignupRepository
from security.secure import get_current_user

router = APIRouter()

@router.post("/signup/add")
def add_signup(req: SignupReq, sess: Session = Depends(sess_db)):
    """
    没有调用 Depends get_user,所以 Swagger 页面接口后面没有 授权 Icon
    :param req:
    :param sess:
    :return:
    """
    repo: SignupRepository = SignupRepository(sess)
    signup = Signup(password=req.password, username=req.username, id=req.id)
    result = repo.insert_signup(signup)
    if result is True:
        return signup
    else:
        return JSONResponse(content={'message': 'create signup problem encountered'}, status_code=500)

@router.get("/signup/list", response_model=List[SignupReq])
def list_signup(current_user: Login = Depends(get_current_user), sess: Session = Depends(sess_db)):
    """
    通过这里的 Depends get_user 实现调用 框架的 OAuth2 表单,所以 Swagger 页面接口后面有 授权 Icon
    :param current_user:
    :param sess:
    :return:
    """
    repo: SignupRepository = SignupRepository(sess)
    result = repo.get_all_signup()
    return result

 


7.5 创建基于作用域的授权

FastAPI 支持基于作用域的授权(scope-based authentication),它使用 OAuth2 协议的 scopes 参数指定一组用户可以访问哪些端点。 scopes 参数是一种放置在令牌中的权限,用于为用户提供额外的细粒度权限。

1. 项目结构 

## 1. description
应用 自定义 OAuth2 进行身份验证,使用 scopes 参数来指定一组用户可以访问哪些端点


## 2. 使用 自定义 OAuth2 进行身份验证
### 需要安装的包
```bash
pip install python-multipart
```

### 项目结构
```
The Application
├── api/                                # 接口包                  
│   └── __init__.py         
│   └── login.py                        # router,接口定义,运行登录事务,使用 OAuth2PasswordRequestForm 表单提交
│   └── admin.py                        # router,接口定义,运行事务,使用 自定义的 OAuth2 进行身份验证 
├── db_config/                          # 数据库配置                
│   └── __init__.py              
│   └── sqlalchemy_connect.py           # 设置数据库连接  
├── models/                             # 模型层                 
│   |── data
│   │   └── __init__.py         
│   │   └── sqlalchemy_models.py        # 定义表及关联              
│   |── requests
│   │   └── __init__.py         
│   │   └── login.py                    # 定义请求结构
│   │   └── signup.py                   # 定义请求结构
│   │   └── tokens.py                   # 定义请求结构                                                    
│   └── __init__.py                                           
├── repository/                         # 存储库包                 
│   └── __init__.py                   
│   └── login.py                        # 创建连接 CRUD    
│   └── signup.py                       # 创建连接 CRUD   
├── security/                           # 安全层               
│   └── __init__.py                   
│   └── secure.py                       # 自定义 OAuth2,使用 scopes 参数来指定一组用户可以访问哪些端点
│── __init__.py
└── main.py                             # 生成 app,app.include_router(api 的 router)                                 
```

### 运行
1. 启动 main.py

 

2. 身份验证部分

# content of security\secure.py
from datetime import timedelta, datetime
from typing import Optional
from fastapi import HTTPException, status, Security, Depends
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.requests import Request
from fastapi.security import OAuth2, SecurityScopes
from fastapi.security.utils import get_authorization_scheme_param
from jose import jwt, JWTError
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Login
from models.request.tokens import TokenData
from repository.login import LoginRepository

crypt_context = CryptContext(schemes=["sha256_crypt", "md5_crypt"])

# 在 ps console 处,通过 "openssl rand -hex 32" 生成 SECRET_KEY
SECRET_KEY = "882fdc269930e113056ec3f630be0c84214c0d0ed477075f0207369548e513a9"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

class OAuth2PasswordBearerScopes(OAuth2):
    # 自定义 OAuth2 类, 添加两个构造函数参数: token_url, scopes 以执行身份验证流程
    def __init__(self, tokenUrl: str, scheme_name: str = None, scopes: dict = None, auto_error: bool = True):
        if not scopes:
            scopes = {}
        # OAuthFlowsModel 类结构,password 对应的 OAuthFlowPassword-> OAuthFlow,对应的参数是 tokenUrl 和 scopes
        flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes})
        super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)

    async def __call__(self, request: Request) -> Optional[str]:
        header_authorization: str = request.headers.get("Authorization")
        header_scheme, header_param = get_authorization_scheme_param(header_authorization)

        scheme = None
        param = None
        if header_scheme.lower() == "bearer":
            authorization = True
            scheme = header_scheme
            param = header_param
        else:
            authorization = False

        if not authorization or scheme.lower() != "bearer":
            if self.auto_error:
                raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated")
            else:
                return None
        else:
            return param

# 构建权限字典
# 由于没有办法将角色权限直连数据库,所以目前直接存储在该构造函数中
oauth2_scheme = OAuth2PasswordBearerScopes(
    tokenUrl="/ch07/login/token",
    scopes={"admin_read": "admin role that has read only role",
            "admin_write": "admin role that has write only role",
            "user": "valid user of the application",
            "guest": "visitor of the site",
            },
)

def create_access_token(data: dict, expires_delta: timedelta):
    to_encode = data.copy()
    expire = datetime.utcnow() + expires_delta
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


def verify_password(plain_password, hashed_password):
    return crypt_context.verify(plain_password, hashed_password)


def authenticate(username, password, account: Login):
    try:
        password_check = verify_password(password, account.passphrase)
        return password_check
    except Exception as e:
        print(e)
        return False

def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme),
                     sess: Session = Depends(sess_db)):
    if security_scopes.scopes:
        authenticate_value = f'Bearer scope={security_scopes.scope_str}'
    else:
        authenticate_value = "Bearer"

    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": authenticate_value},
    )

    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_scopes = payload.get("scopes", [])
        token_data = TokenData(scopes=token_scopes, username=username)
    except JWTError:
        raise credentials_exception

    loginrepo = LoginRepository(sess)
    user = loginrepo.get_all_login_username(token_data.username)

    if user is None:
        raise credentials_exception

    for scope in security_scopes.scopes:
        if scope not in token_data.scopes:
            raise credentials_exception
return user

def get_current_valid_user(current_user: Login = Security(get_current_user, scopes=["user"])):
    if current_user is None:
        raise HTTPException(status_code=400, detail="Invalid user")
    return current_user

解释

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="ch07/login/token") 这行代码定义了一个基于OAuth2密码流(Resource Owner Password Credentials Grant)的认证方案,使用FastAPI的OAuth2PasswordBearer类。它的主要作用如下:
  认证流程定义:通过指定tokenUrl参数为"ch07/login/token",指明客户端应该向该URL发送用户名和密码来换取访问令牌(Access Token)。这是OAuth2协议中的一种认证方式,适用于信任的客户端应用直接处理用户凭证的情况。
  令牌验证:创建的oauth2_scheme实例可以在需要保护的API端点作为依赖项使用。当客户端携带访问令牌访问这些受保护的路由时,FastAPI会自动使用这个实例验证令牌的有效性。验证包括检查令牌格式、过期时间以及可能的撤销状态等。
  简化令牌处理:通过这种方式,开发者无需手动解析和验证每个请求中的令牌。OAuth2PasswordBearer封装了这些逻辑,使得你可以专注于业务逻辑,同时保持应用的安全性。
  安全性增强:OAuth2协议和Bearer Tokens机制提供了现代Web应用所需的安全标准,帮助防止未经授权的访问,同时也支持令牌的刷新和撤销机制,增强了应用的安全性。
简而言之,这行代码是FastAPI应用中实施OAuth2认证的基础配置之一,用于确保特定端点的访问需通过有效令牌验证,从而保护资源免受未授权访问。

是的,接口 "ch07/login/token" 的作用通常是接收用户的凭证(如用户名和密码),验证这些凭证后,如果用户身份合法,就会生成一个访问令牌(Access Token)。这个访问令牌随后会被返回给客户端。客户端在之后访问那些需要授权的API端点时,
    需要在HTTP请求的Authorization头中携带这个令牌(通过上面 OAuth2PasswordBearerScopes 类的 __call__ 方法即可了解 )。它会验证从"ch07/login/token"接口获取并被客户端在后续请求中使用的令牌是否有效,从而决定是否允许该请求继续执行。
所以,从"ch07/login/token"接口获得的令牌确实是直接用于与oauth2_scheme交互,以实现安全认证的。

 

3. 登录事务

# content of api\login.py
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from repository.login import LoginRepository
from security.secure import authenticate, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES

router = APIRouter()

@router.post("/login/token")
def login(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(sess_db)):
    username = form_data.username
    password = form_data.password
    loginrepo = LoginRepository(sess)
    account = loginrepo.get_all_login_username(username)
    if authenticate(username, password, account):
        access_token = create_access_token(data={"sub": username, "scopes": form_data.scopes}, expires_delta=timedelta(ACCESS_TOKEN_EXPIRE_MINUTES))
        return {"access_token": access_token, "token_type": "bearer"}
    else:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

 

3. 将作用域应用于端点

# content of api\admin.py
from typing import List
from fastapi import APIRouter, Depends, Security
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Signup, Login
from models.request.signup import SignupReq
from repository.signup import SignupRepository
from security.secure import get_current_valid_user

router = APIRouter()

@router.post("/signup/add")
def add_signup(req: SignupReq, sess: Session = Depends(sess_db)):
    repo: SignupRepository = SignupRepository(sess)
    signup = Signup(id=req.id, username=req.username, password=req.password)
    result = repo.insert_signup(signup)
    if result is True:
        return signup
    else:
        return JSONResponse(content={"message": "create signup problem encountered"}, status_code=500)

@router.get("/signup/list", response_model=List[SignupReq])
# 注入 get_current_valid_user,替换了 Depends 类,且为每个 API 服务分配作用域(通过 scopes 属性,定义了限制用户访问的有效作用域参数列表)
def list_signup(current_user: Login = Security(get_current_valid_user, scopes=["admin_read"]), sess: Session = Depends(sess_db)):
    repo: SignupRepository = SignupRepository(sess)
    result = repo.get_all_signup()
    return result

@router.patch("/signup/update")
def update_signup(id: int, req: SignupReq, current_user: Login = Security(get_current_valid_user, scopes=["admin_write"]), sess: Session = Depends(sess_db) ):
    signup_dict = req.dict(exclude_unset=True)
    repo:SignupRepository = SignupRepository(sess)
    result = repo.update_signup(id, signup_dict )
    if result:
        return JSONResponse(content={'message':'profile updated successfully'}, status_code=201)
    else:
        return JSONResponse(content={'message':'update profile error'}, status_code=500)

 

#解释

Security 装饰器在 FastAPI 框架中主要用于实现端点的安全访问控制。它的核心作用是确保在执行某个路径操作(如API endpoint)之前,满足特定的安全条件。这包括但不限于验证用户身份、检查权限范围或者执行任何自定义的安全验证逻辑。
     具体来说,它的工作机制涉及以下几个关键点:   身份验证:它可以集成诸如JWT、OAuth2等认证机制,以确认请求发送者的身份。例如,通过检查请求头中的Token并验证其有效性。   授权:允许你指定访问特定资源所需的权限或“scopes”。如代码示例中的scopes
=["admin_read"],意味着只有具备admin_read权限的用户才能访问该路由。   依赖注入:通过依赖注入(如Depends),Security装饰器可以传递经过验证的用户信息或其他安全上下文给路径操作函数,使得在处理请求时可以直接使用这些信息。   自定义逻辑:开发者可以定义自己的安全函数(如get_current_valid_user),在这个函数里实现复杂的验证逻辑,比如检查用户状态、角色或是执行额外的安全检查。   错误处理:如果安全验证失败,Security装饰器通常会自动处理错误,返回合适的HTTP错误码和错误信息,比如未授权访问(401 Unauthorized)或禁止访问(403 Forbidden)。 总之,Security装饰器为FastAPI应用提供了一种灵活且强大的方式来保障API接口的安全性,确保只有合法和授权的用户可以访问敏感资源。

 

4. 效果

1)swagger 界面,/signup/list 和 /signup/update 两个接口有身份验证

 

2)以 /signup/list 接口为例

点击接口后面的锁 icon,输入 用户名及密码,及勾选响应的 scopes,这将调用 接口 /login/token,

  • 用户名及密码:在 /login/token 接口中引用 security/secure.py 的 authenticate 进行校验成功后,将调用 security/secure.py 的 create_access_token,并返回 jwt 信息
  • 通过该 API 的 Security() 的 scopes 及其依赖的 get_current_valid_user 的 scopes,其作用域为 "admin_read" 和 "user",所以在下图的 scopes 中出现了这两个 scope

 

另外,最上面的 Authorize,其 scope 为 security\secure.py 的 oauth2_scheme 定义的 scopes 的4个 scope

   

 

所以流程是:

1). 初始化:每个接口及接口后面的验证 icon(形成表单,包含 username, password, 该接口对应的 scopes 等信息),右上角的验证 icon(表单,包含username, password,所有接口的 scopes 信息等)

2)点击接口的验证 icon,-> 经过 secure.py 的 authenticate() 验证 username, password, 返回 {"access_token": access_token, "jwt_token_type": "bearer"} 给 OAuth2PasswordBearerScopes 类;

3)接口对应的 execute() 时,-> 依赖 get_valid_user -> 依赖 get_current_user -> 依赖 oauth2_scheme (OAuth2PasswordBearerScopes 类的实例),运行该类的 __call__(),解析 request.header,并最终获取到

      "Bearer token_value_xxxx" 值,而 OAuth2PasswordBearer 机制可以妥善处理该情况,所以知道处理该信息的后半部分,所以 get_current_user() 的 token 的值其实就是后半部分 "token_value_xxxx"

4)再在  get_current_user() 的后面对 token 进行解码验证,无异常后,调用 loginrepo 对象的 get_all_login_username 方法

 


7.8 使用内置中间件进行身份验证

1. 项目结构

## 1. description
使用内置中间件进行身份验证
对应: section 7.9 (07j)

功能:使用 AuthenticationMiddleware 等 Starlette 中间件来实现自定义身份验证

## 2. 使用 自定义 AuthenticationBackend 进行身份验证
### 项目结构
```
The Application
├── api/                                # 接口包                  
│   └── __init__.py         
│   └── login.py                        # router,接口定义,运行事务
│   └── admin.py                        # router,接口定义,运行事务,使用 AuthenticationMiddleware OAuth2 进行身份验证 
├── db_config/                          # 数据库配置                
│   └── __init__.py              
│   └── sqlalchemy_connect.py           # 设置数据库连接  
├── models/                             # 模型层                 
│   |── data
│   │   └── __init__.py         
│   │   └── sqlalchemy_models.py        # 定义表及关联              
│   |── requests
│   │   └── __init__.py         
│   │   └── login.py                    # 定义请求结构
│   │   └── signup.py                   # 定义请求结构                                                 
│   └── __init__.py                                           
├── repository/                         # 存储库包                 
│   └── __init__.py                   
│   └── login.py                        # 创建连接 CRUD    
│   └── signup.py                       # 创建连接 CRUD   
├── security/                           # 安全层               
│   └── __init__.py                   
│   └── secure.py                       # 自定义 AuthenticationBackend
│── __init__.py
└── main.py                             # 生成 app,app.include_router(api 的 router)                                 
```

### 运行
1. 启动 main.py

 

2. 身份验证部分

# contents of security\secure.py
from fastapi import Depends
from fastapi.security import HTTPBasic
from passlib.context import CryptContext
from starlette.authentication import AuthenticationBackend, AuthCredentials, BaseUser, AuthenticationError, SimpleUser

from db_config.sqlalchemy_connect import sess_db
from models.data.sqlalchemy_models import Login


crypt_context = CryptContext(schemes=["sha256_crypt", "md5_crypt"])

# 实例化 HTTPBasic 类并将其注入每个 API 服务以保护端点访问
#   http_basic 一旦注入 API 服务,就会使浏览器弹出一个登录表单,输入 username 和 password
http_basic = HTTPBasic()


def verify_password(plain_password, hashed_password):
    return crypt_context.verify(plain_password, hashed_password)


def authenticate(username, password, account: Login):
    try:
        return verify_password(password, account.passphrase)
    except Exception as e:
        print(e)
        return False


class UsernameAuthBackend(AuthenticationBackend):
    def __init__(self, username, sess=Depends(sess_db)):
        self.username = username

    async def authenticate(self, request) -> tuple[AuthCredentials, BaseUser] | None:
        """
        重写 authenticate(),检查 Authorization 凭据是否为 Bearer 类,并验证 username 令牌是否等效于中间件提供的固定用户名凭据
        :param request:
        :return:
        """
        if "Authorization" not in request.headers:
            return

        auth = request.headers["Authorization"]

        try:
            scheme, username = auth.split()
            if scheme.lower().strip() != "basic".strip():
                return
        except Exception as e:
            raise AuthenticationError(f"Invalid basic auth credentials, {e}")

        if not username == self.username:
            return
        # 这里的 "authenticated" 是定义的 scopes
        return AuthCredentials(["authenticated"]), SimpleUser(username)

 

3. 接口

# content of api\login.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import HTTPBasicCredentials
from sqlalchemy.orm import Session

from db_config.sqlalchemy_connect import sess_db
from repository.login import LoginRepository
from security.secure import authenticate, http_basic

router = APIRouter()

@router.get("/login")
def login(credentials: HTTPBasicCredentials = Depends(http_basic), sess: Session = Depends(sess_db)):
    loginrepo = LoginRepository(sess)
    account = loginrepo.get_all_login_username(credentials.username)
    if authenticate(credentials.username, credentials.password, account) and account is not None:
        return account
    else:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

 

# content of api\admin.py
from typing import List

from fastapi import APIRouter, Depends
from fastapi.requests import Request
from sqlalchemy.orm import Session
from starlette.authentication import requires

from db_config.sqlalchemy_connect import sess_db
from models.request.signup import SignupReq
from repository.signup import SignupRepository

router = APIRouter()


@router.get("/signup/list", response_model=List[SignupReq])
# 装饰器确保只有经过身份验证的用户才能访问这个路由。如果用户未通过身份验证,则会被阻止访问此路由。这里的 "authenticated" 是参数 scopes
@requires(scopes="authenticated")
def list_signup(request: Request, sess: Session = Depends(sess_db)):
    repo: SignupRepository = SignupRepository(sess)
    return repo.get_all_signup()

 

posted on 2024-06-22 13:54  bruce_he  阅读(0)  评论(0编辑  收藏  举报