middleware.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package auth
  2. import (
  3. "errors"
  4. "time"
  5. "github.com/gin-gonic/gin"
  6. )
  7. // 可见性过滤条件(auth.md 6.3)。
  8. //
  9. // 所有读接口必须使用这里算出的条件,不得各自拼 visibility 判断
  10. // (conventions.md 第四节、auth.md 6.3)。
  11. const (
  12. // VisibilityPublic 是「公开可见」的唯一判定条件。
  13. VisibilityPublic = `visibility = 'public' AND published_at IS NOT NULL`
  14. // VisibilityOwnedOrPublic 供已登录用户使用:自己的 OR 公开。
  15. // 使用方需把 $1 绑定为当前用户 id。
  16. VisibilityOwnedOrPublic = `(user_id = $1) OR (` + VisibilityPublic + `)`
  17. )
  18. // 注入 *gin.Context 的键。
  19. const (
  20. ctxKeyIdentity = "identity"
  21. ctxKeyVisibility = "visibility"
  22. )
  23. // Visibility 是中间件算出的数据可见性条件。
  24. //
  25. // Condition 是可直接拼进 WHERE 的 SQL 片段,Args 是对应的参数。
  26. // 私有读与写操作在此基础上还要叠加 user_id 过滤(auth.md 6.4)。
  27. type Visibility struct {
  28. // Condition 为空的含义是「不可见任何数据」,调用方应直接返回空列表或 404。
  29. Condition string
  30. Args []any
  31. }
  32. // Visibility 从 gin context 取出可见性条件。
  33. func CurrentVisibility(c *gin.Context) Visibility {
  34. if v, ok := c.Get(ctxKeyVisibility); ok {
  35. if vis, ok := v.(Visibility); ok {
  36. return vis
  37. }
  38. }
  39. return Visibility{}
  40. }
  41. // CurrentIdentity 从 gin context 取出身份,缺失时视为匿名。
  42. func CurrentIdentity(c *gin.Context) Identity {
  43. if v, ok := c.Get(ctxKeyIdentity); ok {
  44. if id, ok := v.(Identity); ok {
  45. return id
  46. }
  47. }
  48. return Identity{Kind: Anonymous}
  49. }
  50. // visibilityFor 按身份计算可见性条件(auth.md 6.3)。
  51. func visibilityFor(id Identity) Visibility {
  52. if !id.IsLoggedIn() {
  53. return Visibility{Condition: VisibilityPublic}
  54. }
  55. return Visibility{
  56. Condition: VisibilityOwnedOrPublic,
  57. Args: []any{id.UserID},
  58. }
  59. }
  60. // Authenticate 是身份识别中间件(auth.md 6.1)。
  61. //
  62. // 顺序:
  63. // 1. Authorization: Bearer 优先于 cookie,Token 匹配失败直接 401;
  64. // 2. 否则查会话 cookie;会话无效视为未登录,继续往后走;
  65. // 3. 否则匿名。
  66. //
  67. // 本中间件只识别身份并注入 context,不做接口级权限判定,
  68. // 权限由 RequireLogin / RequireWrite / RequireTotpVerified 负责(auth.md 6.2)。
  69. func Authenticate(store Store) gin.HandlerFunc {
  70. return func(c *gin.Context) {
  71. ctx := c.Request.Context()
  72. identity := Identity{Kind: Anonymous}
  73. if raw := bearerToken(c); raw != "" {
  74. tok, err := store.FindTokenByHash(ctx, HashToken(raw))
  75. switch {
  76. case errors.Is(err, ErrNotFound):
  77. // Token 无效:直接拒绝,不退回 cookie 身份(auth.md 6.1 第 1 步)
  78. abortUnauthorized(c)
  79. return
  80. case err != nil:
  81. abortInternal(c, err)
  82. return
  83. }
  84. if !tokenUsable(tok, time.Now()) {
  85. abortUnauthorized(c)
  86. return
  87. }
  88. identity = Identity{Kind: TokenUser, UserID: tok.UserID, Scope: tok.Scope}
  89. // 刷新使用时间属于尽力而为,失败不影响请求
  90. _ = store.TouchToken(ctx, tok.ID, time.Now())
  91. c.Set(ctxKeyTokenID, tok.ID)
  92. } else {
  93. // gin 的 Context.Cookie 返回 (string, error):err 非空表示没有该 cookie
  94. raw, cookieErr := c.Cookie(CookieSession)
  95. if cookieErr == nil && raw != "" {
  96. // 会话 token 可能带 TOTP 标记,必须先剥离再哈希(见 session_token.go)
  97. base, totpVerified := splitSessionToken(raw)
  98. sess, loadErr := store.FindSessionByHash(ctx, HashToken(base))
  99. switch {
  100. case errors.Is(loadErr, ErrNotFound):
  101. // 会话无效:视为未登录(auth.md 6.1 第 2 步)
  102. case loadErr != nil:
  103. abortInternal(c, loadErr)
  104. return
  105. default:
  106. identity = Identity{
  107. Kind: SessionUser,
  108. UserID: sess.UserID,
  109. TotpVerified: totpVerified,
  110. }
  111. _ = store.TouchSession(ctx, sess.ID, time.Now())
  112. // 会话 id 需注入 context:撤销当前会话、标记 is_current 与登出都依赖它
  113. c.Set(ctxKeySessionID, sess.ID)
  114. }
  115. }
  116. }
  117. c.Set(ctxKeyIdentity, identity)
  118. c.Set(ctxKeyVisibility, visibilityFor(identity))
  119. c.Next()
  120. }
  121. }
  122. // RequireLogin 要求已登录(会话或任意 Token),否则 401(auth.md 6.2)。
  123. func RequireLogin() gin.HandlerFunc {
  124. return func(c *gin.Context) {
  125. if !CurrentIdentity(c).IsLoggedIn() {
  126. abortUnauthorized(c)
  127. return
  128. }
  129. c.Next()
  130. }
  131. }
  132. // RequireWrite 要求写权限:会话,或 scope = read_write 的 Token;
  133. // read Token 返回 403(auth.md 6.2)。
  134. func RequireWrite() gin.HandlerFunc {
  135. return func(c *gin.Context) {
  136. id := CurrentIdentity(c)
  137. if !id.IsLoggedIn() {
  138. abortUnauthorized(c)
  139. return
  140. }
  141. if id.Kind == TokenUser && id.Scope != ScopeReadWrite {
  142. abortForbidden(c)
  143. return
  144. }
  145. c.Next()
  146. }
  147. }
  148. // RequireTotpVerified 要求会话在创建时通过过 TOTP。
  149. // Token 一律 403;会话未通过 TOTP 也 403(auth.md 6.2、api.md 1.6)。
  150. func RequireTotpVerified() gin.HandlerFunc {
  151. return func(c *gin.Context) {
  152. id := CurrentIdentity(c)
  153. if !id.IsLoggedIn() {
  154. abortUnauthorized(c)
  155. return
  156. }
  157. if id.Kind != SessionUser || !id.TotpVerified {
  158. abortForbidden(c)
  159. return
  160. }
  161. c.Next()
  162. }
  163. }
  164. // RequireSession 要求调用方是会话身份,Token 一律 403。
  165. //
  166. // 凭证类资源(会话 / 授信设备 / Token / TOTP / 密码)只允许 Web 会话操作
  167. // (auth.md 第七节说明;api.md 三、四、五节各接口的「Token 调用返回 403」)。
  168. func RequireSession() gin.HandlerFunc {
  169. return func(c *gin.Context) {
  170. id := CurrentIdentity(c)
  171. if !id.IsLoggedIn() {
  172. abortUnauthorized(c)
  173. return
  174. }
  175. if id.Kind != SessionUser {
  176. abortForbidden(c)
  177. return
  178. }
  179. c.Next()
  180. }
  181. }
  182. // bearerToken 取出 Authorization: Bearer 的值;格式不对视为未提供。
  183. func bearerToken(c *gin.Context) string {
  184. header := c.GetHeader("Authorization")
  185. const prefix = "Bearer "
  186. if len(header) <= len(prefix) || header[:len(prefix)] != prefix {
  187. return ""
  188. }
  189. return header[len(prefix):]
  190. }
  191. // tokenUsable 判断 Token 是否可用于鉴权(auth.md 6.1):
  192. // 未撤销且未过期。
  193. func tokenUsable(t APIToken, now time.Time) bool {
  194. if t.RevokedAt != nil {
  195. return false
  196. }
  197. if t.ExpiresAt != nil && !t.ExpiresAt.After(now) {
  198. return false
  199. }
  200. return true
  201. }
  202. // abortUnauthorized / abortForbidden / abortInternal 走统一错误响应。
  203. // 具体响应构造函数在 internal/httpx,这里通过注册的回调解耦,避免包循环。
  204. var (
  205. unauthorizedHandler = defaultAbort(401, "unauthorized", "未登录或凭证无效")
  206. forbiddenHandler = defaultAbort(403, "forbidden", "权限不足")
  207. internalHandler = defaultAbort(500, "internal_error", "服务端错误")
  208. )
  209. // SetErrorHandlers 注入统一错误响应(由 internal/httpx 在启动时调用)。
  210. func SetErrorHandlers(unauthorized, forbidden, internal func(c *gin.Context, err error)) {
  211. if unauthorized != nil {
  212. unauthorizedHandler = func(c *gin.Context, err error) { unauthorized(c, err) }
  213. }
  214. if forbidden != nil {
  215. forbiddenHandler = func(c *gin.Context, err error) { forbidden(c, err) }
  216. }
  217. if internal != nil {
  218. internalHandler = func(c *gin.Context, err error) { internal(c, err) }
  219. }
  220. }
  221. func abortUnauthorized(c *gin.Context) { unauthorizedHandler(c, nil) }
  222. func abortForbidden(c *gin.Context) { forbiddenHandler(c, nil) }
  223. func abortInternal(c *gin.Context, err error) { internalHandler(c, err) }
  224. func defaultAbort(status int, code, msg string) func(*gin.Context, error) {
  225. return func(c *gin.Context, _ error) {
  226. c.AbortWithStatusJSON(status, gin.H{"error": code, "message": msg})
  227. }
  228. }
苏ICP备2026082406号
苏公网安备 待审核