token.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package auth
  2. import (
  3. "crypto/rand"
  4. "crypto/sha256"
  5. "crypto/subtle"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "fmt"
  9. "strings"
  10. )
  11. // 凭证生成与哈希工具。
  12. //
  13. // 依据 docs/backend/auth.md:
  14. // - 会话 / 授信设备 / 临时登录 token 都是「随机 32 字节,base64url 编码后下发,
  15. // 数据库只存 SHA-256 哈希」(auth.md 第三节规则 1、3.1 规则 1、第四节规则 1);
  16. // - API Token 格式为 haven_<scope>_<32字节随机串base64url>,同样只存哈希,
  17. // 另存明文前 8 位 token_prefix 用于展示(auth.md 第五节规则 2、3)。
  18. //
  19. // 注意:token_prefix 是明文前缀,不是哈希。它的唯一用途是设置页展示,
  20. // 鉴权时必须查 token_hash,不解析前缀(auth.md 第五节规则 2)。
  21. // tokenBytes 是各类 token 的随机字节数,固定 32。
  22. const tokenBytes = 32
  23. // tokenPrefixLen 是 tokens.token_prefix 的长度,即明文前 8 位。
  24. const tokenPrefixLen = 8
  25. // 固定前缀段,便于用户和日志识别 token 类型(不含敏感信息)。
  26. const (
  27. sessionTokenPrefix = "haven_sess_"
  28. deviceTokenPrefix = "haven_dev_"
  29. pendingTokenPrefix = "haven_pend_"
  30. apiTokenReadPrefix = "haven_ro_"
  31. apiTokenWritePrefix = "haven_rw_"
  32. pendingTrustSuffixOn = ":1"
  33. )
  34. // randomToken 生成 base64url 编码的随机串(32 字节)。
  35. func randomToken() (string, error) {
  36. buf := make([]byte, tokenBytes)
  37. if _, err := rand.Read(buf); err != nil {
  38. return "", fmt.Errorf("生成随机 token 失败: %w", err)
  39. }
  40. return base64.RawURLEncoding.EncodeToString(buf), nil
  41. }
  42. // newSessionToken 生成会话 token 明文。调用方只把明文写进 cookie,库里存 HashToken 结果。
  43. func newSessionToken() (string, error) {
  44. tok, err := randomToken()
  45. if err != nil {
  46. return "", err
  47. }
  48. return sessionTokenPrefix + tok, nil
  49. }
  50. // newDeviceToken 生成授信设备 token 明文(cookie haven_trusted_device)。
  51. func newDeviceToken() (string, error) {
  52. tok, err := randomToken()
  53. if err != nil {
  54. return "", err
  55. }
  56. return deviceTokenPrefix + tok, nil
  57. }
  58. // newPendingToken 生成待验证登录的临时 token 明文。
  59. //
  60. // trustDevice 表示第一步登录请求里的 trust_device。按 auth.md 3.1 规则 1,
  61. // 该标记拼接在明文末尾(如 <随机串>:1),不单独落库;校验时先剥离后缀再哈希。
  62. // 注意 suffix 必须在哈希之前剥离,所以临时 token 的哈希覆盖的是**去掉后缀后的值**。
  63. func newPendingToken(trustDevice bool) (string, error) {
  64. tok, err := randomToken()
  65. if err != nil {
  66. return "", err
  67. }
  68. out := pendingTokenPrefix + tok
  69. if trustDevice {
  70. out += pendingTrustSuffixOn
  71. }
  72. return out, nil
  73. }
  74. // splitPendingToken 剥离临时 token 末尾的 trust_device 标记,
  75. // 返回「用于哈希的原始值」与「是否请求授信设备」。
  76. //
  77. // 对不含后缀的值原样返回,trustDevice 为 false。这样即便调用方传入已剥离过的值也安全。
  78. func splitPendingToken(plain string) (token string, trustDevice bool) {
  79. if strings.HasSuffix(plain, pendingTrustSuffixOn) {
  80. return strings.TrimSuffix(plain, pendingTrustSuffixOn), true
  81. }
  82. return plain, false
  83. }
  84. // newAPIToken 按 scope 生成 API Token 明文,并返回展示用的前缀。
  85. //
  86. // scope 只允许 read / read_write,其余取值由调用方的校验层拦截(返回 422)。
  87. func newAPIToken(scope string) (token string, prefix string, err error) {
  88. var p string
  89. switch scope {
  90. case "read":
  91. p = apiTokenReadPrefix
  92. case "read_write":
  93. p = apiTokenWritePrefix
  94. default:
  95. return "", "", fmt.Errorf("非法的 token scope %q", scope)
  96. }
  97. tok, err := randomToken()
  98. if err != nil {
  99. return "", "", err
  100. }
  101. token = p + tok
  102. return token, truncatePrefix(token), nil
  103. }
  104. // truncatePrefix 取明文前 tokenPrefixLen 个字节作为展示前缀。
  105. //
  106. // 前缀段本身都是 ASCII,且长度固定,因此按字节截断不会切坏多字节字符。
  107. func truncatePrefix(token string) string {
  108. if len(token) <= tokenPrefixLen {
  109. return token
  110. }
  111. return token[:tokenPrefixLen]
  112. }
  113. // HashToken 计算 token 的 SHA-256 哈希(小写十六进制),用于落库与按 token_hash 查询。
  114. //
  115. // 对临时 token,调用方必须先经 splitPendingToken 剥离 trust_device 后缀再哈希。
  116. func HashToken(token string) string {
  117. sum := sha256.Sum256([]byte(token))
  118. return hex.EncodeToString(sum[:])
  119. }
  120. // secureEqual 用恒定时间比较两个字符串,避免通过耗时差异推断内容。
  121. func secureEqual(a, b string) bool {
  122. return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
  123. }
苏ICP备2026082406号
苏公网安备 待审核