🐛 修复淮南联合大学登录功能

This commit is contained in:
sugarBecomer
2025-12-27 14:36:16 +08:00
parent a8f4a2afa6
commit 6f25652183
6 changed files with 235 additions and 2 deletions
+2 -2
View File
@@ -19,8 +19,8 @@ balanceMigrateUrl = "http://localhost:3200/balance-migrate/"
mode = "dev" mode = "dev"
[captcha] [captcha]
baseUrl = "http://captcha-resolver.out" #baseUrl = "http://captcha-resolver.out"
#baseUrl = "http://localhost:8890" baseUrl = "http://localhost:8890"
[redis] [redis]
url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379" url = "redis://:jhkdjhkjdhsIUTYURTU_pEGmFP@database.out:56379"
@@ -51,6 +51,24 @@ func ApiAutoOCRVerification(bytes []byte) string {
result := post.String() result := post.String()
return gjson.Get(result, "data").String() return gjson.Get(result, "data").String()
} }
func ApiAutoCalcVerification(b64 string) string {
ctx := gctx.GetInitCtx()
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/calc"
client := httpclient.Cpdaily.Clone(ctx)
client.SetDoNotParseResponse(false)
post, err := client.R().SetBody(map[string]any{
"base64": b64,
}).Post(baseUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("AutoOCRVerification", baseUrl).
WithContext(ctx).
Wrap(err)
})
result := post.String()
return gjson.Get(result, "data").String()
}
func Bytes2Base64(data []byte) string { func Bytes2Base64(data []byte) string {
return base64.StdEncoding.EncodeToString(data) return base64.StdEncoding.EncodeToString(data)
} }
@@ -22,6 +22,8 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
// 河北石油职业技术大学 // 河北石油职业技术大学
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a": case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
return NewIap(ctx) return NewIap(ctx)
case "676c9395-a29c-4068-97a8-b48e05dae499":
return NewHnuu(ctx)
default: default:
if info.IsIap() { if info.IsIap() {
return NewIap(ctx) return NewIap(ctx)
+180
View File
@@ -0,0 +1,180 @@
package login
import (
"bytes"
"context"
"fmt"
"io"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"github.com/PuerkitoBio/goquery"
"github.com/samber/oops"
"resty.dev/v3"
)
var RSA_KEY_MAP = map[string]string{
"public_exponent": "010001",
"private_exponent": "413798867d69babed22e0dd3d4031c635f3e9dbca0fa50a32974a0e230787b7f7ba78caefbee828a051c690357a8cc31dba8efc738b4db22e887571ef1ec5a5a55b6d866f6a67527f6a7d78a127c9f687008bb540228b50aa2d1ca5a4ff71107234f936b611ac46432a26da9c302eaa7180820df70593353b3f8c0247fe97a45",
"modulus": "00b5eeb166e069920e80bebd1fea4829d3d1f3216f2aabe79b6c47a3c18dcee5fd22c2e7ac519cab59198ece036dcf289ea8201e2a0b9ded307f8fb704136eaeb670286f5ad44e691005ba9ea5af04ada5367cd724b5a26fdb5120cc95b6431604bd219c6b7d83a6f8f24b43918ea988a76f93c333aa5a20991493d4eb1117e7b1",
}
// 淮南联合大学登录实现 RSA加密 & 验证码必出(加法验证码)
type CpdailyHnuuLogin struct {
client *resty.Client
}
func (c CpdailyHnuuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
jar := cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
// 获取合法的登录URL
loginUrl := getCasValidLoginUrl(schoolDetail)
// 请求登录页面并获取真正的登录URL
loginPageResponse, err := c.client.R().
WithContext(ctx).
Get(loginUrl)
utils.OkOrPanic(err)
defer loginPageResponse.Body.Close()
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
utils.OkOrPanic(err)
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
// 解析登录页面
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
utils.OkOrPanic(err)
// 判断登录类型
var (
allInputs = map[string]string{}
formNode = loginPage.Find(".ant-form").First()
)
inputs := formNode.Find("input")
for _, input := range inputs.EachIter() {
name, exists := input.Attr("name")
if !exists {
continue
}
value, _ := input.Attr("value")
allInputs[name] = value
}
allInputs["username"] = username
pair := cryptoutil.NewNutRSAKeyPair(RSA_KEY_MAP["public_exponent"], "", RSA_KEY_MAP["modulus"])
// 处理密码
allInputs["password"] = pair.EncryptedString(password)
// 解决验证码
// 解决常规图片验证码
captchaUrl := getHnuuCaptchaUrl(loginResponseUrl)
code, uid := ResolveCalcCaptcha(ctx, captchaUrl, c.client)
allInputs["code"] = code
allInputs["id"] = uid
jar = cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar)
// 登录提交
loginPostResponse, err := c.client.R().
SetFormData(allInputs).
WithContext(ctx).
Post(getHnuuLoginPostUrl(loginResponseUrl))
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
defer loginPostResponse.Body.Close()
type LoginResponse struct {
Meta struct {
Success bool `json:"success"`
StatusCode int `json:"statusCode"`
Message string `json:"message"`
} `json:"meta"`
Data struct {
Code string `json:"code"`
} `json:"data"`
Tgt string `json:"tgt"`
Ticket string `json:"ticket"`
}
json := utils.ReadJson[LoginResponse](loginPostResponse.Body)
if json.Tgt != "" {
handleHnuuLoginSuccess(ctx, c.client, loginResponseUrl.Query().Get("service"), json.Ticket, json.Tgt, schoolDetail.WecloudUrl)
} else {
errMsg := ""
if json.Data.Code == "CODEFALSE" {
errMsg = "验证码错误"
} else if json.Data.Code == "FALSE" {
errMsg = "请检查账号密码是否正确"
} else if json.Data.Code != "PASSERROR" {
errMsg = "密码错误"
} else if json.Data.Code != "NOUSER" {
errMsg = "账号不存在"
} else if json.Data.Code != "USERLOCK" {
errMsg = "账号锁定"
} else {
errMsg = "未知错误" + json.Data.Code
}
utils.OkOrPanic(errors.NewBusinessError(500, errMsg))
}
}
func handleHnuuLoginSuccess(ctx context.Context, client *resty.Client, url, ticket string, tgt string, host string) {
redirectUrl := fmt.Sprintf("%s&ticket=%s", url, ticket)
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
response, err2 := client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get("https://hnuu.campusphere.net/iap/login")
utils.OkOrPanic(err2)
newUrl := response.RawResponse.Request.URL
ticketUrl := fmt.Sprintf("https://cas.hnuu.edu.cn/lyuapServer/v1/tickets/%s", tgt)
get, err := client.R().SetFormData(map[string]string{
"service": url,
"loginToken": "loginToken",
}).Post(ticketUrl)
utils.OkOrPanic(err)
defer get.Body.Close()
ticket = utils.ReadString(get.Body)
redirectUrl = fmt.Sprintf("%s&ticket=%s", newUrl.Query().Get("service"), ticket)
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
ActivateSession(ctx, host, client)
}
func getHnuuLoginPostUrl(url *url.URL) string {
// https://cas.hnuu.edu.cn/lyuapServer/v1/tickets
return fmt.Sprintf("%s://%s/lyuapServer/v1/tickets", url.Scheme, url.Host)
}
func getHnuuCaptchaUrl(url *url.URL) string {
// https://cas.hnuu.edu.cn/lyuapServer/kaptcha?_t=1766803822&uid=
return fmt.Sprintf("%s://%s/lyuapServer/kaptcha?_t=%d&uid=", url.Scheme, url.Host, utils.GetCurrentTimeMillis())
}
func (c CpdailyHnuuLogin) GetCookieJar() http.CookieJar {
return c.client.CookieJar()
}
func (c CpdailyHnuuLogin) GetCookieString() string {
cookieJar := c.GetCookieJar()
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
func (c CpdailyHnuuLogin) GetClient() *resty.Client {
return c.client
}
func NewHnuu(ctx context.Context) CpdailyLogin {
return &CpdailyHnuuLogin{
client: httpclient.NewCpdailyClient(),
}
}
+26
View File
@@ -15,6 +15,32 @@ func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) st
return captcha.ApiAutoOCRVerification(byteData) return captcha.ApiAutoOCRVerification(byteData)
} }
func ResolveCalcCaptcha(ctx context.Context, url string, client *resty.Client) (string, string) {
b64, uid := DownBase64(ctx, url, client)
return captcha.ApiAutoCalcVerification(b64), uid
}
func DownBase64(ctx context.Context, url string, client *resty.Client) (string, string) {
res, err := client.R().
WithContext(ctx).
Get(url)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("captchaUrl", url).
WithContext(ctx).
Wrap(err)
})
defer res.Body.Close()
type CaptchResponse struct {
KaptchaType string `json:"kaptchaType"`
UID string `json:"uid"`
Content string `json:"content"`
Timeout int `json:"timeout"`
}
json := utils.ReadJson[CaptchResponse](res.Body)
return json.Content, json.UID
}
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte { func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
res, err := client.R(). res, err := client.R().
WithContext(ctx). WithContext(ctx).
+7
View File
@@ -0,0 +1,7 @@
package utils
import "time"
func GetCurrentTimeMillis() int64 {
return time.Now().UnixMilli()
}