Files
4an-workorder/app/utils/casdoor_auth.py
T

261 lines
8.9 KiB
Python

"""
Casdoor 认证工具模块
用于处理 Casdoor OAuth/OIDC 认证流程
"""
import requests
import urllib.parse
from flask import session, current_app
import secrets
class CasdoorAuth:
"""Casdoor 认证处理类"""
def __init__(self, app=None):
self.app = app
if app:
self.init_app(app)
def init_app(self, app):
"""初始化应用配置"""
self.endpoint = app.config.get('CASDOOR_ENDPOINT')
self.client_id = app.config.get('CASDOOR_CLIENT_ID')
self.client_secret = app.config.get('CASDOOR_CLIENT_SECRET')
self.organization_name = app.config.get('CASDOOR_ORGANIZATION_NAME', 'built-in')
self.application_name = app.config.get('CASDOOR_APPLICATION_NAME', 'app-built-in')
self.redirect_uri = app.config.get('CASDOOR_REDIRECT_URI')
def get_authorization_url(self, state=None):
"""
生成 Casdoor 授权 URL
Args:
state: 状态参数,用于防止 CSRF 攻击
Returns:
str: 授权 URL
"""
if not state:
state = secrets.token_urlsafe(32)
session['casdoor_state'] = state
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.redirect_uri,
'scope': 'openid profile email phone',
'state': state
}
auth_url = f"{self.endpoint}/login/oauth/authorize"
return f"{auth_url}?{urllib.parse.urlencode(params)}"
def get_token(self, code):
"""
使用授权码获取访问令牌
Args:
code: 授权码
Returns:
dict: 包含 access_token 和 id_token 的字典
"""
token_url = f"{self.endpoint}/api/login/oauth/access_token"
data = {
'grant_type': 'authorization_code',
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.redirect_uri
}
try:
response = requests.post(token_url, data=data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
try:
current_app.logger.error(f"获取 Casdoor token 失败: {str(e)}")
except:
pass # 如果无法记录日志,忽略错误
return None
def get_user_info(self, access_token):
"""
使用访问令牌获取用户信息
Args:
access_token: 访问令牌
Returns:
dict: 用户信息字典
"""
user_info_url = f"{self.endpoint}/api/userinfo"
headers = {
'Authorization': f'Bearer {access_token}'
}
try:
response = requests.get(user_info_url, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
try:
current_app.logger.error(f"获取 Casdoor 用户信息失败: {str(e)}")
except:
pass # 如果无法记录日志,忽略错误
return None
def verify_state(self, state):
"""
验证 state 参数,防止 CSRF 攻击
Args:
state: 从回调中获取的 state 参数
Returns:
bool: 验证是否通过
"""
stored_state = session.get('casdoor_state')
if stored_state and stored_state == state:
session.pop('casdoor_state', None)
return True
return False
def login_with_password(self, username, password):
"""
使用用户名和密码通过 API 登录 Casdoor
Args:
username: 用户名
password: 密码
Returns:
dict: 包含 access_token 和用户信息的字典,失败返回 None
"""
# Casdoor 使用 OAuth 2.0 password grant 方式登录
# 端点应该是 /api/login/oauth/access_token
login_url = f"{self.endpoint}/api/login/oauth/access_token"
# OAuth 2.0 password grant 需要的参数
data = {
'grant_type': 'password',
'username': username,
'password': password,
'client_id': self.client_id,
'client_secret': self.client_secret,
'scope': 'openid profile email phone'
}
try:
current_app.logger.info(f"尝试 Casdoor OAuth password grant 登录: username={username}")
except:
pass
try:
# OAuth 2.0 password grant 通常使用 form-data 格式
response = requests.post(login_url, data=data, timeout=10)
# 如果返回 415,尝试 JSON 格式
if response.status_code == 415:
response = requests.post(login_url, json=data, timeout=10)
# 检查响应状态
if response.status_code != 200:
try:
error_msg = response.text[:200] # 限制错误消息长度
current_app.logger.warning(f"Casdoor OAuth 登录失败 (状态码 {response.status_code}): {error_msg}")
except:
pass
return None
# 尝试解析 JSON 响应
try:
result = response.json()
except ValueError as e:
try:
current_app.logger.error(f"Casdoor API 登录响应不是有效的 JSON: {str(e)}, 响应内容: {response.text[:200]}")
except:
pass
return None
# 检查 result 是否为 None
if result is None:
try:
current_app.logger.error("Casdoor API 登录返回 None")
except:
pass
return None
# OAuth 2.0 返回格式通常是 {"access_token": "...", "token_type": "Bearer", ...}
# 或者 Casdoor 格式 {"status": "ok", "data": {"access_token": "..."}}
access_token = None
if isinstance(result, dict):
# 检查是否有错误
if result.get('status') == 'error':
try:
error_msg = result.get('msg', '未知错误')
current_app.logger.warning(f"Casdoor OAuth 登录错误: {error_msg}")
except:
pass
return None
# 标准 OAuth 2.0 格式
access_token = result.get('access_token')
# Casdoor 格式 {"status": "ok", "data": {...}}
if not access_token and result.get('status') == 'ok':
token_data = result.get('data', {})
if isinstance(token_data, dict):
access_token = token_data.get('access_token') or token_data.get('token') or token_data.get('accessToken')
# 如果还是没有,尝试其他可能的字段
if not access_token:
access_token = result.get('token') or result.get('accessToken')
# 如果还是没有 token,尝试从其他字段获取
if not access_token and 'data' in result and isinstance(result.get('data'), dict):
access_token = result['data'].get('access_token') or result['data'].get('token')
if not access_token:
try:
current_app.logger.warning(f"Casdoor API 登录返回格式异常,未找到 token: {result}")
except:
pass
return None
# 使用 token 获取用户信息
user_info = self.get_user_info(access_token)
if not user_info:
return None
return {
'access_token': access_token,
'user_info': user_info
}
except requests.exceptions.Timeout:
try:
current_app.logger.error("Casdoor API 登录超时")
except:
pass
return None
except requests.exceptions.RequestException as e:
try:
current_app.logger.error(f"Casdoor API 登录失败: {str(e)}")
except:
pass
return None
except Exception as e:
try:
current_app.logger.error(f"Casdoor API 登录发生未知错误: {str(e)}")
except:
pass
return None
# 创建全局实例
casdoor_auth = CasdoorAuth()