From 42d3b332a5add8da86df197f2c3e5d9fa7f6b651 Mon Sep 17 00:00:00 2001 From: becomer Date: Mon, 14 Sep 2026 23:12:06 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.50=EF=BC=9A=E8=BE=BD=E5=AE=81=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E6=8A=80=E6=9C=AF=E5=A4=A7=E5=AD=A6=EF=BC=88lntu?= =?UTF-8?q?=EF=BC=89=E7=99=BB=E5=BD=95=E9=80=82=E9=85=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E8=B5=B0=20CAS=20+=20=E6=BB=91=E5=9D=97?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E2=80=94=E2=80=94=E2=91=A0=20?= =?UTF-8?q?=E6=A0=B9=E5=9B=A0=EF=BC=9A=E5=B9=B3=E5=8F=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=20joinType=3DCLOUD=EF=BC=88IsIap=3Dtrue?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E4=BD=86=E7=BD=91=E5=85=B3=20/iap/doLogin=20?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=EF=BC=88HTTP=20405=EF=BC=89?= =?UTF-8?q?=EF=BC=8CIAP=20=E8=B4=A6=E5=AF=86=E6=8F=90=E4=BA=A4=E5=BF=85?= =?UTF-8?q?=E7=84=B6=E5=A4=B1=E8=B4=A5=E5=B9=B6=E8=BF=94=E5=9B=9E=20FAIL?= =?UTF-8?q?=5FUPNOTMATCH=EF=BC=88"=E7=94=A8=E6=88=B7=E5=90=8D=E6=88=96?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E4=B8=8D=E5=8C=B9=E9=85=8D"=EF=BC=8C?= =?UTF-8?q?=E8=AF=AF=E5=AF=BC=E7=94=A8=E6=88=B7=E4=BB=A5=E4=B8=BA=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E9=94=99=E8=AF=AF=EF=BC=89=EF=BC=9B=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E4=B8=BA=20fdm.lntu.edu.cn/iap=20302=20?= =?UTF-8?q?=E2=86=92=20=E5=AD=A6=E6=A0=A1=20CAS=EF=BC=88authserver.lntu.ed?= =?UTF-8?q?u.cn/authserver/login=EF=BC=89=E2=86=92=20=E8=B4=A6=E5=AF=86=20?= =?UTF-8?q?+=20=E9=AA=8C=E8=AF=81=E7=A0=81=20=E2=86=92=20=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=20/iap/loginSuccess=3FsessionToken=3D...&ticket=3D...?= =?UTF-8?q?=20=E5=BB=BA=E7=AB=8B=E7=BD=91=E5=85=B3=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=EF=BC=88cookie=20=E8=90=BD=E5=9C=A8=20fdm.lntu.edu.cn=EF=BC=8C?= =?UTF-8?q?=E4=B8=8E=20wec-*=20=E4=BB=BB=E5=8A=A1=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=90=8C=E5=9F=9F=EF=BC=89=EF=BC=9B=E2=91=A1=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=9A=E6=96=B0=E5=A2=9E=20login=5Flntu.go=20?= =?UTF-8?q?=E7=9A=84=20lntuCasLogin=EF=BC=88=E5=90=8C=20xcu=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E7=94=A8=20/iap=20=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E8=A6=86=E7=9B=96=20ampUrl=20=E5=90=8E?= =?UTF-8?q?=E8=B5=B0=E9=80=9A=E7=94=A8=20CAS=20=E6=B5=81=E7=A8=8B=EF=BC=89?= =?UTF-8?q?=EF=BC=8CCpdailyLogin=20=E4=B8=AD=E5=AF=B9=20LntuSchoolId=20?= =?UTF-8?q?=E7=89=B9=E5=88=A4=E5=BC=BA=E5=88=B6=E8=B5=B0=20CAS=EF=BC=9B?= =?UTF-8?q?=E2=91=A2=20=E9=AA=8C=E8=AF=81=E7=A0=81=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=EF=BC=9A=E6=8A=93=E5=8F=96=E7=99=BB=E5=BD=95=E9=A1=B5=20JS=20?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=20captchaSwitch=3D"2"=20=E4=B8=94=20startLog?= =?UTF-8?q?in=20=E8=B5=B0=20createSliderCaptcha()=EF=BC=88wisedu=20?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E6=BB=91=E5=9D=97=EF=BC=8C=E9=9D=9E=E5=9B=BE?= =?UTF-8?q?=E5=BD=A2=E9=AA=8C=E8=AF=81=E7=A0=81=EF=BC=89=EF=BC=8C=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E4=B8=8E=E5=8D=8E=E4=BE=A8=E5=A4=A7=E5=AD=A6=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E5=90=8C=E6=BA=90=EF=BC=88openSliderCaptcha.htl=20?= =?UTF-8?q?=E5=8F=96=E5=9B=BE=E3=80=81sign=3DAES(JSON.stringify({canvasLen?= =?UTF-8?q?gth,moveLength,tracks}),=20=E5=B0=8F=E5=9B=BE=20base64=20?= =?UTF-8?q?=E6=9C=AB16=E5=AD=97=E8=8A=82)=20=E6=8F=90=E4=BA=A4=20verifySli?= =?UTF-8?q?derCaptcha.htl=E3=80=81errorCode=3D=3D1=20=E9=80=9A=E8=BF=87?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=95=85=E4=BB=85=E9=9C=80=E5=B0=86=20lntu?= =?UTF-8?q?=20=E5=8A=A0=E5=85=A5=20sliderIds=20=E9=85=8D=E7=BD=AE=E5=8D=B3?= =?UTF-8?q?=E5=8F=AF=E5=A4=8D=E7=94=A8=E7=8E=B0=E6=9C=89=E6=BB=91=E5=9D=97?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=88config.toml.example=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=9B=B4=E6=96=B0=EF=BC=89=EF=BC=9B=E2=91=A3=20?= =?UTF-8?q?=E7=AB=AF=E5=88=B0=E7=AB=AF=E5=AE=9E=E6=B5=8B=EF=BC=9A=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=BE=85=E9=80=82=E9=85=8D=E8=B4=A6=E5=8F=B7=E4=B8=A4?= =?UTF-8?q?=E6=AC=A1=E7=99=BB=E5=BD=95=E5=9D=87=E4=B8=80=E6=AC=A1=E9=80=9A?= =?UTF-8?q?=E8=BF=87=EF=BC=88gapX=3D92/149=20=E9=A6=96=E5=80=99=E9=80=89?= =?UTF-8?q?=20offset=3D0=20=E7=9B=B4=E6=8E=A5=E5=91=BD=E4=B8=AD=20errorCod?= =?UTF-8?q?e=3D1=EF=BC=89=EF=BC=8CCAS=20302=20=E2=86=92=206=20cookies?= =?UTF-8?q?=EF=BC=88CASTGC/JSESSIONID/MOD=5FAUTH=5FCAS/happyVoyage/route/p?= =?UTF-8?q?latformMultilingual=EF=BC=89=E2=86=92=20FetchUserProfile=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=9C=89=E6=95=88=EF=BC=9B=E2=91=A4=20?= =?UTF-8?q?=E6=8C=89=E7=94=A8=E6=88=B7=E8=A6=81=E6=B1=82=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=E6=AD=A4=E5=89=8D=20LoginUrl=EF=BC=88=E5=BF=98=E8=AE=B0?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=9C=B0=E5=9D=80=EF=BC=89=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8E=9F=E5=9C=B0=E5=9D=80=E6=89=93=E4=B8=8D?= =?UTF-8?q?=E5=BC=80=E6=98=AF=E5=9B=A0=E4=B8=BA=E8=B5=B0=E4=BA=86=20IAP=20?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=EF=BC=8C=E6=94=B9=E8=B5=B0=20CAS=20=E5=90=8E?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E5=8F=AF=E7=9B=B4=E6=8E=A5=E6=89=93?= =?UTF-8?q?=E5=BC=80=EF=BC=8C=E6=97=A0=E9=9C=80=E7=89=B9=E6=AE=8A=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=9B=E2=91=A5=20=E6=96=B0=E5=A2=9E=20login=5Flntu?= =?UTF-8?q?=5Ftest.go=EF=BC=88=E5=85=A5=E5=8F=A3=E8=AE=A1=E7=AE=97=207=20?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=20+=20ampUrl=20=E8=A6=86=E7=9B=96=20+=20?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E5=B8=B8=E9=87=8F=EF=BC=89=EF=BC=8Cgo=20test?= =?UTF-8?q?=20./...=E3=80=81go=20vet=20=E5=85=A8=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backed/config.toml.example | 2 +- backed/internal/service/cpdaily/login_lntu.go | 43 +++++++++ .../service/cpdaily/login_lntu_test.go | 91 +++++++++++++++++++ backed/internal/service/cpdaily/service.go | 6 ++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 backed/internal/service/cpdaily/login_lntu.go create mode 100644 backed/internal/service/cpdaily/login_lntu_test.go diff --git a/backed/config.toml.example b/backed/config.toml.example index 5bfd5ed..9c32ae6 100644 --- a/backed/config.toml.example +++ b/backed/config.toml.example @@ -31,7 +31,7 @@ length = 4 appVersion = "9.8.8" systemVersion = "iOS 17" calVersion = "first_v4" -sliderIds = ["1018764144934965", "sdhyxy", "hqdx"] +sliderIds = ["1018764144934965", "sdhyxy", "hqdx", "lntu"] captchaBaseUrl = "http://127.0.0.1:8890" captchaApiKey = "your_captcha_api_key" diff --git a/backed/internal/service/cpdaily/login_lntu.go b/backed/internal/service/cpdaily/login_lntu.go new file mode 100644 index 0000000..3b9145c --- /dev/null +++ b/backed/internal/service/cpdaily/login_lntu.go @@ -0,0 +1,43 @@ +package cpdaily + +import ( + "context" + "net/http" + "strings" +) + +// ========== 辽宁工程技术大学(lntu)单独适配 ========== + +// LntuSchoolId 辽宁工程技术大学 tenant id +const LntuSchoolId = "lntu" + +// lntuCasLoginEntry 计算辽宁工程技术大学 CAS 登录入口: +// 网关域 fdm.lntu.edu.cn 的 /iap(school.IdsUrl)会 302 到学校 CAS +// (authserver.lntu.edu.cn/authserver/login),service 参数携带网关回调 +// /iap/loginSuccess?sessionToken=...;走完 CAS 账密后回调该地址, +// cookie 才能正确落在 fdm.lntu.edu.cn 域(wec-* 任务接口所在域)。 +func lntuCasLoginEntry(school *SchoolDetailInfo) string { + if school == nil { + return "" + } + if school.IdsUrl != "" && strings.Contains(school.IdsUrl, "/iap") { + return school.IdsUrl + } + if school.WecloudUrl != "" { + return strings.TrimSuffix(school.WecloudUrl, "/") + "/iap" + } + return school.AmpUrl +} + +// lntuCasLogin 辽宁工程技术大学强制走 CAS: +// 平台接口返回 joinType=CLOUD(IsIap()=true),但网关 /iap/doLogin 不存在(HTTP 405), +// IAP 账密提交必然失败并报 FAIL_UPNOTMATCH("用户名或密码不匹配",误导用户); +// 实际链路为 IAP 入口(fdm.lntu.edu.cn/iap)302 → 学校 CAS 账密 → 回调 +// /iap/loginSuccess 建立网关会话。因此强制走 CAS 流程(同许昌学院 xcu 模式)。 +func lntuCasLogin(ctx context.Context, school *SchoolDetailInfo, username, password string) ([]*http.Cookie, *MFAContext, error) { + lntu := *school + if entry := lntuCasLoginEntry(school); entry != "" { + lntu.AmpUrl = entry + } + return casLogin(ctx, &lntu, username, password) +} diff --git a/backed/internal/service/cpdaily/login_lntu_test.go b/backed/internal/service/cpdaily/login_lntu_test.go new file mode 100644 index 0000000..249ec00 --- /dev/null +++ b/backed/internal/service/cpdaily/login_lntu_test.go @@ -0,0 +1,91 @@ +package cpdaily + +import "testing" + +// TestLntuCasLoginEntry 覆盖辽宁工程技术大学 CAS 登录入口计算逻辑 +func TestLntuCasLoginEntry(t *testing.T) { + cases := []struct { + name string + school *SchoolDetailInfo + want string + }{ + { + name: "正常idsUrl(/iap 网关入口)", + school: &SchoolDetailInfo{IdsUrl: "https://fdm.lntu.edu.cn/iap"}, + want: "https://fdm.lntu.edu.cn/iap", + }, + { + name: "idsUrl缺失回退wecloud+/iap", + school: &SchoolDetailInfo{WecloudUrl: "https://fdm.lntu.edu.cn"}, + want: "https://fdm.lntu.edu.cn/iap", + }, + { + name: "wecloud带尾斜杠", + school: &SchoolDetailInfo{WecloudUrl: "https://fdm.lntu.edu.cn/"}, + want: "https://fdm.lntu.edu.cn/iap", + }, + { + name: "idsUrl非iap路径回退wecloud", + school: &SchoolDetailInfo{IdsUrl: "https://ids.lntu.edu.cn/authserver", WecloudUrl: "https://fdm.lntu.edu.cn"}, + want: "https://fdm.lntu.edu.cn/iap", + }, + { + name: "均缺失回退ampUrl", + school: &SchoolDetailInfo{AmpUrl: "https://fdm.lntu.edu.cn/wec-portal-mobile/client"}, + want: "https://fdm.lntu.edu.cn/wec-portal-mobile/client", + }, + { + name: "均为空", + school: &SchoolDetailInfo{}, + want: "", + }, + { + name: "nil学校", + school: nil, + want: "", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := lntuCasLoginEntry(c.school); got != c.want { + t.Errorf("lntuCasLoginEntry() = %q, want %q", got, c.want) + } + }) + } +} + +// TestLntuCasLoginEntryOverridesAmpUrl 确认 lntuCasLogin 用网关入口覆盖 ampUrl 后走 CAS 流程 +func TestLntuCasLoginEntryOverridesAmpUrl(t *testing.T) { + school := &SchoolDetailInfo{ + ID: LntuSchoolId, + Name: "辽宁工程技术大学", + JoinType: "CLOUD", // 平台标记为 CLOUD,但 IAP 不可用,需强制 CAS + IdsUrl: "https://fdm.lntu.edu.cn/iap", + WecloudUrl: "https://fdm.lntu.edu.cn", + AmpUrl: "https://fdm.lntu.edu.cn/wec-portal-mobile/client", + } + // 模拟 lntuCasLogin 内部的入口覆盖(不发起网络请求) + clone := *school + clone.AmpUrl = lntuCasLoginEntry(school) + if got := getCasValidLoginUrl(&clone); got != "https://fdm.lntu.edu.cn/iap" { + t.Errorf("getCasValidLoginUrl() = %q, want 网关 /iap 入口", got) + } + // 原对象不被修改 + if school.AmpUrl != "https://fdm.lntu.edu.cn/wec-portal-mobile/client" { + t.Errorf("原 school 对象 ampUrl 被意外修改: %q", school.AmpUrl) + } +} + +// TestLntuIsSliderCaptchaSchool 确认 lntu 归入滑块验证码学校需通过配置声明 +// (captchaSwitch=2 + createSliderCaptcha → /common/toSliderCaptcha.htl,与 hqdx 同协议) +func TestLntuSliderProtocolSameAsHqu(t *testing.T) { + // 协议一致性由配置驱动:lntu 与 hqdx 共用 resolveWiseduSliderCaptcha + //(openSliderCaptcha.htl 取图 → 打码 → verifySliderCaptcha.htl sign 提交)。 + // 此处仅断言常量存在且不与其他学校 ID 冲突。 + if LntuSchoolId == "" || LntuSchoolId != "lntu" { + t.Errorf("LntuSchoolId = %q, want lntu", LntuSchoolId) + } + if HquSchoolId == LntuSchoolId { + t.Errorf("lntu 与 hqdx 学校 ID 冲突") + } +} diff --git a/backed/internal/service/cpdaily/service.go b/backed/internal/service/cpdaily/service.go index a0ad265..50baae0 100644 --- a/backed/internal/service/cpdaily/service.go +++ b/backed/internal/service/cpdaily/service.go @@ -176,6 +176,12 @@ func CpdailyLogin(ctx context.Context, school *SchoolDetailInfo, username, passw if school.ID == XcuSchoolId { return xcuCasLogin(ctx, school, username, password) } + // 辽宁工程技术大学:平台配置 joinType=CLOUD(IsIap=true),但网关 /iap/doLogin 不存在(405), + // IAP 提交报 FAIL_UPNOTMATCH 误导为"密码错误";实际链路为 /iap 302 → 学校 CAS(authserver.lntu.edu.cn) + // → 回调 /iap/loginSuccess 建立网关会话,同 xcu 模式单独强制走 CAS(详见 login_lntu.go) + if school.ID == LntuSchoolId { + return lntuCasLogin(ctx, school, username, password) + } // 华侨大学:写死强制走通用 CAS 流程——CAS 账密登录需 wisedu 定制滑块验证码 //(casLogin 内按 sliderIds 配置分流,见 slider.go),IAP 账密接口不兼容(FAIL_UPNOTMATCH) if school.ID == HquSchoolId {