手机号验证码登录的账号没有 raw_password,cookie 过期后只能靠长期凭证 续期。但 RefreshCasCookie 只发 AmpCookies 头、未把长期凭证 CASTGC(TGC) 预置进客户端,且不跟随 CAS 重定向,导致请求被 302 到登录页,续期必然失败 (表现为「解析续期响应失败: unexpected end of JSON input」),用户被误判 为登录失效。 对齐 ExchangeCasCookie:预置 CASTGC/sessionToken/tenantId 等 cookie,并 跟随 CAS 重定向(跨跳保留原请求头),使网关能换回 MOD_AUTH_CAS。 新增 sms_login_test.go:以 mock 网关覆盖「带 CASTGC 续期成功」与 「缺 sessionToken 直接报错」;旧代码下前者复现同一失败,修复后通过。
This commit is contained in:
@@ -311,6 +311,50 @@ func RefreshCasCookie(ctx context.Context, cookieMap map[string]string, host str
|
||||
}
|
||||
|
||||
client := newSmsClient()
|
||||
// 与 ExchangeCasCookie 保持一致:跟随 CAS 重定向,且跨跳时保留原请求头
|
||||
// (否则 UPDATE 网关 302 到 /iap/login 后不跟随,或跟随中丢失 AmpCookies/SessionToken,
|
||||
// 都会落到登录页 HTML,无法换回 MOD_AUTH_CAS)。
|
||||
client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 50 {
|
||||
return fmt.Errorf("重定向次数过多")
|
||||
}
|
||||
for key, val := range via[0].Header {
|
||||
if !strings.EqualFold(key, "Cookie") {
|
||||
req.Header[key] = val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
// 关键:把长期凭证(CASTGC 等)连同基础 cookie 预置进 jar。
|
||||
// 手机号登录时 CASTGC(TGC)会随登录响应一起保存,是真正的长期 SSO 凭证;
|
||||
// 仅凭 sessionToken 头(AmpCookies)无法续期——网关会 302 到登录页。
|
||||
// 此前这里未预置 CASTGC,导致手机号登录账号一旦 MOD_AUTH_CAS 过期便无法自动续期。
|
||||
presetCookies := map[string]string{
|
||||
"clientType": "cpdaily_student",
|
||||
"standAlone": "0",
|
||||
"sessionToken": sessionToken,
|
||||
}
|
||||
if v := cookieMap["tenantId"]; v != "" {
|
||||
presetCookies["tenantId"] = v
|
||||
}
|
||||
if v := cookieMap["CASTGC"]; v != "" {
|
||||
presetCookies["CASTGC"] = v
|
||||
}
|
||||
targetSchoolURL, err := url.Parse(strings.TrimSuffix(host, "/"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析学校地址失败: %w", err)
|
||||
}
|
||||
targetAPIURL, _ := url.Parse("https://api.campushoy.com")
|
||||
for name, value := range presetCookies {
|
||||
c := &http.Cookie{Name: name, Value: value}
|
||||
if jar := client.GetClient().Jar; jar != nil {
|
||||
jar.SetCookies(targetSchoolURL, []*http.Cookie{c})
|
||||
if targetAPIURL != nil {
|
||||
jar.SetCookies(targetAPIURL, []*http.Cookie{c})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ampCookieMap := map[string]any{
|
||||
"AMP2": []map[string]any{
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package cpdaily
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 复刻学校网关行为:未携带有效 CASTGC 时,getImUserToken 会 302 到 CAS 登录页;
|
||||
// 携带 CASTGC 时,CAS 登录页换发一次性 ST 并回调原 service(下发 MOD_AUTH_CAS)。
|
||||
// 用于验证 RefreshCasCookie 是否(1)把长期凭证 CASTGC 预置进客户端、(2)跟随重定向。
|
||||
func newRefreshMockServer(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
const validTGT = "iap-test-TGT-abc"
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
// CAS 登录入口:带有效 TGT 则换发 ST 回调 service;否则踢回登录页
|
||||
case r.URL.Path == "/iap/login":
|
||||
service := r.URL.Query().Get("service")
|
||||
if c, err := r.Cookie("CASTGC"); err == nil && c.Value == validTGT {
|
||||
w.Header().Set("Location", service+"?ticket=ST-iap:test:ST:x:20260101120000")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Location", "/iap/login/pc.html")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
// service 回调:带 ticket 时下发 MOD_AUTH_CAS(模拟 CAS 建立会话)
|
||||
case r.URL.Path == "/wec-im-group/group/imUserToken/getImUserToken":
|
||||
if strings.HasPrefix(r.URL.Query().Get("ticket"), "ST-iap:") {
|
||||
http.SetCookie(w, &http.Cookie{Name: "MOD_AUTH_CAS", Value: r.URL.Query().Get("ticket"), Path: "/"})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"errCode":0,"errMsg":"","data":{}}`))
|
||||
return
|
||||
}
|
||||
// 无有效会话:302 到 CAS 登录入口
|
||||
w.Header().Set("Location", "/iap/login?service="+r.URL.Path)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// 携带 CASTGC 时应能续期成功并拿回 MOD_AUTH_CAS。
|
||||
// 修复前 RefreshCasCookie 未预置 CASTGC,会落到登录页、解析 HTML 失败。
|
||||
func TestRefreshCasCookiePresetsCASTGC(t *testing.T) {
|
||||
srv := newRefreshMockServer(t)
|
||||
defer srv.Close()
|
||||
|
||||
cookies, err := RefreshCasCookie(context.Background(), map[string]string{
|
||||
"sessionToken": "st-test",
|
||||
"tenantId": "lzufe",
|
||||
"CASTGC": "iap-test-TGT-abc",
|
||||
}, srv.URL, "device-1")
|
||||
if err != nil {
|
||||
t.Fatalf("续期应成功,实际报错: %v", err)
|
||||
}
|
||||
hasMOD := false
|
||||
for _, c := range cookies {
|
||||
if c.Name == "MOD_AUTH_CAS" {
|
||||
hasMOD = true
|
||||
}
|
||||
}
|
||||
if !hasMOD {
|
||||
t.Fatalf("续期后应取得 MOD_AUTH_CAS,实际 cookies: %+v", cookies)
|
||||
}
|
||||
}
|
||||
|
||||
// 缺少 sessionToken 时保持既有保护:直接报错,不发请求。
|
||||
func TestRefreshCasCookieRequiresSessionToken(t *testing.T) {
|
||||
if _, err := RefreshCasCookie(context.Background(), map[string]string{
|
||||
"CASTGC": "iap-test-TGT-abc",
|
||||
}, "https://school.campusphere.net", "device-1"); err == nil {
|
||||
t.Fatal("缺少 sessionToken 应报错")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user