zoukankan      html  css  js  c++  java
  • FastAPI 安全机制(四) OAuth2 scopes

    作者:麦克煎蛋   出处:https://www.cnblogs.com/mazhiyong/ 转载请保留这段声明,谢谢!

    OAuth2 scopes是一种细粒度的安全许可机制,通常用来对用户或者第三方应用提供特定的访问许可。

    在OAuth2的规范中,scopes是一个基于空格分隔符的字符串列表。这些scopes代表着"许可"。

    每一个scope项是一个不带空格的字符串,通常用来表示特定的安全许可,例如:

    • users:read 或者 users:write:这是通常的使用场景
    • instagram_basic: Facebook / Instagram的使用场景
    • https://www.googleapis.com/auth/drive : Google使用场景

    scope的具体内容根据业务需求而定,对OAuth2来说只是字符串。

    我们可以在FastAPI中直接使用无缝集成的OAuth2 scopes。

    一、通过token返回scopes信息

    1、后台获取权限

    通常情况下,用户登陆成功以后,我们可以获取到用户真实的权限,并通过token返回scopes信息。

    示例中的scopes信息仅是演示,使用时应该根据用户的实际系统权限来赋值。

    @app.post("/login", response_model=Token)
    async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
        # 首先校验用户信息
        user = authenticate_user(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"},
            )
    
        # 生成并返回token信息
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={"sub": user.username, "scopes": "me"}, 
            expires_delta=access_token_expires
        )
    
        return {"access_token": access_token, "token_type": "bearer"}

    2、用户登陆时选择

    用户也可以在登陆时选择scopes信息,这也是Google等登陆时所用的机制。

    我们需要在OAuth2PasswordBearer中添加scopes信息。

    oauth2_scheme = OAuth2PasswordBearer(
        tokenUrl="token",
        scopes={"me": "Read information about the current user.", "items": "Read items."},
    )

    同样我们也是通过token返回scopes信息,只不过scopes信息的来源是用户端。

    @app.post("/login2", response_model=Token)
    async def login_for_access_token2(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
        # 首先校验用户信息
        user = authenticate_user(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"},
            )
    
        # 生成并返回token信息
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={"sub": user.username, "scopes": form_data.scopes},
            expires_delta=access_token_expires
        )
    
        return {"access_token": access_token, "token_type": "bearer"}

    我们可以在交互式文档中查看显示效果:

    二、scopes权限校验

    当有请求访问时,需要从token中解析出有效信息,不仅需要完成用户身份校验,还需要完成基于scopes的权限校验。

    1、scopes数据传递

    首先需要在路径操作中添加对scopes的依赖项:

    @app.get("/users/me/", response_model=User)
    async def read_users_me(
        current_user: User = Security(get_current_user, scopes=["me"])
    ):
        return current_user

    Security实际上是Depends的子类,只不过多了一个参数,可以接收scopes的列表信息。

    通过使用Security而不是Depends,FastAPI将会知道它会声明并内部使用scopes信息,并且在交互式文档中显示这些信息。

    2、scopes数据解析

    SecurityScopes的属性scopes,是一个包含所有它需要的scopes以及所有依赖项(把它作为子依赖项)的列表。

    SecurityScopes的属性scope_str,是包含所有scopes的一个字符串(以空格分隔)。

    class SecurityScopes:
        def __init__(self, scopes: List[str] = None):
            self.scopes = scopes or []
            self.scope_str = " ".join(self.scopes)
    async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
        if security_scopes.scopes:
            authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
        else:
            authenticate_value = f"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")
            print(username)
    
            if username is None:
                raise credentials_exception
    
            # 读取scopes信息
            token_scopes = payload.get("scopes", [])
            token_data = TokenData(scopes=token_scopes, username=username)
    except (PyJWTError, ValidationError):
            raise credentials_exception
    
        # 用户身份校验
        user = DBUser.get_by_username(db, token_data.username)
        if user is None:
            raise credentials_exception
    
        # 基于scope的权限校验
        for scope in security_scopes.scopes:
            if scope not in token_data.scopes:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Not enough permissions",
                    headers={"WWW-Authenticate": authenticate_value},
                )
    
        return user

    3、依赖项树和scopes

    修改部分代码逻辑如下:

    async def get_current_active_user(
        current_user: User = Security(get_current_user, scopes=["me"])
    ):
        if current_user.disabled:
            raise HTTPException(status_code=400, detail="Inactive user")
        return current_user
    
    
    @app.get("/users/me/", response_model=User)
    async def read_users_me(
        current_user: User = Security(get_current_active_user, scopes=["me"])
    ):
        return current_user
    
    
    @app.get("/items/")
    async def read_items(token: str = Depends(oauth2_scheme)):
        return {"token": token}
    
    
    @app.get("/users/me/items/")
    async def read_own_items(
        current_user: User = Security(get_current_active_user, scopes=["items"])
    ):
        return [{"item_id": "Foo", "owner": current_user.username}]
    
    
    @app.get("/status/")
    async def read_system_status(current_user: User = Depends(get_current_user)):
        return {"status": "ok"}

    关于依赖项和scopes的层次提下如下:

    路径操作read_own_items有:

      * 依赖项需要的scopes: ["items"]

      * 依赖项函数 get_current_active_user:

        * 依赖项需要的scopes: ["me"]

        * 依赖项函数 get_current_user:

            * 自身不需要scopes

            * 依赖项 oauth2_scheme

            * SecurityScopes类型的参数security_scopes

              * 参数security_scopes的属性scopes包含所有以上声明的scopes的一个列表,因此

                * 对于路径操作 read_own_items来说,security_scopes.scopes 包含 ["me", "items"]

                * 对于路径操作 read_users_me来说,security_scopes.scopes 包含 ["me"]

                * 对于路径操作 read_system_status来说,security_scopes.scopes 的内容为[]

               

      

  • 相关阅读:
    Ogre的骨骼动画
    ID卡读取方法(用于区分ID卡读取出来的数据和一般人手录入的数据)
    FastSpring学习笔记一
    数学 方程的解
    单调栈+桶+分治 奇袭
    神奇DP [HNOI2004] 打砖块
    DFS 找硬币
    树DP 树上染色
    android 适配器Adpter的使用总结 之 BaseExpandableListAdapter
    Java删除文件夹以及文件夹下的子目录与文件
  • 原文地址:https://www.cnblogs.com/mazhiyong/p/13328363.html
Copyright © 2011-2022 走看看