Fastapi框架-冷饭再炒-基础知识补充篇(7)- 授权认证机制中作用域分配实操步骤

1,848 阅读8分钟

1:授权作用域练习篇:

之前我们再说安全机制的时候,说过官方提供的安全认证机制的是使用用户和密码的机制,但是当时没有涉及到关于授权方案中涉及的授权作用域的问题。今天特定的来补习一下功课:

第1步:确定授权方案信息

首先关于授权方案的确定,我们需要考虑的几个问题点:

  • 1:指定授权认证的签发token的地址
  • 2:这个认证方案里面有哪些可以选择的授权域
  • 2:再者就是就是一个token可以被授予哪些权限,哪些接口需要哪些权限。

如下授权防范的对象的定义:

#定义认证方案
oauth2_scheme = OAuth2PasswordBearer(
    # 配置我们的授权方案,发起授权的请求的是进行授权处理的接口地址-如在操作文档输入用户名和密码等之后,会自动调用此尽快进行授权码的签发和授权区域的写入
    tokenUrl="/api/v1/token",
    # 定义我们的操作文档显示授权码授权区域----这个和下面的授权的区域关联起来,表示某个接口需要的授权域
    scopes={
        "get_admin_info": "获取管理员用户信息",
        "del_admin_info": "删除管理员用户信息",
        "get_user_info": "获取用户信息",
        "get_user_role": "获取用户所属角色信息",
        "get_user_permission": "获取用户相关的权限信息",

    },
)

第2步:配置相关Token请求接口

需要注意是事项点:

  • 需要表单的方式进行提交,因为我们的操作文档是使用表单的方式
  • 记得安装我们的表单提交需要的依赖库
class Token(BaseModel):
    access_token: str
    token_type: str

@app.post("/api/v1/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    print('当前的作用去scopes:', form_data.scopes)
    # ['items', 'me']
    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"}

当我们的再操作文档操作,点击相关的锁按你的时候,

image.png

点击锁按钮进行授权处理: image.png

我们的提交的信息都会自动的跑到我们上面的接口进行用户信息的认证处理。

PS:注意事项点:

这一步的我们需要注意的事项点就是我们本节主要的对象,作用域:

image.png

这个地方需要勾选,我们当前登入的用户,允许分配哪些授权标记给当前的准备签发的TOKEN信息。

第3步:进行用户信息认证

关于用户信息的认证流程其实主要是对用户身份的认证,主要流程有:

  • 1 用户名验证(从数据库里面对比用户名是否存在)
  • 2 用户密码验证(对比密码信息,主要是加盐密码的比较)
  • 3 如果不存在相关用户信息则抛出异常

主要的代码段为:


# 进行用户认证用户的认证
def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


# 从上面定义的字典表里查询用户信息,并返回用户信息实体
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 验证密码信息
def verify_password(plain_password, hashed_password):
    # 为了方便,这里就不对密码进行校验了!直接通过!!!只是为了方便!
    # return pwd_context.verify(plain_password, hashed_password)
    return True

第4步:进行token创建

这个地方主要留意的就是我们的前端勾选的scopes,这里会通过form_data: OAuth2PasswordRequestForm = Depends()里面的form_data.scopes来获取。

创建token的代码段:

access_token = create_access_token(
        data={"sub": user.username, "scopes": form_data.scopes},
        expires_delta=access_token_expires,
    )
    
 # 创建TOKEN给用户签发的TOKEN
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    '''
    签发token
    :param data: data里面包含用户信息和签发授权的作用域信息
    :param expires_delta:
    :return:
    '''
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

创建完成后返回给我们的前端:

    return {"access_token": access_token, "token_type": "bearer"}

示例如:

image.png

第5步:开始进行验证权限:

所有的接口:

image.png

对我们相关的需要授权的接口进行配置:

image.png


@app.get("/status/")
async def read_system_status(current_user: User = Depends(get_current_user)):
    return {"status": "ok"}


@app.get("/api/v1/get_admin_info", response_model=User)
async def get_admin_info(current_user: User = Security(get_current_active_user, scopes=["get_admin_info"])):
    return current_user


@app.get("/api/v1/del_admin_info", response_model=User)
async def del_admin_info(current_user: User = Security(get_current_active_user, scopes=["del_admin_info"])):
    return current_user

@app.get("/api/v1/get_user_info", response_model=User)
async def get_user_info(current_user: User = Security(get_current_active_user, scopes=["get_user_info"])):
    return current_user

@app.get("/api/v1/get_user_role", response_model=User)
async def get_user_role(current_user: User = Security(get_current_active_user, scopes=["get_user_role"])):
    return current_user

@app.get("/api/v1/get_user_permission", response_model=User)
async def get_user_role(current_user: User = Security(get_current_active_user, scopes=["get_user_permission"])):
    return current_user


@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}]

上面我们的只给我们的xiaozhongtongxue 分配了两个接口的访问的权限:

所以此时我们的进行接口访问的时候样子会是这样:

1)先授权并分配权限作用域

image.png

输入正确的用户名和密码验证通过后,则我们的接口会返回对应的token给前端,前端 会自己去获取并写入相关的cookie里面方便,其他需要验证的接口携带token上来,这个过程是自动处理好的。

2)访问已授权的接口地址

image.png

上面的接口响应正常返回。

3)访问没有授权的接口地址

image.png

提示没有权限。

总结:完整的示例代码:

#!/usr/bin/evn python
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
   文件名称 :     quanxianyu
   文件功能描述 :   功能描述
   创建人 :       小钟同学
   创建时间 :          2021/6/10
