177 lines
5.2 KiB
Python
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
|
|
})
|