v1.2.50:辽宁工程技术大学(lntu)登录适配——强制走 CAS + 滑块验证码——① 根因:平台接口返回 joinType=CLOUD(IsIap=true),但网关 /iap/doLogin 不存在(HTTP 405),IAP 账密提交必然失败并返回 FAIL_UPNOTMATCH("用户名或密码不匹配",误导用户以为密码错误);真实链路为 fdm.lntu.edu.cn/iap 302 → 学校 CAS(authserver.lntu.edu.cn/authserver/login)→ 账密 + 验证码 → 回调 /iap/loginSuccess?sessionToken=...&ticket=... 建立网关会话(cookie 落在 fdm.lntu.edu.cn,与 wec-* 任务接口同域);② 修复:新增 login_lntu.go 的 lntuCasLogin(同 xcu 模式,用 /iap 网关入口覆盖 ampUrl 后走通用 CAS 流程),CpdailyLogin 中对 LntuSchoolId 特判强制走 CAS;③ 验证码类型:抓取登录页 JS 确认 captchaSwitch="2" 且 startLogin 走 createSliderCaptcha()(wisedu 定制滑块,非图形验证码),协议与华侨大学完全同源(openSliderCaptcha.htl 取图、sign=AES(JSON.stringify({canvasLength,moveLength,tracks}), 小图 base64 末16字节) 提交 verifySliderCaptcha.htl、errorCode==1 通过),故仅需将 lntu 加入 sliderIds 配置即可复用现有滑块实现(config.toml.example 同步更新);④ 端到端实测:真实待适配账号两次登录均一次通过(gapX=92/149 首候选 offset=0 直接命中 errorCode=1),CAS 302 → 6 cookies(CASTGC/JSESSIONID/MOD_AUTH_CAS/happyVoyage/route/platformMultilingual)→ FetchUserProfile 会话有效;⑤ 按用户要求回滚此前 LoginUrl(忘记密码地址)改动——原地址打不开是因为走了 IAP 入口,改走 CAS 后浏览器可直接打开,无需特殊处理;⑥ 新增 login_lntu_test.go(入口计算 7 用例 + ampUrl 覆盖 + 协议常量),go test ./...、go vet 全过
ci/woodpecker/tag/build Pipeline was successful

This commit is contained in:
becomer
2026-09-14 23:12:06 +08:00
parent 021ac7f7bd
commit 42d3b332a5
4 changed files with 141 additions and 1 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ length = 4
appVersion = "9.8.8" appVersion = "9.8.8"
systemVersion = "iOS 17" systemVersion = "iOS 17"
calVersion = "first_v4" calVersion = "first_v4"
sliderIds = ["1018764144934965", "sdhyxy", "hqdx"] sliderIds = ["1018764144934965", "sdhyxy", "hqdx", "lntu"]
captchaBaseUrl = "http://127.0.0.1:8890" captchaBaseUrl = "http://127.0.0.1:8890"
captchaApiKey = "your_captcha_api_key" captchaApiKey = "your_captcha_api_key"
@@ -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)
}
@@ -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 冲突")
}
}
@@ -176,6 +176,12 @@ func CpdailyLogin(ctx context.Context, school *SchoolDetailInfo, username, passw
if school.ID == XcuSchoolId { if school.ID == XcuSchoolId {
return xcuCasLogin(ctx, school, username, password) 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 定制滑块验证码 // 华侨大学:写死强制走通用 CAS 流程——CAS 账密登录需 wisedu 定制滑块验证码
//(casLogin 内按 sliderIds 配置分流,见 slider.go),IAP 账密接口不兼容(FAIL_UPNOTMATCH) //(casLogin 内按 sliderIds 配置分流,见 slider.go),IAP 账密接口不兼容(FAIL_UPNOTMATCH)
if school.ID == HquSchoolId { if school.ID == HquSchoolId {