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 {