package fwh_pro import ( "encoding/json" "fmt" "fwh-toolbox-server/utils/httpClient" "strings" "time" "github.com/google/uuid" ) var loginInfo LoginData = LoginData{ UserId: 0, Token: "", } type LoginPayload struct { LoginName string `json:"loginName"` Password string `json:"password"` Type int `json:"type"` Timezone string `json:"timezone"` Lang string `json:"lang"` } type LoginData struct { UserId uint `json:"userId"` Token string `json:"token"` MfaRequired bool `json:"mfaRequired"` MfaToken string `json:"mfaToken"` MfaMethod string `json:"mfaMethod"` AvailableMfaMethods []string `json:"availableMfaMethods"` } type MfaVerifyPayload struct { MfaToken string `json:"mfaToken"` Code string `json:"code"` Method string `json:"method"` DeviceFingerprint string `json:"deviceFingerprint"` RememberDevice string `json:"rememberDevice"` Lang string `json:"lang"` } type LoginResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data LoginData `json:"data"` Success bool `json:"success"` } type JwtPayload struct { AdminLevel int `json:"adminLevel"` LoginName string `json:"loginName"` AccountType int `json:"accountType"` GroupId int `json:"groupId"` Iss string `json:"iss"` UserManage int `json:"userManage"` Exp uint64 `json:"exp"` UserName string `json:"userName"` Iat uint64 `json:"iat"` UserId int `json:"userId"` } func Login() error { return loginWithPost(postFwhLoginRequest, time.Now, uuid.NewString()) } type loginPostFunc func(path string, body any, clientID string) (*httpClient.Response, error) func loginWithPost(post loginPostFunc, now func() time.Time, clientID string) error { // A failed re-login must not leave a previously issued token available for reuse. ClearToken() payload := LoginPayload{ LoginName: fwhUser, Password: fwhPassword, Type: 1, Timezone: "Asia/Shanghai", Lang: "ZH_CN", } response, err := post("/api-user/user/login", payload, clientID) if err != nil { return err } var loginRes LoginResponse if err := json.Unmarshal(response.Body, &loginRes); err != nil { return err } if response.StatusCode != 200 || !loginRes.Success || loginRes.Code != 200 { return fmt.Errorf("FWH login rejected (HTTP %d, code %d)", response.StatusCode, loginRes.Code) } if loginRes.Data.MfaRequired { if fwhTotpSecret == "" { return fmt.Errorf("FWH login requires TOTP; set FWH_TOTP_SECRET") } if loginRes.Data.MfaToken == "" { return fmt.Errorf("FWH login requires TOTP but returned no mfaToken") } if !supportsTOTP(loginRes.Data) { return fmt.Errorf("FWH login requires an unsupported MFA method") } code, err := generateTOTP(fwhTotpSecret, now()) if err != nil { return err } verify := MfaVerifyPayload{ MfaToken: loginRes.Data.MfaToken, Code: code, Method: "TOTP", DeviceFingerprint: clientID, RememberDevice: "", Lang: "ZH_CN", } response, err = post("/api-user/mfa/login/verify", verify, clientID) if err != nil { return err } loginRes = LoginResponse{} if err := json.Unmarshal(response.Body, &loginRes); err != nil { return err } if response.StatusCode != 200 || !loginRes.Success || loginRes.Code != 200 { return fmt.Errorf("FWH TOTP verification rejected (HTTP %d, code %d)", response.StatusCode, loginRes.Code) } } if loginRes.Data.Token == "" { return fmt.Errorf("FWH login returned no token") } loginInfo.UserId = loginRes.Data.UserId loginInfo.Token = loginRes.Data.Token return nil } func supportsTOTP(data LoginData) bool { if strings.EqualFold(data.MfaMethod, "TOTP") { return true } for _, method := range data.AvailableMfaMethods { if strings.EqualFold(method, "TOTP") { return true } } return false } // 弃用:token payload被加密 // func isTokenExpired(token string) (bool, error) { // parts := strings.Split(token, ".") // if len(parts) != 3 { // return false, fmt.Errorf("token 格式错误") // } // payloadSegment := parts[1] // // JWT 使用 base64 URL encoding // payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadSegment) // if err != nil { // return false, err // } // var payload map[string]interface{} // if err := json.Unmarshal(payloadBytes, &payload); err != nil { // zap.L().Error("token payload解析失败", zap.String("payload", string(payloadBytes))) // return false, err // } // expFloat, ok := payload["exp"].(float64) // if !ok { // return false, fmt.Errorf("token payload 中没有 exp") // } // expTime := time.Unix(int64(expFloat), 0) // return time.Now().After(expTime), nil // } func getLoginToken() string { // 判断是否已经有token if loginInfo.Token != "" { // 判断token是否过期 // token.payload加密,无法判断过期,改为判断401 // expired, err := isTokenExpired(loginInfo.Token) // if err != nil { // zap.L().Error("loginInfo.Token 解析出错.", zap.String("token", loginInfo.Token), zap.String("err", err.Error())) // return "" // } // if expired { // zap.L().Info("fwh Token 已过期") // } else { // Token 有效 return loginInfo.Token // } } // 重新登录 Login() return loginInfo.Token } // 弃用:token payload被加密 // func getUserId(token string) (int, error) { // // JWT 由三部分组成: header.payload.signature // parts := strings.Split(token, ".") // if len(parts) < 2 { // return 0, fmt.Errorf("getUserId: invalid token") // } // // 解码 payload 部分 // payload, err := base64.RawURLEncoding.DecodeString(parts[1]) // if err != nil { // return 0, fmt.Errorf("getUserId: %s", err.Error()) // } // // 解析 JSON // var claims JwtPayload // if err := json.Unmarshal(payload, &claims); err != nil { // return 0, fmt.Errorf("getUserId: %s", err.Error()) // } // return claims.UserId, nil // } func getUserId() uint { return loginInfo.UserId } // 清空token func ClearToken() { loginInfo.UserId = 0 loginInfo.Token = "" } // 过期重新登录 - 提供给httpClient func reLogin() string { // 重新登录 Login() return loginInfo.Token }