package auth import ( "crypto/rand" "encoding/hex" "errors" "strings" "time" "server/models" ) // ============================================================= // 移动端免登录票据 // // 背景:移动端用「密码直连」拿到的是 client_id=yz-uniapp、user_type=app 的令牌, // Web 端(管理后台)只认自己 localStorage 里 client_id=yz-backend 的令牌, // 两者登录态不共享,导致移动端打开 webview 时又要走一遍统一认证登录。 // // 方案:移动端令牌 → 一次性票据 → Web 端令牌。 // 票据本身是一次性的短时效凭证,即使泄露也无法二次使用。 // ============================================================= // AppTicketTTL 票据有效期。移动端拿到后应立即跳转,不需要长时效。 const AppTicketTTL = 60 * time.Second var ( ErrAppTicketInvalid = errors.New("登录票据无效") ErrAppTicketUsed = errors.New("登录票据已被使用") ErrAppTicketExpired = errors.New("登录票据已过期") ) // CreateAppTicket 签发一次性票据 func CreateAppTicket(identityID, tid uint64, clientID string) (string, error) { if identityID == 0 { return "", errors.New("身份无效") } b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } ticket := hex.EncodeToString(b) if clientID == "" { clientID = "yz-backend" } row := &models.AuthAppLoginTicket{ Ticket: ticket, IdentityID: identityID, Tid: tid, ClientID: clientID, ExpireAt: time.Now().Add(AppTicketTTL), } if _, err := models.Orm.Insert(row); err != nil { return "", err } return ticket, nil } // ConsumeAppTicket 核销一次性票据(CAS 防重放),返回票据携带的身份与租户。 func ConsumeAppTicket(ticket string) (*models.AuthAppLoginTicket, error) { ticket = strings.TrimSpace(ticket) if ticket == "" { return nil, ErrAppTicketInvalid } var row models.AuthAppLoginTicket if err := models.Orm.QueryTable(new(models.AuthAppLoginTicket)). Filter("ticket", ticket).One(&row); err != nil { return nil, ErrAppTicketInvalid } if row.UsedAt != nil { return nil, ErrAppTicketUsed } if row.ExpireAt.Before(time.Now()) { return nil, ErrAppTicketExpired } now := time.Now() affected, err := models.Orm.QueryTable(new(models.AuthAppLoginTicket)). Filter("id", row.ID). Filter("used_at__isnull", true). Update(map[string]interface{}{"used_at": now}) if err != nil { return nil, err } if affected == 0 { return nil, ErrAppTicketUsed } row.UsedAt = &now return &row, nil } // PurgeExpiredAppTickets 清理过期票据(防止表无限增长,由调用方定期触发) func PurgeExpiredAppTickets() (int64, error) { return models.Orm.QueryTable(new(models.AuthAppLoginTicket)). Filter("expire_at__lt", time.Now().Add(-24*time.Hour)). Delete() }