topfans/backend/scripts/loadgen/seed/tokens.go
zerosaturation 293c7b14ae fix(security): JWT key governance — MustInit fail-fast + atomic.Value
- pkg/jwt: 删 public SetSecret; 加 MustInit(secret string) 启动时强制注入,
  缺/为空/等于弱默认值时返回 error(运行期不可再改); 密钥用 atomic.Value
  存 []byte,所有读走 mustSecret() 原子 Load,消除 SetSecret/ParseToken 并发
  data race(go test -race 零告警)。
- gateway main + auth_provider: 启动时 MustInit 读 JWT_SECRET env,失败 fatal。
- scripts/loadgen/seed/tokens: 同步 MustInit。
- .env.example: JWT_SECRET 改为 ≥32 字节 base64 示例(原为空,被 MustInit
  立即拒);注释提示生产 MUST replace。
- 测试: 4 个 MustInit 行为 + 1 个 50-goroutine race 覆盖。
- 行为变更: 任何 .env 缺 JWT_SECRET 或用占位 secret 的服务,启动会 panic
  (这是 fail-fast 期望行为);其余 4 个 .env 文件占位由 ops 单独轮换。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 18:50:12 +08:00

102 lines
2.4 KiB
Go

package main
import (
"encoding/csv"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/lib/pq"
"github.com/topfans/backend/pkg/jwt"
)
type TestUser struct {
UserID int64
Mobile string
AssetIDs pq.Int64Array
ExhibitionIDs pq.Int64Array
}
func GenerateTokensForLoadtest(cfg *Config) error {
if err := jwt.MustInit(cfg.JWTSecret); err != nil {
return fmt.Errorf("jwt.MustInit failed: %w", err)
}
db, err := openDB(cfg)
if err != nil {
return err
}
defer db.Close()
rows, err := db.Query(`
SELECT u.id, u.mobile,
COALESCE(array_agg(DISTINCT a.id) FILTER (WHERE a.id IS NOT NULL), '{}'::int8[]),
COALESCE(array_agg(DISTINCT e.id) FILTER (WHERE e.id IS NOT NULL), '{}'::int8[])
FROM users u
LEFT JOIN assets a ON a.owner_uid = u.id AND a.star_id = $1
LEFT JOIN fan_profiles fp ON fp.user_id = u.id AND fp.star_id = $1
LEFT JOIN booth_slots bs ON bs.host_profile_id = fp.id AND bs.slot_index IN (1, 2)
LEFT JOIN exhibitions e ON e.slot_id = bs.slot_id AND e.occupier_star_id = $1
WHERE u.id BETWEEN $2 AND $3
GROUP BY u.id, u.mobile
ORDER BY u.id
`, LoadtestStarID, LoadtestUserMin, LoadtestUserMax)
if err != nil {
return err
}
defer rows.Close()
var users []TestUser
for rows.Next() {
var u TestUser
if err := rows.Scan(&u.UserID, &u.Mobile, &u.AssetIDs, &u.ExhibitionIDs); err != nil {
return err
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return err
}
f, err := os.Create("users.csv")
if err != nil {
return err
}
defer f.Close()
w := csv.NewWriter(f)
defer w.Flush()
if err := w.Write([]string{"phone", "password", "user_id", "star_id", "jwt_token", "asset_ids", "exhibition_ids"}); err != nil {
return err
}
now := time.Now().UnixMilli()
for _, u := range users {
token, err := jwt.GenerateToken(u.UserID, LoadtestStarID, now)
if err != nil {
return err
}
if err := w.Write([]string{
u.Mobile, "Test@123",
strconv.FormatInt(u.UserID, 10),
"999900", token,
joinInt64([]int64(u.AssetIDs)),
joinInt64([]int64(u.ExhibitionIDs)),
}); err != nil {
return err
}
}
fmt.Printf("✅ users.csv written: %d rows\n", len(users))
return nil
}
func joinInt64(s []int64) string {
parts := make([]string, len(s))
for i, v := range s {
parts[i] = strconv.FormatInt(v, 10)
}
return strings.Join(parts, ";")
}