-------------------------------------------------
   修改描述-2021/6/10:         
-------------------------------------------------
"""
from datetime import datetime, timedelta
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import (OAuth2PasswordBearer, OAuth2PasswordRequestForm, SecurityScopes)
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, ValidationError

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 加密方案
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 用户信息表
fake_users_db = {
    "xiaozhongtongxue": {
        "username": "xiaozhongtongxue",
        "full_name": "xiaozhongtongxue",
        "email": "xxxxxxxxx@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False
    }
}


class TokenData(BaseModel):
    username: Optional[str] = None
    scopes: List[str] = []


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


class UserInDB(User):
    hashed_password: str


# 定义认证方案
oauth2_scheme = OAuth2PasswordBearer(
    # 配置我们的授权方案,发起授权的请求的是进行授权处理的接口地址-如在操作文档输入用户名和密码等之后,会自动调用此尽快进行授权码的签发和授权区域的写入
    tokenUrl="token",
    # 定义我们的操作文档显示授权码授权区域----这个和下面的授权的区域关联起来,表示某个接口需要的授权域
    scopes={
        "get_admin_info": "获取管理员用户信息",
        "del_admin_info": "删除管理员用户信息",
        "get_user_info": "获取用户信息",
        "get_user_role": "获取用户所属角色信息",
        "get_user_permission": "获取用户相关的权限信息",

    },
)


# 定义我们的APP服务对象
app = FastAPI()



# 获取加盐密码
def get_password_hash(password):
    return pwd_context.hash(password)




# 进行用户认证用户的认证
def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


# 从上面定义的字典表里查询用户信息,并返回用户信息实体
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 验证密码信息
def verify_password(plain_password, hashed_password):
    # 为了方便,这里就不对密码进行校验了!直接通过!!!只是为了方便!
    # return pwd_context.verify(plain_password, hashed_password)
    return True


# 创建我们的授权之后,给用户签发的TOKEN
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    '''
    签发token
    :param data: data里面包含用户信息和签发授权的作用域信息
    :param expires_delta:
    :return:
    '''
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
    print("当前认证方案里面的作用域:", security_scopes.scope_str)
    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},
    )
    print("当前携带上来的token值:", token)
    try:
        # 开始反向解析我们的TOKEN.,解析相关的信息
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception

        print("当前用户名称:", username)
        token_scopes = payload.get("scopes", [])
        #
        print("当前用户所属的toekn信息里面包含的scopes信息有:", token_scopes)
        token_data = TokenData(scopes=token_scopes, username=username)
        print("token_data", token_data)
    except (JWTError, ValidationError):
        raise credentials_exception

    # 再一次从数据库里面验证用户信息
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception

    # 然后
    print("当前认证方案里面所有security_scopes信息有:", security_scopes.scopes)
    for scope in security_scopes.scopes:
        # 对比用户的token锁携带的用户的作用区域授权信息
        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


# 注意接口的也可以定义相关的权限的依赖!!!!或者组合的,比如我这里要是定义其他的话,依赖这个的接口,如果没有这个权限也无法访问!
async def get_current_active_user(current_user: User = Security(get_current_user, scopes=["get_admin_info"])):
    print("输出当前用户:", current_user)
    # 判断用书是否已经被禁用了!!!如果没有则继续执行
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


class Token(BaseModel):
    access_token: str
    token_type: str

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    print('当前写入的作用去scopes:', form_data.scopes)

    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"}

@app.get("/status/")
async def read_system_status():
    return {"status": "ok"}


@app.get("/api/v1/get_admin_info", response_model=User)
async def get_admin_info(current_user: User = Security(get_current_active_user, scopes=["get_admin_info"])):
    return current_user


@app.get("/api/v1/del_admin_info", response_model=User)
async def del_admin_info(current_user: User = Security(get_current_active_user, scopes=["del_admin_info"])):
    return current_user

@app.get("/api/v1/get_user_info", response_model=User)
async def get_user_info(current_user: User = Security(get_current_active_user, scopes=["get_user_info"])):
    return current_user

@app.get("/api/v1/get_user_role", response_model=User)
async def get_user_role(current_user: User = Security(get_current_active_user, scopes=["get_user_role"])):
    return current_user

@app.get("/api/v1/get_user_permission", response_model=User)
async def get_user_role(current_user: User = Security(get_current_active_user, scopes=["get_user_permission"])):
    return current_user






import uvicorn

if __name__ == '__main__':
    # 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:uvicorn xxx:app --reload
    uvicorn.run('quanxianyu:app', host="127.0.0.1", port=8000, debug=True, reload=True)

OAuth2PasswordBearer :

  • 指定toekn签发访问的地址
  • 定义全局的作用域信息

OAuth2PasswordRequestForm:

  • 接收客户端发送的表单里面的用户和密码

  • 如果请求相关的需要鉴权的接口,FastAPI会检查请求的Authorization头信息,如果没有Authorization头信息,或者头信息的内容不是Bearer token,它会返回401状态码(UNAUTHORIZED)。

Security:

  • 对toekn解析和权限安全的校验处理

2:思考扩展

如果我们的使用上面这种方案来处理我们的TOKen的话,需要注意的点:

上面的认证的方面的仅仅对应的地址仅仅和是我们的操作文档上的授权处理,如果你想定制的自己的响应报文和对应的获取token,需要重新定义的新的获取token接口,并且自己的进行相关的token的值的获取!

结尾

简单小笔记!仅供参考!

END

简书:www.jianshu.com/u/d6960089b…

掘金:juejin.cn/user/296393…

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【原创】【欢迎一起学习交流】| QQ:308711822