token_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package auth
  2. import (
  3. "regexp"
  4. "strings"
  5. "testing"
  6. )
  7. // 32 字节 base64url 无填充后固定 43 个字符。
  8. const b64Len = 43
  9. func TestRandomTokenFormat(t *testing.T) {
  10. seen := make(map[string]bool, 100)
  11. for i := 0; i < 100; i++ {
  12. tok, err := randomToken()
  13. if err != nil {
  14. t.Fatalf("randomToken() 返回错误: %v", err)
  15. }
  16. if len(tok) != b64Len {
  17. t.Fatalf("base64url(32 字节) 长度应为 %d,实际 %d (%q)", b64Len, len(tok), tok)
  18. }
  19. if !regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString(tok) {
  20. t.Fatalf("token 含 base64url 之外的字符: %q", tok)
  21. }
  22. if seen[tok] {
  23. t.Fatalf("生成了重复 token: %q", tok)
  24. }
  25. seen[tok] = true
  26. }
  27. }
  28. func TestSessionAndDeviceTokenUnique(t *testing.T) {
  29. a, err := newSessionToken()
  30. if err != nil {
  31. t.Fatal(err)
  32. }
  33. if !strings.HasPrefix(a, sessionTokenPrefix) {
  34. t.Errorf("会话 token 应以 %q 开头,实际 %q", sessionTokenPrefix, a)
  35. }
  36. b, err := newDeviceToken()
  37. if err != nil {
  38. t.Fatal(err)
  39. }
  40. if !strings.HasPrefix(b, deviceTokenPrefix) {
  41. t.Errorf("设备 token 应以 %q 开头,实际 %q", deviceTokenPrefix, b)
  42. }
  43. if a == b {
  44. t.Error("两次生成的 token 不应相同")
  45. }
  46. }
  47. func TestPendingTokenTrustDevice(t *testing.T) {
  48. t.Run("不带授信标记", func(t *testing.T) {
  49. plain, err := newPendingToken(false)
  50. if err != nil {
  51. t.Fatal(err)
  52. }
  53. if strings.HasSuffix(plain, pendingTrustSuffixOn) {
  54. t.Fatalf("未请求授信时不应带后缀: %q", plain)
  55. }
  56. token, trust := splitPendingToken(plain)
  57. if trust {
  58. t.Error("splitPendingToken 应返回 trustDevice=false")
  59. }
  60. if token != plain {
  61. t.Errorf("无后缀时应原样返回,实际 %q", token)
  62. }
  63. })
  64. t.Run("带授信标记", func(t *testing.T) {
  65. plain, err := newPendingToken(true)
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. if !strings.HasSuffix(plain, pendingTrustSuffixOn) {
  70. t.Fatalf("请求授信时应带后缀: %q", plain)
  71. }
  72. token, trust := splitPendingToken(plain)
  73. if !trust {
  74. t.Error("splitPendingToken 应返回 trustDevice=true")
  75. }
  76. if strings.HasSuffix(token, pendingTrustSuffixOn) {
  77. t.Errorf("剥离后不应残留后缀: %q", token)
  78. }
  79. if len(token) != len(pendingTokenPrefix)+b64Len {
  80. t.Errorf("剥离后长度异常: %d", len(token))
  81. }
  82. })
  83. }
  84. func TestPendingTokenHashIgnoresSuffix(t *testing.T) {
  85. // 同一个 token 是否请求授信,剥离后缀后哈希必须一致;
  86. // 不剥离直接哈希则不同 —— 这正是「先剥离再哈希」不可省略的原因。
  87. plain, err := newPendingToken(true)
  88. if err != nil {
  89. t.Fatal(err)
  90. }
  91. base, trust := splitPendingToken(plain)
  92. if !trust {
  93. t.Fatal("splitPendingToken 应识别出授信标记")
  94. }
  95. if HashToken(plain) == HashToken(base) {
  96. t.Error("未剥离后缀时哈希应不同,说明剥离步骤不可省略")
  97. }
  98. rebuilt := base + pendingTrustSuffixOn
  99. if stripped, _ := splitPendingToken(rebuilt); HashToken(stripped) != HashToken(base) {
  100. t.Error("带后缀的值剥离后应与 base 哈希一致")
  101. }
  102. }
  103. func TestPendingTokenSuffixStrippedBeforeHash(t *testing.T) {
  104. // 直接对带后缀的明文哈希会查不到记录,这是调用方必须遵守的顺序。
  105. if HashToken("tok:1") == HashToken("tok") {
  106. t.Error("带后缀与不带后缀的哈希应不同")
  107. }
  108. }
  109. func TestAPIToken(t *testing.T) {
  110. t.Run("read", func(t *testing.T) {
  111. token, prefix, err := newAPIToken("read")
  112. if err != nil {
  113. t.Fatal(err)
  114. }
  115. if !strings.HasPrefix(token, apiTokenReadPrefix) {
  116. t.Errorf("read token 应以 %q 开头,实际 %q", apiTokenReadPrefix, token)
  117. }
  118. if len(prefix) != tokenPrefixLen {
  119. t.Errorf("token_prefix 长度应为 %d,实际 %d (%q)", tokenPrefixLen, len(prefix), prefix)
  120. }
  121. if !strings.HasPrefix(token, prefix) {
  122. t.Errorf("token_prefix 应是明文前缀: prefix=%q token=%q", prefix, token)
  123. }
  124. })
  125. t.Run("read_write", func(t *testing.T) {
  126. token, _, err := newAPIToken("read_write")
  127. if err != nil {
  128. t.Fatal(err)
  129. }
  130. if !strings.HasPrefix(token, apiTokenWritePrefix) {
  131. t.Errorf("read_write token 应以 %q 开头,实际 %q", apiTokenWritePrefix, token)
  132. }
  133. })
  134. t.Run("非法 scope", func(t *testing.T) {
  135. if _, _, err := newAPIToken("admin"); err == nil {
  136. t.Error("非法 scope 应返回错误")
  137. }
  138. })
  139. }
  140. func TestHashToken(t *testing.T) {
  141. got := HashToken("hello")
  142. // 固定向量,防止哈希实现被意外改动
  143. const want = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
  144. if got != want {
  145. t.Errorf("HashToken(\"hello\") = %q,期望 %q", got, want)
  146. }
  147. if len(got) != 64 {
  148. t.Errorf("SHA-256 十六进制长度应为 64,实际 %d", len(got))
  149. }
  150. }
  151. func TestSecureEqual(t *testing.T) {
  152. if !secureEqual("abc", "abc") {
  153. t.Error("相同字符串应返回 true")
  154. }
  155. if secureEqual("abc", "abd") {
  156. t.Error("不同字符串应返回 false")
  157. }
  158. if secureEqual("abc", "abcd") {
  159. t.Error("不同长度应返回 false")
  160. }
  161. }
苏ICP备2026082406号
苏公网安备 待审核