Files
dromara--domain-admin/domain_admin/service/oidc_service.py
T
2026-07-13 12:31:34 +08:00

177 lines
5.2 KiB
Python

# -*- coding: utf-8 -*-
"""
oidc_service.py
OpenID Connect 单点登录服务
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from authlib.integrations.flask_client import OAuth
from flask import url_for
from domain_admin.log import logger
from domain_admin.model.user_model import UserModel
from domain_admin.service import token_service
from domain_admin.utils.flask_ext.app_exception import AppException
# OAuth 实例
oauth = OAuth()
def init_oidc(app):
"""
初始化 OIDC OAuth 客户端
:param app: Flask 应用实例
"""
try:
if not is_oidc_enabled():
logger.info("OIDC 单点登录未启用,跳过初始化")
return
# 初始化 OAuth
oauth.init_app(app)
# 获取配置
config = get_oidc_config()
client_id = config.get('client_id')
client_secret = config.get('client_secret')
issuer_url = config.get('issuer_url')
scopes = config.get('scopes', 'openid profile email')
if not all([client_id, client_secret, issuer_url]):
logger.warning("OIDC 配置不完整,跳过初始化")
return
# 注册 OIDC 客户端(authlib 会自动存储,可通过 oauth.oidc 访问)
oauth.register(
name='oidc',
client_id=client_id,
client_secret=client_secret,
server_metadata_url=f"{issuer_url}/.well-known/openid-configuration",
client_kwargs={
'scope': scopes
}
)
logger.info("OIDC OAuth 客户端初始化成功")
except Exception as e:
logger.error(f"OIDC 初始化失败: {str(e)}")
# 优雅降级 - 不让应用崩溃
def is_oidc_enabled():
"""
检查 OIDC 是否启用
:return: bool
"""
from domain_admin.config import OIDC_ENABLED
return OIDC_ENABLED
def get_oidc_config():
"""
Get OIDC configuration from environment variables
:return: dict with keys: issuer_url, client_id, client_secret, scopes
"""
from domain_admin.config import (
OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES,
OIDC_AUTO_CREATE_USER_ROLE, OIDC_AUTO_CREATE_USER_STATUS
)
return {
'issuer_url': OIDC_ISSUER_URL,
'client_id': OIDC_CLIENT_ID,
'client_secret': OIDC_CLIENT_SECRET,
'scopes': OIDC_SCOPES,
'auto_create_user_role': OIDC_AUTO_CREATE_USER_ROLE,
'auto_create_user_status': OIDC_AUTO_CREATE_USER_STATUS
}
def get_authorization_url():
"""
生成授权 URL 并重定向
:return: Flask redirect response
"""
if not hasattr(oauth, 'oidc'):
raise AppException("OIDC 客户端未初始化")
# 动态构建完整的 redirect_uri
# 使用 url_for 生成回调地址,_external=True 生成完整 URL
redirect_uri = url_for('oidc_callback', _external=True)
return oauth.oidc.authorize_redirect(redirect_uri)
def handle_callback():
"""
处理 OIDC 回调
:return: JWT token
"""
if not hasattr(oauth, 'oidc'):
raise AppException("OIDC 客户端未初始化")
try:
logger.info("开始处理 OIDC 回调")
# 交换授权码获取 token
token = oauth.oidc.authorize_access_token()
logger.info(f"Token 交换成功,token keys: {token.keys() if token else 'None'}")
# 获取用户信息
userinfo = token.get('userinfo')
if not userinfo:
# 如果 token 中没有 userinfo,从 userinfo 端点获取
logger.info("从 userinfo 端点获取用户信息")
userinfo = oauth.oidc.userinfo(token=token)
logger.info(f"用户信息获取成功: {userinfo.keys() if userinfo else 'None'}")
# 处理用户登录或注册
return process_oidc_user(userinfo)
except AppException:
raise
except Exception as e:
logger.error(f"OIDC 回调处理失败: {str(e)}")
raise AppException(f"OIDC 认证失败: {str(e)}")
def process_oidc_user(userinfo):
"""
处理 OIDC 用户信息,创建或更新用户
:param userinfo: OIDC 用户信息
:return: JWT token
"""
# 从 userinfo 中提取用户标识
# 优先使用 preferred_username,其次使用 email,最后使用 sub
username = userinfo.get('preferred_username') or userinfo.get('email') or userinfo.get('sub')
if not username:
raise AppException('无法从 OIDC 提供商获取用户标识')
logger.info(f"处理 OIDC 用户: {username}")
# 查找或创建用户
user_row = UserModel.select().where(
UserModel.username == username
).get_or_none()
if not user_row:
# 自动创建用户
oidc_config = get_oidc_config()
user_row = UserModel.create(
username=username,
password='', # OIDC 用户不需要密码
role=oidc_config['auto_create_user_role'],
status=oidc_config['auto_create_user_status'],
avatar_url=userinfo.get('picture', '')
)
logger.info(f"创建新用户: {username}")
else:
logger.info(f"用户已存在: {username}")
# 生成 JWT token
return token_service.encode_token({
'user_id': user_row.id
})