Implement Casdoor authentication integration, including login and callback routes. Update environment variables for Casdoor configuration. Modify login page to support Casdoor login option and enhance UI styles. Adjust uWSGI port settings and update application logging for user actions.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# Casdoor 集成配置指南
|
||||
|
||||
本文档将指导您如何配置 Casdoor 作为项目的登录认证系统。
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 已通过 1Panel 部署 Casdoor 服务
|
||||
2. Casdoor 服务可以正常访问
|
||||
3. 拥有 Casdoor 管理员权限
|
||||
|
||||
## 第一步:在 Casdoor 中创建应用
|
||||
|
||||
1. **登录 Casdoor 管理界面**
|
||||
- 访问您的 Casdoor 地址(例如:`http://your-server:8000`)
|
||||
- 使用管理员账户登录(默认:`admin` / `123`)
|
||||
|
||||
2. **创建应用(Application)**
|
||||
- 进入 "Applications" 页面
|
||||
- 点击 "Add" 按钮创建新应用
|
||||
- 填写以下信息:
|
||||
- **Name**: 应用名称(例如:`4an-app`)
|
||||
- **Display name**: 显示名称(例如:`4AN 工单管理系统`)
|
||||
- **Organization**: 选择组织(默认:`built-in`)
|
||||
- **Enable password**: 是否启用密码登录(根据需要选择)
|
||||
- **Enable sign up**: 是否允许注册(根据需要选择)
|
||||
|
||||
3. **配置回调 URL(Redirect URI)**
|
||||
- 在应用配置中找到 "Redirect URLs" 或 "Redirect URI"
|
||||
- 添加您的应用回调地址,例如:
|
||||
```
|
||||
http://your-app-server:18061/callback
|
||||
```
|
||||
- 如果有多个环境,可以添加多个回调地址
|
||||
|
||||
4. **记录应用凭证**
|
||||
- 创建应用后,Casdoor 会生成:
|
||||
- **Client ID**: 客户端 ID
|
||||
- **Client Secret**: 客户端密钥
|
||||
- 请妥善保存这些信息,后续配置需要使用
|
||||
|
||||
## 第二步:配置项目环境变量
|
||||
|
||||
在您的环境变量文件(`.env` 或系统环境变量)中添加以下配置:
|
||||
|
||||
```bash
|
||||
# Casdoor 配置
|
||||
CASDOOR_ENDPOINT=http://your-casdoor-server:8000
|
||||
CASDOOR_CLIENT_ID=your_client_id_here
|
||||
CASDOOR_CLIENT_SECRET=your_client_secret_here
|
||||
CASDOOR_ORGANIZATION_NAME=built-in
|
||||
CASDOOR_APPLICATION_NAME=your_app_name
|
||||
CASDOOR_REDIRECT_URI=http://your-app-server:18061/callback
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
- `CASDOOR_ENDPOINT`: Casdoor 服务的完整地址(不包含路径)
|
||||
- `CASDOOR_CLIENT_ID`: 在 Casdoor 中创建应用时获得的 Client ID
|
||||
- `CASDOOR_CLIENT_SECRET`: 在 Casdoor 中创建应用时获得的 Client Secret
|
||||
- `CASDOOR_ORGANIZATION_NAME`: 组织名称(默认:`built-in`)
|
||||
- `CASDOOR_APPLICATION_NAME`: 应用名称(与 Casdoor 中创建的应用名称一致)
|
||||
- `CASDOOR_REDIRECT_URI`: 回调地址,必须与 Casdoor 应用中配置的 Redirect URI 完全一致
|
||||
|
||||
## 第三步:安装依赖
|
||||
|
||||
确保已安装所有必要的 Python 依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
主要依赖包括:
|
||||
- `requests`: 用于与 Casdoor API 通信
|
||||
- `Flask-Login`: 用于会话管理
|
||||
- 其他项目依赖
|
||||
|
||||
## 第四步:在 Casdoor 中创建用户
|
||||
|
||||
1. **创建用户**
|
||||
- 进入 Casdoor 的 "Users" 页面
|
||||
- 点击 "Add" 创建新用户
|
||||
- 填写用户信息:
|
||||
- **Name**: 用户名(唯一标识)
|
||||
- **Display name**: 显示名称
|
||||
- **Email**: 邮箱(可选)
|
||||
- **Phone**: 手机号(建议填写,用于与本地用户表关联)
|
||||
- **Password**: 密码
|
||||
- **Organization**: 选择组织
|
||||
|
||||
2. **关联用户到应用**
|
||||
- 在用户详情页面,确保用户属于正确的组织
|
||||
- 用户登录后会自动同步到本地数据库
|
||||
|
||||
## 第五步:测试登录
|
||||
|
||||
1. **启动应用**
|
||||
```bash
|
||||
python app/app.py
|
||||
# 或使用 uwsgi/supervisor 启动
|
||||
```
|
||||
|
||||
2. **访问登录页面**
|
||||
- 访问 `http://your-app-server:18061/login`
|
||||
- 您应该看到两个登录选项:
|
||||
- "使用 Casdoor 登录" 按钮(如果配置了 Casdoor)
|
||||
- 手机号登录表单(备用方案)
|
||||
|
||||
3. **测试 Casdoor 登录**
|
||||
- 点击 "使用 Casdoor 登录" 按钮
|
||||
- 应该重定向到 Casdoor 登录页面
|
||||
- 使用 Casdoor 中的用户账户登录
|
||||
- 登录成功后应该重定向回应用并自动登录
|
||||
|
||||
## 用户信息同步
|
||||
|
||||
当用户通过 Casdoor 首次登录时,系统会:
|
||||
|
||||
1. 从 Casdoor 获取用户信息
|
||||
2. 检查本地数据库中是否存在该用户(通过手机号匹配)
|
||||
3. 如果不存在,创建新用户记录
|
||||
4. 如果存在,更新用户信息(姓名、支局、角色等)
|
||||
|
||||
用户信息映射:
|
||||
- `phone`: 从 Casdoor 的 `phone` 或 `phoneNumber` 字段获取
|
||||
- `name`: 从 Casdoor 的 `name` 或 `displayName` 字段获取
|
||||
- `branch`: 从 Casdoor 的 `affiliation` 字段获取
|
||||
- `role`: 从 Casdoor 的 `type` 字段获取
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题:点击 Casdoor 登录按钮没有反应或报错
|
||||
|
||||
**解决方案**:
|
||||
1. 检查环境变量是否正确配置
|
||||
2. 检查 `CASDOOR_ENDPOINT` 是否可以访问
|
||||
3. 检查 `CASDOOR_CLIENT_ID` 和 `CASDOOR_CLIENT_SECRET` 是否正确
|
||||
4. 查看应用日志获取详细错误信息
|
||||
|
||||
### 问题:登录后回调失败
|
||||
|
||||
**解决方案**:
|
||||
1. 确保 `CASDOOR_REDIRECT_URI` 与 Casdoor 应用中配置的完全一致
|
||||
2. 检查回调地址是否可以从 Casdoor 服务器访问
|
||||
3. 检查防火墙和网络配置
|
||||
|
||||
### 问题:无法获取用户信息
|
||||
|
||||
**解决方案**:
|
||||
1. 检查 Casdoor 应用配置中的 Scope 是否包含 `openid profile email phone`
|
||||
2. 检查用户是否有正确的权限
|
||||
3. 查看应用日志中的详细错误信息
|
||||
|
||||
### 问题:用户信息同步失败
|
||||
|
||||
**解决方案**:
|
||||
1. 确保 Casdoor 用户信息中包含手机号(phone)
|
||||
2. 检查数据库连接是否正常
|
||||
3. 查看应用日志获取详细错误信息
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **保护 Client Secret**
|
||||
- 不要将 `CASDOOR_CLIENT_SECRET` 提交到代码仓库
|
||||
- 使用环境变量或密钥管理服务存储敏感信息
|
||||
|
||||
2. **使用 HTTPS**
|
||||
- 生产环境建议使用 HTTPS
|
||||
- 更新 `CASDOOR_ENDPOINT` 和 `CASDOOR_REDIRECT_URI` 为 HTTPS 地址
|
||||
|
||||
3. **定期更新密码**
|
||||
- 定期更新 Casdoor 管理员密码
|
||||
- 定期轮换 Client Secret
|
||||
|
||||
4. **监控日志**
|
||||
- 定期检查应用日志,关注认证失败的情况
|
||||
- 设置告警机制
|
||||
|
||||
## 更多信息
|
||||
|
||||
- Casdoor 官方文档:https://casdoor.org/docs/overview
|
||||
- Casdoor GitHub:https://github.com/casbin/casdoor
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 如果未配置 Casdoor(未设置 `CASDOOR_CLIENT_ID`),系统会自动使用原有的手机号登录方式
|
||||
2. 两种登录方式可以并存,用户可以选择使用哪种方式登录
|
||||
3. 用户信息会从 Casdoor 同步到本地数据库,确保数据一致性
|
||||
|
||||
@@ -33,4 +33,11 @@ UWSGI_MAX_REQUESTS=1000
|
||||
UWSGI_BUFFER_SIZE=32768
|
||||
UWSGI_DISABLE_LOGGING=true
|
||||
UWSGI_MEMORY_REPORT=true
|
||||
UWSGI_ENABLE_THREADS=true
|
||||
UWSGI_ENABLE_THREADS=true
|
||||
|
||||
CASDOOR_ENDPOINT=http://10.10.10.14:18000
|
||||
CASDOOR_CLIENT_ID=8f323670e073612794ef
|
||||
CASDOOR_CLIENT_SECRET=bc37d89f9220fe5a46c17125c731d2daf22f1299
|
||||
CASDOOR_REDIRECT_URI=http://10.10.10.14:18019/callback
|
||||
CASDOOR_ORGANIZATION_NAME=built-in
|
||||
CASDOOR_APPLICATION_NAME=4an
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -92,4 +92,4 @@ if __name__ == '__main__':
|
||||
app.logger.info("数据库表创建成功")
|
||||
except Exception as e:
|
||||
app.logger.error(f"创建数据库表时出错: {str(e)}")
|
||||
app.run(host='0.0.0.0', port=18061, debug=True)
|
||||
app.run(host='0.0.0.0', port=18019, debug=True)
|
||||
+9
-1
@@ -22,4 +22,12 @@ class Config:
|
||||
'user': os.environ.get('KEY110_DB_USER'),
|
||||
'password': os.environ.get('KEY110_DB_PASSWORD'),
|
||||
'database': os.environ.get('KEY110_DB_DATABASE')
|
||||
}
|
||||
}
|
||||
|
||||
# Casdoor 配置
|
||||
CASDOOR_ENDPOINT = os.environ.get('CASDOOR_ENDPOINT', 'http://localhost:8000')
|
||||
CASDOOR_CLIENT_ID = os.environ.get('CASDOOR_CLIENT_ID')
|
||||
CASDOOR_CLIENT_SECRET = os.environ.get('CASDOOR_CLIENT_SECRET')
|
||||
CASDOOR_ORGANIZATION_NAME = os.environ.get('CASDOOR_ORGANIZATION_NAME', 'built-in')
|
||||
CASDOOR_APPLICATION_NAME = os.environ.get('CASDOOR_APPLICATION_NAME', 'app-built-in')
|
||||
CASDOOR_REDIRECT_URI = os.environ.get('CASDOOR_REDIRECT_URI', 'http://localhost:18061/callback')
|
||||
+9
-94
@@ -1,94 +1,9 @@
|
||||
2025-11-27 16:03:48 INFO: 应用启动
|
||||
2025-11-27 16:03:48 INFO: 错误处理器初始化完成
|
||||
2025-11-27 16:03:57 INFO: 用户 韦矍森(19977899008) 登录成功
|
||||
2025-11-27 16:03:57 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-11-27 16:05:16 INFO: 应用启动
|
||||
2025-11-27 16:05:16 INFO: 错误处理器初始化完成
|
||||
2025-11-27 16:05:23 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-11-27 16:05:29 INFO: 用户 韦矍森 访问历史记录页面
|
||||
2025-11-27 16:05:30 INFO: 用户 韦矍森 访问历史记录页面
|
||||
2025-12-03 14:31:04 INFO: 用户 韦矍森(19977899008) 登录成功
|
||||
2025-12-03 14:31:04 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-12-03 14:31:23 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:31:23 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:31:27 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-12-03 14:31:27 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:31:27 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:31:31 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:31:31 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-03 14:32:03 INFO: 用户 韦矍森(19977899008) 登出
|
||||
2025-12-03 14:32:29 INFO: 用户 唐昊(18977846300) 登录成功
|
||||
2025-12-03 14:32:29 INFO: 用户 唐昊 访问仪表盘页面
|
||||
2025-12-03 14:32:30 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:32:30 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:32:35 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:32:35 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:32:45 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:32:45 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:33:04 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:33:04 INFO: 用户 唐昊 访问统计页面
|
||||
2025-12-03 14:33:12 INFO: 用户 韦炳育(18176156082) 登录成功
|
||||
2025-12-03 14:33:12 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-03 14:34:03 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-03 14:38:02 INFO: 用户 韦炳育 开始创建手工故障单
|
||||
2025-12-03 14:38:02 INFO: 用户 韦炳育 创建手工故障单成功:M202512030001
|
||||
2025-12-03 14:38:02 INFO: 用户 韦炳育 访问历史记录页面
|
||||
2025-12-03 14:38:05 INFO: 用户 韦炳育 访问历史记录页面
|
||||
2025-12-03 14:38:29 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-03 14:38:45 INFO: 用户 韦炳育 访问历史记录页面
|
||||
2025-12-03 14:38:47 INFO: 用户 韦炳育 查看工单 M202512030001 的详情
|
||||
2025-12-03 14:39:26 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-05 08:08:48 INFO: 应用启动
|
||||
2025-12-05 08:08:48 INFO: 错误处理器初始化完成
|
||||
2025-12-05 12:10:36 INFO: 用户 韦炳育(18176156082) 登录成功
|
||||
2025-12-05 12:10:36 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-05 12:11:59 INFO: 用户 韦炳育 访问仪表盘页面
|
||||
2025-12-05 14:57:55 INFO: 应用启动
|
||||
2025-12-05 14:57:55 INFO: 错误处理器初始化完成
|
||||
2025-12-09 12:23:07 INFO: 用户 韦矍森(19977899008) 登录成功
|
||||
2025-12-09 12:23:07 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-12-09 12:23:15 INFO: 用户请求验证码
|
||||
2025-12-09 12:23:15 INFO: 验证码获取成功
|
||||
2025-12-09 12:23:21 INFO: 用户 韦矍森 开始同步摄像头第一步
|
||||
2025-12-09 12:23:21 INFO: 尝试登录中兴平台
|
||||
2025-12-09 12:23:21 INFO: 登录成功,开始获取在线信息
|
||||
2025-12-09 12:23:22 INFO: 获取到 1436 个设备信息,开始更新数据库
|
||||
2025-12-09 12:23:22 INFO: 数据库更新完成,更新了 1436 条记录,其中离线设备 720 个
|
||||
2025-12-09 12:23:23 INFO: 用户 韦矍森 开始同步摄像头第二步
|
||||
2025-12-09 12:23:24 INFO: 查询到 1107 个需要同步的设备
|
||||
2025-12-09 12:23:24 INFO: 发现 35 个离线设备需要创建工单
|
||||
2025-12-09 12:23:24 INFO: 成功创建 35 个工单
|
||||
2025-12-10 10:38:00 ERROR: 未处理异常: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
Traceback (most recent call last):
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 891, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 500, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore[misc]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/ctx.py", line 362, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
|
||||
2025-12-10 10:38:00 ERROR: 未处理异常: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
Traceback (most recent call last):
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 891, in dispatch_request
|
||||
self.raise_routing_exception(req)
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/app.py", line 500, in raise_routing_exception
|
||||
raise request.routing_exception # type: ignore[misc]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/flask/ctx.py", line 362, in match_request
|
||||
result = self.url_adapter.match(return_rule=True) # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/v6ole/pyproject/venv/lib/python3.12/site-packages/werkzeug/routing/map.py", line 624, in match
|
||||
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
|
||||
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
|
||||
|
||||
2025-12-10 15:06:31 INFO: 重定向到 Casdoor 授权页面
|
||||
2025-12-10 15:06:39 INFO: Casdoor 用户信息: {'sub': '798f4436-fbb6-45f4-bf0c-6e1cf4add605', 'iss': 'http://10.10.10.14:18000', 'aud': '8f323670e073612794ef', 'preferred_username': 'test', 'name': '韦矍森', 'email': 'voole@vip.qq.com', 'email_verified': True, 'picture': 'https://cdn.casbin.org/img/casbin.svg', 'phone': '19977899008'}
|
||||
2025-12-10 15:06:39 INFO: 更新用户信息: 韦矍森(19977899008)
|
||||
2025-12-10 15:06:39 INFO: 用户 韦矍森(19977899008) 通过 Casdoor 登录成功
|
||||
2025-12-10 15:06:39 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
2025-12-10 15:06:49 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-10 15:06:50 INFO: 用户 韦矍森 访问历史记录页面
|
||||
2025-12-10 15:06:51 INFO: 用户 韦矍森 访问统计页面
|
||||
2025-12-10 15:06:54 INFO: 用户 韦矍森 访问仪表盘页面
|
||||
|
||||
@@ -323,6 +323,47 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.login_button_casdoor {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login_button_casdoor:hover {
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.login_button_phone {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.login_casdoor_section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login_divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 25px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login_divider::before,
|
||||
.login_divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.login_divider span {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 991.98px) {
|
||||
.login_box {
|
||||
|
||||
@@ -5,17 +5,39 @@
|
||||
<h2 class="login_title">欢迎使用</h2>
|
||||
<div class="login_subtitle">大化重点项目工单管理平台</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="login_error">
|
||||
{% for message in messages %}
|
||||
{{ message }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if casdoor_enabled %}
|
||||
<!-- Casdoor 登录按钮 -->
|
||||
<div class="login_casdoor_section">
|
||||
<a href="{{ url_for('casdoor_login') }}" class="login_button login_button_casdoor">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 8px;">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
|
||||
</svg>
|
||||
使用 Casdoor 登录
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="login_divider">
|
||||
<span>或</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 原有手机号登录表单 -->
|
||||
<form method="POST" class="login_form">
|
||||
<div class="login_input_group">
|
||||
<label for="phone" class="login_label">用户名</label>
|
||||
<input type="text" id="phone" name="phone" class="login_input" required>
|
||||
<label for="phone" class="login_label">用户名(手机号)</label>
|
||||
<input type="text" id="phone" name="phone" class="login_input" placeholder="请输入手机号" required>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="login_error">请先登录以访问此页面</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<button type="submit" class="login_button">登 录</button>
|
||||
<button type="submit" class="login_button login_button_phone">手机号登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
casdoor_auth = CasdoorAuth()
|
||||
|
||||
Binary file not shown.
+106
-3
@@ -1,26 +1,129 @@
|
||||
from flask import render_template, request, redirect, url_for, flash
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from flask_login import login_user, login_required, logout_user, current_user
|
||||
from scripts.models import db, User
|
||||
from utils.casdoor_auth import casdoor_auth
|
||||
|
||||
def init_auth_routes(app):
|
||||
# 初始化 Casdoor 认证
|
||||
casdoor_auth.init_app(app)
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
# 如果已登录,直接跳转到仪表盘
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# 检查是否启用了 Casdoor
|
||||
casdoor_enabled = bool(app.config.get('CASDOOR_CLIENT_ID'))
|
||||
|
||||
# 处理手机号登录
|
||||
if request.method == 'POST':
|
||||
phone = request.form['phone']
|
||||
user = User.query.filter_by(phone=phone).first()
|
||||
if user:
|
||||
login_user(user)
|
||||
app.logger.info(f'用户 {user.name}({user.phone}) 登录成功')
|
||||
# 无论是统计员还是装维员,都跳转到仪表盘
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
app.logger.warning(f'登录失败:用户不存在 (手机号: {phone})')
|
||||
flash('用户不存在')
|
||||
return render_template('login.html')
|
||||
|
||||
return render_template('login.html', casdoor_enabled=casdoor_enabled)
|
||||
|
||||
@app.route('/casdoor/login')
|
||||
def casdoor_login():
|
||||
"""Casdoor 登录入口,重定向到 Casdoor 授权页面"""
|
||||
if not app.config.get('CASDOOR_CLIENT_ID'):
|
||||
flash('Casdoor 未配置,请使用手机号登录')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
auth_url = casdoor_auth.get_authorization_url()
|
||||
app.logger.info('重定向到 Casdoor 授权页面')
|
||||
return redirect(auth_url)
|
||||
|
||||
@app.route('/callback')
|
||||
def casdoor_callback():
|
||||
"""Casdoor OAuth 回调处理"""
|
||||
code = request.args.get('code')
|
||||
state = request.args.get('state')
|
||||
error = request.args.get('error')
|
||||
|
||||
if error:
|
||||
app.logger.error(f'Casdoor 认证失败: {error}')
|
||||
flash(f'登录失败: {error}')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not code:
|
||||
app.logger.warning('Casdoor 回调缺少授权码')
|
||||
flash('登录失败:未收到授权码')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# 验证 state 参数
|
||||
if not casdoor_auth.verify_state(state):
|
||||
app.logger.warning('Casdoor state 验证失败,可能存在 CSRF 攻击')
|
||||
flash('登录失败:安全验证失败')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# 获取访问令牌
|
||||
token_data = casdoor_auth.get_token(code)
|
||||
if not token_data or 'access_token' not in token_data:
|
||||
app.logger.error('获取 Casdoor token 失败')
|
||||
flash('登录失败:无法获取访问令牌')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
access_token = token_data['access_token']
|
||||
|
||||
# 获取用户信息
|
||||
user_info = casdoor_auth.get_user_info(access_token)
|
||||
if not user_info:
|
||||
app.logger.error('获取 Casdoor 用户信息失败')
|
||||
flash('登录失败:无法获取用户信息')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# 从 Casdoor 用户信息中提取数据
|
||||
# Casdoor 返回的用户信息可能包含:name, email, phone, id 等字段
|
||||
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 '未知用户'
|
||||
|
||||
app.logger.info(f'Casdoor 用户信息: {user_info}')
|
||||
|
||||
# 查找或创建本地用户
|
||||
user = User.query.filter_by(phone=phone).first()
|
||||
if not user:
|
||||
# 如果用户不存在,创建新用户
|
||||
user = User(
|
||||
phone=phone,
|
||||
name=name,
|
||||
branch=user_info.get('affiliation', ''),
|
||||
role=user_info.get('type', '')
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
app.logger.info(f'创建新用户: {name}({phone})')
|
||||
else:
|
||||
# 更新用户信息
|
||||
user.name = name
|
||||
if user_info.get('affiliation'):
|
||||
user.branch = user_info.get('affiliation')
|
||||
if user_info.get('type'):
|
||||
user.role = user_info.get('type')
|
||||
db.session.commit()
|
||||
app.logger.info(f'更新用户信息: {name}({phone})')
|
||||
|
||||
# 登录用户
|
||||
login_user(user)
|
||||
app.logger.info(f'用户 {user.name}({user.phone}) 通过 Casdoor 登录成功')
|
||||
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
app.logger.info(f'用户 {current_user.name}({current_user.phone}) 登出')
|
||||
logout_user()
|
||||
# 如果配置了 Casdoor,可以重定向到 Casdoor 登出页面
|
||||
if app.config.get('CASDOOR_ENDPOINT'):
|
||||
logout_url = f"{app.config.get('CASDOOR_ENDPOINT')}/logout"
|
||||
return redirect(logout_url)
|
||||
return redirect(url_for('login'))
|
||||
@@ -0,0 +1,9 @@
|
||||
Flask>=2.0.0
|
||||
Flask-Login>=0.6.0
|
||||
Flask-Migrate>=3.0.0
|
||||
Flask-SQLAlchemy>=2.5.0
|
||||
SQLAlchemy>=1.4.0
|
||||
python-dotenv>=0.19.0
|
||||
requests>=2.25.0
|
||||
PyMySQL>=1.0.0
|
||||
|
||||
Reference in New Issue
Block a user