455 lines
12 KiB
Markdown
455 lines
12 KiB
Markdown
# Casdoor 集成经验总结
|
||
|
||
本文档总结了在 Flask 项目中集成 Casdoor 并使用现有登录页面时遇到的所有问题和解决方案,帮助后续项目避免踩坑。
|
||
|
||
## 目录
|
||
|
||
1. [集成方式选择](#集成方式选择)
|
||
2. [配置步骤](#配置步骤)
|
||
3. [常见问题与解决方案](#常见问题与解决方案)
|
||
4. [最佳实践](#最佳实践)
|
||
5. [代码示例](#代码示例)
|
||
|
||
---
|
||
|
||
## 集成方式选择
|
||
|
||
### 方式一:OAuth 重定向方式(不推荐用于现有登录页面)
|
||
|
||
**特点:**
|
||
- 用户点击登录后跳转到 Casdoor 登录页面
|
||
- 登录成功后回调到项目
|
||
- 适合新项目或不需要保留现有登录页面的场景
|
||
|
||
**缺点:**
|
||
- 无法使用项目现有的登录页面
|
||
- 用户体验不够统一
|
||
|
||
### 方式二:API 方式(推荐)
|
||
|
||
**特点:**
|
||
- 使用 OAuth 2.0 password grant 方式
|
||
- 在项目现有登录页面输入用户名密码
|
||
- 通过 API 直接验证,无需跳转
|
||
- 用户体验更好,界面统一
|
||
|
||
**推荐使用方式二**
|
||
|
||
---
|
||
|
||
## 配置步骤
|
||
|
||
### 1. 环境变量配置
|
||
|
||
在 `.env` 文件或系统环境变量中添加:
|
||
|
||
```bash
|
||
# Casdoor 服务地址
|
||
CASDOOR_ENDPOINT=https://your-casdoor-server.com
|
||
|
||
# Casdoor 应用配置(在 Casdoor 管理界面创建应用后获取)
|
||
CASDOOR_CLIENT_ID=your_client_id
|
||
CASDOOR_CLIENT_SECRET=your_client_secret
|
||
|
||
# 组织名称和应用名称(通常在 Casdoor 中配置)
|
||
CASDOOR_ORGANIZATION_NAME=your_organization
|
||
CASDOOR_APPLICATION_NAME=your_application
|
||
|
||
# 会话 Cookie 配置(跨域 SSO 时建议设置)
|
||
SESSION_COOKIE_SAMESITE=Lax # 或 None(跨域时)
|
||
SESSION_COOKIE_SECURE=False # HTTPS 时设为 True
|
||
```
|
||
|
||
### 2. 在 Casdoor 中创建应用
|
||
|
||
1. 登录 Casdoor 管理界面
|
||
2. 进入 "Applications" 页面
|
||
3. 创建新应用,记录 `Client ID` 和 `Client Secret`
|
||
4. **重要**:确保应用启用了 password grant 方式(如果 Casdoor 版本支持)
|
||
|
||
### 3. 代码集成
|
||
|
||
参考项目中的以下文件:
|
||
- `app/utils/casdoor_auth.py` - Casdoor 认证工具类
|
||
- `app/views/auth.py` - 认证路由处理
|
||
- `app/templates/login.html` - 登录页面模板
|
||
|
||
---
|
||
|
||
## 常见问题与解决方案
|
||
|
||
### 问题 1:登录后提示 "Please log in to access this page"
|
||
|
||
**原因:**
|
||
- Flask-Login 的默认提示消息是英文
|
||
- 登录成功后可能有残留的 flash 消息
|
||
|
||
**解决方案:**
|
||
|
||
在 `app.py` 中配置 Flask-Login:
|
||
|
||
```python
|
||
login_manager = LoginManager()
|
||
login_manager.init_app(app)
|
||
login_manager.login_view = 'login'
|
||
login_manager.login_message = '请先登录以访问此页面' # 设置中文提示
|
||
login_manager.login_message_category = 'info'
|
||
```
|
||
|
||
在登录成功后清除 flash 消息:
|
||
|
||
```python
|
||
# 在 auth.py 的登录成功处理中
|
||
session.pop('_flashes', None)
|
||
```
|
||
|
||
在模板中过滤已登录用户的未登录提示:
|
||
|
||
```html
|
||
<!-- base.html -->
|
||
{% with messages = get_flashed_messages() %}
|
||
{% if messages %}
|
||
{% for message in messages %}
|
||
{% if not (current_user.is_authenticated and '请先登录' in message) %}
|
||
<!-- 显示消息 -->
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
```
|
||
|
||
### 问题 2:API 登录返回 "Unauthorized operation"
|
||
|
||
**原因:**
|
||
- 使用了错误的 API 端点
|
||
- 参数格式不正确
|
||
- 未使用 OAuth 2.0 password grant 方式
|
||
|
||
**解决方案:**
|
||
|
||
使用正确的 OAuth 2.0 password grant 端点:
|
||
|
||
```python
|
||
login_url = f"{endpoint}/api/login/oauth/access_token"
|
||
|
||
data = {
|
||
'grant_type': 'password', # 必须指定 grant_type
|
||
'username': username,
|
||
'password': password,
|
||
'client_id': client_id, # 必须包含 client_id
|
||
'client_secret': client_secret, # 必须包含 client_secret
|
||
'scope': 'openid profile email phone'
|
||
}
|
||
```
|
||
|
||
**注意:** 某些 Casdoor 版本可能不支持 password grant,需要检查 Casdoor 文档或使用 OAuth 授权码方式。
|
||
|
||
### 问题 3:退出时跳转到 Casdoor 404 页面
|
||
|
||
**原因:**
|
||
- 退出时尝试跳转到 Casdoor 的退出页面
|
||
- Casdoor 退出 URL 不正确
|
||
|
||
**解决方案:**
|
||
|
||
如果不需要同时退出 Casdoor,只退出本地会话:
|
||
|
||
```python
|
||
@app.route('/logout')
|
||
@login_required
|
||
def logout():
|
||
logout_user()
|
||
return redirect(url_for('login'))
|
||
```
|
||
|
||
如果需要同时退出 Casdoor,使用正确的退出 URL:
|
||
|
||
```python
|
||
# 注意:需要确保 redirect_uri 在 Casdoor 应用的回调白名单中
|
||
logout_url = f"{casdoor_endpoint}/login/oauth/logout?client_id={client_id}&redirect_uri={login_url}"
|
||
```
|
||
|
||
### 问题 4:语法错误 - try-except 块不匹配
|
||
|
||
**原因:**
|
||
- 代码重构时 try 块未正确闭合
|
||
- 在 try 块外使用了 try 块内定义的变量
|
||
|
||
**解决方案:**
|
||
|
||
确保所有逻辑都在完整的 try-except 块中:
|
||
|
||
```python
|
||
try:
|
||
# 所有逻辑代码
|
||
response = requests.post(...)
|
||
result = response.json()
|
||
# ... 处理逻辑
|
||
return result
|
||
except requests.exceptions.Timeout:
|
||
# 处理超时
|
||
return None
|
||
except requests.exceptions.RequestException as e:
|
||
# 处理请求异常
|
||
return None
|
||
except Exception as e:
|
||
# 处理其他异常
|
||
return None
|
||
```
|
||
|
||
**检查要点:**
|
||
- 每个 `try` 必须有对应的 `except` 或 `finally`
|
||
- 变量作用域要正确(在 try 块内定义的变量不能在块外使用)
|
||
|
||
### 问题 5:API 响应格式解析错误
|
||
|
||
**原因:**
|
||
- Casdoor API 返回格式可能不同
|
||
- 未处理各种可能的响应格式
|
||
|
||
**解决方案:**
|
||
|
||
兼容多种响应格式:
|
||
|
||
```python
|
||
result = response.json()
|
||
|
||
# 检查错误
|
||
if isinstance(result, dict) and result.get('status') == 'error':
|
||
error_msg = result.get('msg', '未知错误')
|
||
return None
|
||
|
||
# 尝试多种格式获取 token
|
||
access_token = None
|
||
if isinstance(result, dict):
|
||
# 标准 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')
|
||
|
||
# 其他可能的字段
|
||
if not access_token:
|
||
access_token = result.get('token') or result.get('accessToken')
|
||
```
|
||
|
||
### 问题 6:用户信息同步问题
|
||
|
||
**原因:**
|
||
- Casdoor 返回的用户信息字段名可能不同
|
||
- 本地数据库字段映射不正确
|
||
|
||
**解决方案:**
|
||
|
||
使用多个可能的字段名:
|
||
|
||
```python
|
||
# 从 Casdoor 用户信息中提取数据
|
||
casdoor_id = user_info.get('sub') or user_info.get('id') or user_info.get('name')
|
||
phone = user_info.get('phone') or user_info.get('phoneNumber') or casdoor_id
|
||
name = user_info.get('name') or user_info.get('displayName') or username
|
||
branch = user_info.get('affiliation', '')
|
||
role = user_info.get('type', '')
|
||
```
|
||
|
||
### 问题 7:会话 Cookie 配置问题
|
||
|
||
**原因:**
|
||
- 跨域时 Cookie 无法正确设置
|
||
- SameSite 和 Secure 属性配置不当
|
||
|
||
**解决方案:**
|
||
|
||
在 `config.py` 中配置:
|
||
|
||
```python
|
||
# 跨域 SSO 时
|
||
SESSION_COOKIE_SAMESITE = 'None' # 需要配合 Secure=True
|
||
SESSION_COOKIE_SECURE = True # HTTPS 必须
|
||
|
||
# 同域时
|
||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||
SESSION_COOKIE_SECURE = False # HTTP 开发环境
|
||
```
|
||
|
||
**注意:** `SameSite=None` 必须配合 `Secure=True` 使用,且需要 HTTPS。
|
||
|
||
---
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 错误处理
|
||
|
||
- 始终使用 try-except 包裹 API 调用
|
||
- 记录详细的错误日志,便于排查
|
||
- 对用户显示友好的错误提示
|
||
|
||
```python
|
||
try:
|
||
result = casdoor_auth.login_with_password(username, password)
|
||
if not result:
|
||
flash('用户名或密码错误')
|
||
return render_template('login.html')
|
||
except Exception as e:
|
||
app.logger.error(f"登录异常: {str(e)}")
|
||
flash('登录失败,请稍后重试')
|
||
return render_template('login.html')
|
||
```
|
||
|
||
### 2. 日志记录
|
||
|
||
- 记录登录尝试(不记录密码)
|
||
- 记录 API 调用结果
|
||
- 记录用户信息同步情况
|
||
|
||
```python
|
||
app.logger.info(f"尝试 Casdoor API 登录: username={username}")
|
||
app.logger.info(f"Casdoor 用户信息: {user_info}")
|
||
app.logger.info(f"用户 {user.name}({user.phone}) 通过 Casdoor API 登录成功")
|
||
```
|
||
|
||
### 3. 安全性
|
||
|
||
- 永远不要在日志中记录密码
|
||
- 使用 HTTPS(生产环境)
|
||
- 验证和清理用户输入
|
||
- 使用环境变量存储敏感配置
|
||
|
||
### 4. 用户体验
|
||
|
||
- 提供清晰的错误提示
|
||
- 登录成功后清除残留的提示消息
|
||
- 保持登录页面的原有样式
|
||
|
||
### 5. 代码组织
|
||
|
||
- 将 Casdoor 相关逻辑封装在独立的工具类中
|
||
- 保持路由处理简洁
|
||
- 使用配置类管理所有配置项
|
||
|
||
---
|
||
|
||
## 代码示例
|
||
|
||
### 完整的认证工具类结构
|
||
|
||
```python
|
||
# app/utils/casdoor_auth.py
|
||
class CasdoorAuth:
|
||
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')
|
||
|
||
def login_with_password(self, username, password):
|
||
"""使用用户名和密码通过 API 登录"""
|
||
login_url = f"{self.endpoint}/api/login/oauth/access_token"
|
||
data = {
|
||
'grant_type': 'password',
|
||
'username': username,
|
||
'password': password,
|
||
'client_id': self.client_id,
|
||
'client_secret': self.client_secret,
|
||
'scope': 'openid profile email phone'
|
||
}
|
||
|
||
try:
|
||
response = requests.post(login_url, data=data, timeout=10)
|
||
# ... 处理响应
|
||
except Exception as e:
|
||
# ... 错误处理
|
||
return None
|
||
|
||
def get_user_info(self, access_token):
|
||
"""使用访问令牌获取用户信息"""
|
||
# ... 实现
|
||
pass
|
||
```
|
||
|
||
### 登录路由处理
|
||
|
||
```python
|
||
# app/views/auth.py
|
||
@app.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
if current_user.is_authenticated:
|
||
return redirect(url_for('dashboard'))
|
||
|
||
if request.method == 'POST':
|
||
username = request.form.get('username', '').strip()
|
||
password = request.form.get('password', '').strip()
|
||
|
||
if not username or not password:
|
||
flash('请输入用户名和密码')
|
||
return render_template('login.html')
|
||
|
||
# 使用 API 方式登录
|
||
result = casdoor_auth.login_with_password(username, password)
|
||
|
||
if not result:
|
||
flash('用户名或密码错误')
|
||
return render_template('login.html')
|
||
|
||
# 处理用户信息同步
|
||
user_info = result.get('user_info', {})
|
||
# ... 创建或更新用户
|
||
|
||
login_user(user)
|
||
session.pop('_flashes', None) # 清除残留提示
|
||
return redirect(url_for('dashboard'))
|
||
|
||
return render_template('login.html')
|
||
```
|
||
|
||
---
|
||
|
||
## 检查清单
|
||
|
||
在集成 Casdoor 时,确保完成以下检查:
|
||
|
||
- [ ] 环境变量配置正确
|
||
- [ ] Casdoor 应用已创建并获取 Client ID 和 Secret
|
||
- [ ] 使用正确的 API 端点(`/api/login/oauth/access_token`)
|
||
- [ ] 使用 OAuth 2.0 password grant 方式
|
||
- [ ] 所有 try-except 块正确闭合
|
||
- [ ] 错误处理完善,有详细的日志记录
|
||
- [ ] 登录成功后清除残留的 flash 消息
|
||
- [ ] 用户信息同步逻辑正确
|
||
- [ ] 会话 Cookie 配置正确(跨域时)
|
||
- [ ] 登录页面样式保持一致
|
||
- [ ] 测试各种错误场景(错误密码、网络错误等)
|
||
|
||
---
|
||
|
||
## 参考资源
|
||
|
||
- [Casdoor 官方文档](https://casdoor.org/docs/overview)
|
||
- [OAuth 2.0 Password Grant](https://oauth.net/2/grant-types/password/)
|
||
- [Flask-Login 文档](https://flask-login.readthedocs.io/)
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
集成 Casdoor 到现有项目时,主要注意以下几点:
|
||
|
||
1. **选择正确的集成方式**:API 方式更适合保留现有登录页面
|
||
2. **使用正确的 API 端点和参数**:OAuth 2.0 password grant 方式
|
||
3. **完善的错误处理**:处理各种异常情况和响应格式
|
||
4. **用户体验优化**:清除残留提示,保持界面统一
|
||
5. **安全性考虑**:使用 HTTPS,不在日志中记录敏感信息
|
||
|
||
遵循以上实践,可以避免大部分常见问题,顺利完成 Casdoor 集成。
|
||
|