Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/module_identity/controller/oauth_session_controller.py
T
insistence a89aec54aa feat: 新增 OAuth 2.0 / OIDC 统一认证中心 (#129)
* feat: 新增统一认证中心

* perf: 优化代码

* fix: 修复问题

* perf: 优化页面

* fix: 修复部分接口缺失Request参数的问题

* perf: 优化访问控制机制

* perf: 优化代码

* fix: 修复 Logout Token 缺少 exp

* perf&fix: 优化和修复

* perf: 优化代码

* perf: 优化代码

* fix: 修复测试问题
2026-10-01 22:56:05 +08:00

236 lines
9.7 KiB
Python

from typing import Annotated
from fastapi import Path, Query, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from common.annotation.log_annotation import Log
from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset
from common.aspect.db_session import DBSessionDependency
from common.aspect.interface_auth import UserInterfaceAuthDependency
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
from common.constant import ApiNamespace
from common.enums import BusinessType
from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from exceptions.exception import ServiceException
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_identity.entity.vo.oauth_session_vo import (
AccessPolicyModel,
AccessPolicyPageQueryModel,
GrantAccessModel,
GrantModel,
GrantPageQueryModel,
SessionPageQueryModel,
SessionRevokeModel,
SsoSessionModel,
)
from module_identity.service.oauth_session_management_service import OAuthSessionManagementService
from utils.oidc_util import OidcUtil
from utils.response_util import ResponseUtil
oauth_session_controller = APIRouterPro(
prefix='/system/oauth/session', order_num=22, tags=['系统管理-OAuth Session'], dependencies=[PreAuthDependency()]
)
oauth_grant_controller = APIRouterPro(
prefix='/system/oauth/grant', order_num=23, tags=['系统管理-OAuth Grant'], dependencies=[PreAuthDependency()]
)
_MAX_BATCH_SIZE = 100
def _actor(user: CurrentUserModel) -> str:
"""
提取管理操作者标识
:param user: 当前登录用户
:return: 安全截断后的用户名
:raises ServiceException: 当前用户不可用
"""
try:
return OidcUtil.actor_name(
getattr(getattr(user, 'user', None), 'user_name', None), error_message='当前操作者不可用'
)
except ValueError as exc:
raise ServiceException(message=str(exc)) from exc
@oauth_session_controller.get(
'/list',
summary='获取 OAuth 会话分页列表接口',
description='用于获取 OAuth 会话分页列表',
response_model=PageResponseModel[SsoSessionModel],
dependencies=[UserInterfaceAuthDependency('system:oauthSession:list')],
)
async def list_oauth_sessions(
query: Annotated[SessionPageQueryModel, Query()], query_db: Annotated[AsyncSession, DBSessionDependency()]
) -> Response:
rows, total = await OAuthSessionManagementService.list_sessions(query_db, query)
return ResponseUtil.success(rows=rows, dict_content={'total': total})
@oauth_session_controller.delete(
'/user/{user_id}',
summary='撤销用户 OAuth 会话接口',
description='用于撤销指定用户的 OAuth 会话',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('system:oauthSession:revoke')],
)
@ApiRateLimit(
namespace=ApiNamespace.SYSTEM_OAUTH_SESSION_USER_REVOKE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION
)
@Log(title='OAuth 会话管理', business_type=BusinessType.DELETE)
async def revoke_user_oauth_sessions(
request: Request,
user_id: Annotated[int, Path(gt=0)],
payload: SessionRevokeModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
count = await OAuthSessionManagementService.revoke_user(
query_db, request.app.state.redis, user_id, _actor(current_user), payload.reason
)
return ResponseUtil.success(msg='单点登录会话已撤销', data={'count': count})
@oauth_session_controller.get(
'/{sid}',
summary='查询 OAuth 会话详情接口',
description='用于查询 OAuth 会话详情',
response_model=DataResponseModel[SsoSessionModel],
dependencies=[UserInterfaceAuthDependency('system:oauthSession:list')],
)
async def get_oauth_session(
sid: Annotated[str, Path(min_length=1, max_length=64)], query_db: Annotated[AsyncSession, DBSessionDependency()]
) -> Response:
data = await OAuthSessionManagementService.get_session(query_db, sid)
if data is None:
raise ServiceException(message='会话不存在')
return ResponseUtil.success(data=data)
@oauth_session_controller.delete(
'/{sids}',
summary='批量撤销 OAuth 会话接口',
description='用于批量撤销 OAuth 会话',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('system:oauthSession:revoke')],
)
@ApiRateLimit(namespace=ApiNamespace.SYSTEM_OAUTH_SESSION_REVOKE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION)
@Log(title='OAuth 会话管理', business_type=BusinessType.DELETE)
async def revoke_oauth_sessions(
request: Request,
sids: Annotated[str, Path(min_length=1, max_length=6500)],
payload: SessionRevokeModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
try:
batch_ids = OidcUtil.split_batch(sids, 'sids', max_size=_MAX_BATCH_SIZE, validate_all_first=True)
except ValueError as exc:
raise ServiceException(message=str(exc)) from exc
count = await OAuthSessionManagementService.revoke_sessions(
query_db, request.app.state.redis, batch_ids, _actor(current_user), payload.reason
)
return ResponseUtil.success(msg='单点登录会话已撤销', data={'count': count})
@oauth_grant_controller.get(
'/list',
summary='获取 OAuth 授权分页列表接口',
description='用于获取 OAuth 授权分页列表',
response_model=PageResponseModel[GrantModel],
dependencies=[UserInterfaceAuthDependency('system:oauthGrant:list')],
)
async def list_oauth_grants(
query: Annotated[GrantPageQueryModel, Query()], query_db: Annotated[AsyncSession, DBSessionDependency()]
) -> Response:
rows, total = await OAuthSessionManagementService.list_grants(query_db, query)
return ResponseUtil.success(rows=rows, dict_content={'total': total})
@oauth_grant_controller.get(
'/access/list',
summary='获取用户应用访问策略分页列表接口',
description='用于查询独立访问策略,包含尚未授权过的用户与应用',
response_model=PageResponseModel[AccessPolicyModel],
dependencies=[UserInterfaceAuthDependency('system:oauthGrant:list')],
)
async def list_oauth_access_policies(
query: Annotated[AccessPolicyPageQueryModel, Query()], query_db: Annotated[AsyncSession, DBSessionDependency()]
) -> Response:
rows, total = await OAuthSessionManagementService.list_access_policies(query_db, query)
return ResponseUtil.success(rows=rows, dict_content={'total': total})
@oauth_grant_controller.get(
'/{grant_id}',
summary='查询 OAuth 授权详情接口',
description='用于查询 OAuth 授权详情',
response_model=DataResponseModel[GrantModel],
dependencies=[UserInterfaceAuthDependency('system:oauthGrant:list')],
)
async def get_oauth_grant(
grant_id: Annotated[str, Path(min_length=1, max_length=64)],
query_db: Annotated[AsyncSession, DBSessionDependency()],
) -> Response:
data = await OAuthSessionManagementService.get_grant(query_db, grant_id)
if data is None:
raise ServiceException(message='授权记录不存在')
return ResponseUtil.success(data=data)
@oauth_grant_controller.delete(
'/{grant_ids}',
summary='批量撤销 OAuth 授权接口',
description='撤销选中记录所属用户对应用的全部现有授权,重新同意后可再次访问',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('system:oauthGrant:revoke')],
)
@ApiRateLimit(namespace=ApiNamespace.SYSTEM_OAUTH_GRANT_REVOKE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION)
@Log(title='OAuth 授权管理', business_type=BusinessType.DELETE)
async def revoke_oauth_grants(
request: Request,
grant_ids: Annotated[str, Path(min_length=1, max_length=6500)],
payload: SessionRevokeModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
try:
batch_ids = OidcUtil.split_batch(grant_ids, 'grant_ids', max_size=_MAX_BATCH_SIZE, validate_all_first=True)
except ValueError as exc:
raise ServiceException(message=str(exc)) from exc
count = await OAuthSessionManagementService.revoke_grants(query_db, batch_ids, _actor(current_user), payload.reason)
return ResponseUtil.success(msg='OAuth 授权已撤销', data={'count': count})
@oauth_grant_controller.put(
'/user/{user_id}/client/{client_id}/access',
summary='更新用户应用访问策略接口',
description='用于禁止用户访问应用或解除禁止,解除后仍需重新授权',
response_model=ResponseBaseModel,
dependencies=[UserInterfaceAuthDependency('system:oauthGrant:revoke')],
)
@ApiRateLimit(namespace=ApiNamespace.SYSTEM_OAUTH_GRANT_ACCESS, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION)
@Log(title='OAuth 授权管理', business_type=BusinessType.UPDATE)
async def set_oauth_client_access(
request: Request,
user_id: Annotated[int, Path(gt=0)],
client_id: Annotated[str, Path(min_length=1, max_length=128)],
payload: GrantAccessModel,
query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
) -> Response:
count = await OAuthSessionManagementService.set_access(
query_db, user_id, client_id, payload.blocked, _actor(current_user), payload.reason
)
return ResponseUtil.success(
msg='已禁止用户访问该应用' if payload.blocked else '已解除禁止,请重新授权',
data={'count': count, 'accessStatus': 'blocked' if payload.blocked else 'allowed'},
)