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:
2025-12-10 15:08:04 +08:00
parent 0f41063688
commit bb39314940
16 changed files with 541 additions and 2148 deletions
+8 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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: 用户 韦矍森 访问仪表盘页面
+41
View File
@@ -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 {
+30 -8
View File
@@ -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.
+130
View File
@@ -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
View File
@@ -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'))