fwh_login.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package fwh_pro
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "fwh-toolbox-server/utils/httpClient"
  6. "strings"
  7. "time"
  8. "github.com/google/uuid"
  9. )
  10. var loginInfo LoginData = LoginData{
  11. UserId: 0,
  12. Token: "",
  13. }
  14. type LoginPayload struct {
  15. LoginName string `json:"loginName"`
  16. Password string `json:"password"`
  17. Type int `json:"type"`
  18. Timezone string `json:"timezone"`
  19. Lang string `json:"lang"`
  20. }
  21. type LoginData struct {
  22. UserId uint `json:"userId"`
  23. Token string `json:"token"`
  24. MfaRequired bool `json:"mfaRequired"`
  25. MfaToken string `json:"mfaToken"`
  26. MfaMethod string `json:"mfaMethod"`
  27. AvailableMfaMethods []string `json:"availableMfaMethods"`
  28. }
  29. type MfaVerifyPayload struct {
  30. MfaToken string `json:"mfaToken"`
  31. Code string `json:"code"`
  32. Method string `json:"method"`
  33. DeviceFingerprint string `json:"deviceFingerprint"`
  34. RememberDevice string `json:"rememberDevice"`
  35. Lang string `json:"lang"`
  36. }
  37. type LoginResponse struct {
  38. Code int `json:"code"`
  39. Msg string `json:"msg"`
  40. Data LoginData `json:"data"`
  41. Success bool `json:"success"`
  42. }
  43. type JwtPayload struct {
  44. AdminLevel int `json:"adminLevel"`
  45. LoginName string `json:"loginName"`
  46. AccountType int `json:"accountType"`
  47. GroupId int `json:"groupId"`
  48. Iss string `json:"iss"`
  49. UserManage int `json:"userManage"`
  50. Exp uint64 `json:"exp"`
  51. UserName string `json:"userName"`
  52. Iat uint64 `json:"iat"`
  53. UserId int `json:"userId"`
  54. }
  55. func Login() error {
  56. return loginWithPost(postFwhLoginRequest, time.Now, uuid.NewString())
  57. }
  58. type loginPostFunc func(path string, body any, clientID string) (*httpClient.Response, error)
  59. func loginWithPost(post loginPostFunc, now func() time.Time, clientID string) error {
  60. // A failed re-login must not leave a previously issued token available for reuse.
  61. ClearToken()
  62. payload := LoginPayload{
  63. LoginName: fwhUser,
  64. Password: fwhPassword,
  65. Type: 1,
  66. Timezone: "Asia/Shanghai",
  67. Lang: "ZH_CN",
  68. }
  69. response, err := post("/api-user/user/login", payload, clientID)
  70. if err != nil {
  71. return err
  72. }
  73. var loginRes LoginResponse
  74. if err := json.Unmarshal(response.Body, &loginRes); err != nil {
  75. return err
  76. }
  77. if response.StatusCode != 200 || !loginRes.Success || loginRes.Code != 200 {
  78. return fmt.Errorf("FWH login rejected (HTTP %d, code %d)", response.StatusCode, loginRes.Code)
  79. }
  80. if loginRes.Data.MfaRequired {
  81. if fwhTotpSecret == "" {
  82. return fmt.Errorf("FWH login requires TOTP; set FWH_TOTP_SECRET")
  83. }
  84. if loginRes.Data.MfaToken == "" {
  85. return fmt.Errorf("FWH login requires TOTP but returned no mfaToken")
  86. }
  87. if !supportsTOTP(loginRes.Data) {
  88. return fmt.Errorf("FWH login requires an unsupported MFA method")
  89. }
  90. code, err := generateTOTP(fwhTotpSecret, now())
  91. if err != nil {
  92. return err
  93. }
  94. verify := MfaVerifyPayload{
  95. MfaToken: loginRes.Data.MfaToken,
  96. Code: code,
  97. Method: "TOTP",
  98. DeviceFingerprint: clientID,
  99. RememberDevice: "",
  100. Lang: "ZH_CN",
  101. }
  102. response, err = post("/api-user/mfa/login/verify", verify, clientID)
  103. if err != nil {
  104. return err
  105. }
  106. loginRes = LoginResponse{}
  107. if err := json.Unmarshal(response.Body, &loginRes); err != nil {
  108. return err
  109. }
  110. if response.StatusCode != 200 || !loginRes.Success || loginRes.Code != 200 {
  111. return fmt.Errorf("FWH TOTP verification rejected (HTTP %d, code %d)", response.StatusCode, loginRes.Code)
  112. }
  113. }
  114. if loginRes.Data.Token == "" {
  115. return fmt.Errorf("FWH login returned no token")
  116. }
  117. loginInfo.UserId = loginRes.Data.UserId
  118. loginInfo.Token = loginRes.Data.Token
  119. return nil
  120. }
  121. func supportsTOTP(data LoginData) bool {
  122. if strings.EqualFold(data.MfaMethod, "TOTP") {
  123. return true
  124. }
  125. for _, method := range data.AvailableMfaMethods {
  126. if strings.EqualFold(method, "TOTP") {
  127. return true
  128. }
  129. }
  130. return false
  131. }
  132. // 弃用:token payload被加密
  133. // func isTokenExpired(token string) (bool, error) {
  134. // parts := strings.Split(token, ".")
  135. // if len(parts) != 3 {
  136. // return false, fmt.Errorf("token 格式错误")
  137. // }
  138. // payloadSegment := parts[1]
  139. // // JWT 使用 base64 URL encoding
  140. // payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadSegment)
  141. // if err != nil {
  142. // return false, err
  143. // }
  144. // var payload map[string]interface{}
  145. // if err := json.Unmarshal(payloadBytes, &payload); err != nil {
  146. // zap.L().Error("token payload解析失败", zap.String("payload", string(payloadBytes)))
  147. // return false, err
  148. // }
  149. // expFloat, ok := payload["exp"].(float64)
  150. // if !ok {
  151. // return false, fmt.Errorf("token payload 中没有 exp")
  152. // }
  153. // expTime := time.Unix(int64(expFloat), 0)
  154. // return time.Now().After(expTime), nil
  155. // }
  156. func getLoginToken() string {
  157. // 判断是否已经有token
  158. if loginInfo.Token != "" {
  159. // 判断token是否过期
  160. // token.payload加密,无法判断过期,改为判断401
  161. // expired, err := isTokenExpired(loginInfo.Token)
  162. // if err != nil {
  163. // zap.L().Error("loginInfo.Token 解析出错.", zap.String("token", loginInfo.Token), zap.String("err", err.Error()))
  164. // return ""
  165. // }
  166. // if expired {
  167. // zap.L().Info("fwh Token 已过期")
  168. // } else {
  169. // Token 有效
  170. return loginInfo.Token
  171. // }
  172. }
  173. // 重新登录
  174. Login()
  175. return loginInfo.Token
  176. }
  177. // 弃用:token payload被加密
  178. // func getUserId(token string) (int, error) {
  179. // // JWT 由三部分组成: header.payload.signature
  180. // parts := strings.Split(token, ".")
  181. // if len(parts) < 2 {
  182. // return 0, fmt.Errorf("getUserId: invalid token")
  183. // }
  184. // // 解码 payload 部分
  185. // payload, err := base64.RawURLEncoding.DecodeString(parts[1])
  186. // if err != nil {
  187. // return 0, fmt.Errorf("getUserId: %s", err.Error())
  188. // }
  189. // // 解析 JSON
  190. // var claims JwtPayload
  191. // if err := json.Unmarshal(payload, &claims); err != nil {
  192. // return 0, fmt.Errorf("getUserId: %s", err.Error())
  193. // }
  194. // return claims.UserId, nil
  195. // }
  196. func getUserId() uint {
  197. return loginInfo.UserId
  198. }
  199. // 清空token
  200. func ClearToken() {
  201. loginInfo.UserId = 0
  202. loginInfo.Token = ""
  203. }
  204. // 过期重新登录 - 提供给httpClient
  205. func reLogin() string {
  206. // 重新登录
  207. Login()
  208. return loginInfo.Token
  209. }