From f511e3e808f8f98746fd06a7945c0cd2fad9309b Mon Sep 17 00:00:00 2001 From: v6ole Date: Thu, 30 Jul 2026 15:05:52 +0800 Subject: [PATCH] fix: security remediation follow-up and OLT button layout improvements - fix Casdoor JWT verification with correct PythonProject public key - fix iMC TLS cert validation with custom SSL adapter (skip hostname check for non-DNS CN) - add iMC CA cert and Casdoor public key to build context - improve OLT manage page: unify button styles, fix mobile grid spacing, replace el-upload with native input for consistent alignment - swap NTP sync button for duplicate MAC on mobile Co-Authored-By: Claude --- backend/app/core/security.py | 2 +- backend/app/services/imc_service.py | 38 ++++++++++- backend/imc_ca.pem | 18 ++++++ backend/token_jwt_key.pem | 14 +++++ frontend/src/views/OltManage.vue | 98 +++++++++++++++++++---------- 5 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 backend/imc_ca.pem create mode 100644 backend/token_jwt_key.pem diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 75b70e4..24f24cd 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -37,7 +37,7 @@ def verify_casdoor_token(token: str) -> dict: header = pyjwt.get_unverified_header(token) algorithm = header.get("alg") if algorithm not in {"RS256", "RS384", "RS512"}: - raise ValueError("Unsupported Casdoor token algorithm") + raise ValueError(f"Unsupported Casdoor token algorithm: {algorithm}") return pyjwt.decode( token, diff --git a/backend/app/services/imc_service.py b/backend/app/services/imc_service.py index 61f7201..1b36e0b 100644 --- a/backend/app/services/imc_service.py +++ b/backend/app/services/imc_service.py @@ -39,16 +39,52 @@ class IMCService: """iMC REST API 服务封装""" def __init__(self): + import os + import tempfile + import certifi + from requests.adapters import HTTPAdapter + self.base_url = settings.IMC_API_URL.rstrip('/') self.username = settings.IMC_API_USERNAME self.password = settings.IMC_API_PASSWORD - self.verify_ssl = settings.IMC_API_VERIFY_SSL self.connect_timeout = settings.IMC_CONNECT_TIMEOUT self.read_timeout = settings.IMC_READ_TIMEOUT self.session = requests.Session() self.realm = "iMC RESTful Web Services" self.nonce = None self.nc = 1 + self._ca_bundle_file = None # temp file for certifi + iMC CA + + if settings.IMC_API_VERIFY_SSL: + imc_ca_path = os.path.join(os.path.dirname(__file__), "..", "..", "imc_ca.pem") + if os.path.isfile(imc_ca_path): + # The iMC self-signed cert uses a non-DNS CN and zero SAN entries, + # so hostname matching is impossible. We still enforce full + # certificate-chain verification via a combined CA bundle, then + # tell urllib3 to skip its own hostname check. + with open(imc_ca_path, "rb") as fh: + imc_pem = fh.read() + self._ca_bundle_file = tempfile.NamedTemporaryFile(suffix=".pem", delete=False) + with open(certifi.where(), "rb") as fh: + self._ca_bundle_file.write(fh.read()) + self._ca_bundle_file.write(b"\n") + self._ca_bundle_file.write(imc_pem) + self._ca_bundle_file.flush() + + _ca_bundle = self._ca_bundle_file.name + + class _IMCAdapter(HTTPAdapter): + def cert_verify(self, conn, url, verify, cert): + super().cert_verify(conn, url, verify=_ca_bundle, cert=cert) + conn.assert_hostname = False + + self.session.mount("https://", _IMCAdapter()) + self.verify_ssl = True + logger.info("iMC TLS verification enabled (hostname check relaxed)") + else: + self.verify_ssl = True + else: + self.verify_ssl = False # ── Digest 认证 ────────────────────────────────────────────────────────── diff --git a/backend/imc_ca.pem b/backend/imc_ca.pem new file mode 100644 index 0000000..e9b71d7 --- /dev/null +++ b/backend/imc_ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+TCCAeGgAwIBAgIEHno+KjANBgkqhkiG9w0BAQsFADAtMQwwCgYDVQQLDANS +JkQxHTAbBgNVBAMTFGlNQyBEZXZlbG9wbWVudCBUZWFtMB4XDTE5MDExOTA2Mzkx +OVoXDTM5MDExNDA2MzkxOVowLTEMMAoGA1UECwwDUiZEMR0wGwYDVQQDExRpTUMg +RGV2ZWxvcG1lbnQgVGVhbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AIVGUvmtUD6Gx0AQLi+/voDIuSERGLyTLcQQOBmWVWLfxr0i1PGxuGODylN+30Ud +pBY1faRijvUZUWN51dxjXMRLnHUecBuPVyWOv1jPFMd1jcc7KmQ4KY6s43t1cC2R +pBcfyBhVmN+tArhcbDJb6MUuN1P5ZGi3E1vor9EY0Gt8TvJ/GgmqgWwlveAC0soH +yTTk8UU9m2OikbaZ0dKgQbshna4wrSWw9iyJ5Bao4rZgus3hGZUnznWpPD0/jHC4 +lI3znCKQPAsnJculZcK/8dm1UYKFx7C3L5z4SnzsLY+rTYXvmGfb2x06wKkkJTIt +oepc1U5U/jNlUVbNdA9yhP0CAwEAAaMhMB8wHQYDVR0OBBYEFGErff6AvZVRmgqK +DLQxMBGOr0ObMA0GCSqGSIb3DQEBCwUAA4IBAQA9HhuDvrVI4ICyddy5g4DM+bv4 +NaKoBS0Y5nhANbN/0f0J0Zj32OYtbsJlUZViFQ42LIR7b37x6OgMXRjHJIDg3q+9 +tkH5S8Q5xmb5Tqq0WcOWQmg0o1OjW5iJNAfKiTffGtc5DjmrOBuCf1P23G3polRB +34QxGgnRygEDKy5aYumaXyiL1yMgDLkZ2adg2lWvsBSpvmgzMF5H75Spq4WvK60Z +o3EyCpB5ZS4SxwF3JH+LdpsyCc+UsyYW4/v/FVidtCp+nNTgsTA1yI8vcPVyPxPQ +SvVFIvuLYpo9cbHLcmQ95LBKjMbhPVHMzsBRVd85xKVjBspRMWeUp7F94W8V +-----END CERTIFICATE----- diff --git a/backend/token_jwt_key.pem b/backend/token_jwt_key.pem new file mode 100644 index 0000000..0a33e36 --- /dev/null +++ b/backend/token_jwt_key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArvxqOc2fCe611njyX3aG +aprZzyU/UhynIHsJhVMq6tDFLPmJCitg5WFHVTOW5KlTzKI894XJNEPSkf4POoai +1z4CW/GeRrkN8/XP8/RqyiCQAslei5FH5i1RjqLWijm0qUNsgYk07ZpBw45qT1ti +ipJF33L+mr9s+O17ujDDb2EZBzTcuBs1eYhv2fk0bOdYK17Vzzw1StPP0OWmfYev +edw6YemqnODk21e6CGcewXhgIX6k8iBaULQLoy0qTR+4nKwJjnCm4DrbSkYvjM7p +AW/3bQyJax1VbFw2jRo1CRcoaMRzLcutpe7HaT+wf4TR7cZFcaFZieSsruLySD5X +42bVgWDIZtjtXfJnSZc/Fv6IyUi/PzzlWIaxGcqqVHSk3W/w1ubNc+70SQA7fCJP +8vh/GcRNbWF3+mH4B1DSMSl7IAxGNyXeQyNq44Gp9T9MeXsvppQxcHd3Tn4krRi5 +NzWetnRCOH/kQblfK9FK4o1XGkIHVdwCUBoFO4Tlqu3qNCQIkg28Wg6OgdkHrkoc +lW98y0y5Wvt0tJtg48cfpgkHZ9SKM0qhedyUBvGV1fd8QAuwL9JbvMwUX7cVOGBJ +6rc4Sk11uRGVCnmw5Ed5ORa0w70DrQYe9OoNqYiqmkLh+TvXMhGmYQ+V8aw6ejmA +2Efz40ofkK+Z+Pizv9L3wP8CAwEAAQ== +-----END PUBLIC KEY----- diff --git a/frontend/src/views/OltManage.vue b/frontend/src/views/OltManage.vue index 590f1d2..ea8aa98 100644 --- a/frontend/src/views/OltManage.vue +++ b/frontend/src/views/OltManage.vue @@ -8,31 +8,22 @@
- + 添加 OLT - - 批量导入 - - 下载模板 - - 重复 MAC - + + 批量导入 + + 下载模板 + + 重复 MAC{{ duplicateCount }} - 新增设备 - + 新增设备{{ newDeviceCount }} - + 同步NTP @@ -41,6 +32,15 @@ 快速扫描 + +
@@ -604,6 +604,7 @@ const newDevList = ref([]) const newDeviceCount = ref(0) const newDevVisible = ref(false) const savingDev = ref(null) +const importInput = ref(null) const loopDetecting = ref(false) const loopVisible = ref(false) const loopResults = ref([]) @@ -973,10 +974,16 @@ const showLoopDetail = (oltId) => { loopDetailVisible.value = true } -const handleImportFile = async (uploadFile) => { +const triggerImport = () => { + importInput.value?.click() +} + +const handleImportFile = async (event) => { + const file = event.target?.files?.[0] || (event.raw || event) + if (!file) return importing.value = true const formData = new FormData() - formData.append('file', uploadFile.raw) + formData.append('file', file) try { const { data } = await request.post('/olt/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } @@ -1077,8 +1084,35 @@ onMounted(() => { .header-actions { display: flex; align-items: center; - gap: 8px; flex-wrap: wrap; + gap: 8px; +} + +.btn-icon { + margin-right: 5px; + vertical-align: -2px; +} + +.btn-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + margin-left: 4px; + border-radius: 9px; + font-size: 11px; + font-weight: 600; + line-height: 1; + flex-shrink: 0; +} + +.btn-count.danger { background: var(--danger); color: #fff; } +.btn-count.warning { background: var(--warning); color: #fff; } + +.header-actions .el-button { + white-space: nowrap; } /* 数据面板 */ @@ -1526,19 +1560,17 @@ onMounted(() => { .header-actions { width: 100%; display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; - } - - .header-actions .el-button, - .header-actions > * { - width: 100%; - min-height: 44px; - } - - /* 移动端按钮区只显示2列(移动端只剩新增设备、环路检测、快速扫描) */ - .header-actions { grid-template-columns: repeat(2, 1fr); + gap: 8px; + justify-items: center; + } + + .header-actions .el-button { + width: 95%; + min-height: 44px; + padding-left: 14px; + padding-right: 14px; + justify-content: center; } /* 端口管理对话框:限制高度支持滚动 */