package auth import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "sort" "strconv" "strings" "time" ) // Telegram Login Widget:没有授权码流程,官方脚本把用户资料连同 // hash 一起交给前端,前端 POST 到 /api/auth/telegram,后端用 bot // token 验签。算法见官方文档:secret = SHA256(bot_token), // data-check-string 是除 hash 外所有收到的字段按 key 排序的 "k=v" 行。 type Telegram struct { Bot string // bot 用户名(不含 @),下发给 widget Token string // bot token,只用于验签,不出后端 } func (t Telegram) Enabled() bool { return t.Bot != "" && t.Token != "" } // telegramAuthTTL 是验签窗口:widget 回传的 auth_date 超过它视为过期 const telegramAuthTTL = 24 * time.Hour // TelegramUser 是 widget 回传的身份字段(验签通过后从中取)。 // widget 把所有值都当字符串发(id 也不例外),但前端测试或手工 // 调用可能发数字 —— FlexStr 两种都收。 type TelegramUser struct { ID FlexStr `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` PhotoURL string `json:"photo_url"` } // FlexStr 兼容 JSON 里的字符串和数字 type FlexStr string func (f *FlexStr) UnmarshalJSON(b []byte) error { if len(b) > 0 && b[0] == '"' { var s string if err := json.Unmarshal(b, &s); err != nil { return err } *f = FlexStr(s) return nil } *f = FlexStr(b) return nil } func (f FlexStr) Int64() (int64, error) { return strconv.ParseInt(strings.Trim(string(f), `"`), 10, 64) } func (u TelegramUser) IDInt() int64 { id, _ := u.ID.Int64(); return id } func (u TelegramUser) DisplayName() string { name := strings.TrimSpace(u.FirstName + " " + u.LastName) if name == "" { name = u.Username } return name } // normalize 把 JSON 值规整成 widget 发送时的字符串形态 // (widget 的数值字段也是字符串,但调用方可能发真数字) func normalize(v any) string { switch x := v.(type) { case string: return x case float64: return strconv.FormatInt(int64(x), 10) case json.Number: return x.String() case bool: return strconv.FormatBool(x) default: return fmt.Sprint(x) } } // VerifyMap 校验 hash 与时效。fields 是前端原样 POST 的 JSON 对象; // exclude 里的 key(我们附加的非 Telegram 字段)不参与验签。 func (t Telegram) VerifyMap(fields map[string]any, exclude ...string) error { hash := normalize(fields["hash"]) if hash == "" { return fmt.Errorf("telegram: missing hash") } authDate, err := strconv.ParseInt(normalize(fields["auth_date"]), 10, 64) if err != nil || authDate == 0 || time.Since(time.Unix(authDate, 0)) > telegramAuthTTL { return fmt.Errorf("telegram: auth_date expired") } skip := make(map[string]bool, len(exclude)+1) skip["hash"] = true for _, k := range exclude { skip[k] = true } keys := make([]string, 0, len(fields)) for k := range fields { if !skip[k] { keys = append(keys, k) } } sort.Strings(keys) lines := make([]string, 0, len(keys)) for _, k := range keys { lines = append(lines, k+"="+normalize(fields[k])) } secret := sha256.Sum256([]byte(t.Token)) mac := hmac.New(sha256.New, secret[:]) mac.Write([]byte(strings.Join(lines, "\n"))) if !hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(strings.ToLower(hash))) { return fmt.Errorf("telegram: hash mismatch") } return nil }