| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- package auth
- import (
- "errors"
- "time"
- "github.com/gin-gonic/gin"
- )
- // 可见性过滤条件(auth.md 6.3)。
- //
- // 所有读接口必须使用这里算出的条件,不得各自拼 visibility 判断
- // (conventions.md 第四节、auth.md 6.3)。
- const (
- // VisibilityPublic 是「公开可见」的唯一判定条件。
- VisibilityPublic = `visibility = 'public' AND published_at IS NOT NULL`
- // VisibilityOwnedOrPublic 供已登录用户使用:自己的 OR 公开。
- // 使用方需把 $1 绑定为当前用户 id。
- VisibilityOwnedOrPublic = `(user_id = $1) OR (` + VisibilityPublic + `)`
- )
- // 注入 *gin.Context 的键。
- const (
- ctxKeyIdentity = "identity"
- ctxKeyVisibility = "visibility"
- )
- // Visibility 是中间件算出的数据可见性条件。
- //
- // Condition 是可直接拼进 WHERE 的 SQL 片段,Args 是对应的参数。
- // 私有读与写操作在此基础上还要叠加 user_id 过滤(auth.md 6.4)。
- type Visibility struct {
- // Condition 为空的含义是「不可见任何数据」,调用方应直接返回空列表或 404。
- Condition string
- Args []any
- }
- // Visibility 从 gin context 取出可见性条件。
- func CurrentVisibility(c *gin.Context) Visibility {
- if v, ok := c.Get(ctxKeyVisibility); ok {
- if vis, ok := v.(Visibility); ok {
- return vis
- }
- }
- return Visibility{}
- }
- // CurrentIdentity 从 gin context 取出身份,缺失时视为匿名。
- func CurrentIdentity(c *gin.Context) Identity {
- if v, ok := c.Get(ctxKeyIdentity); ok {
- if id, ok := v.(Identity); ok {
- return id
- }
- }
- return Identity{Kind: Anonymous}
- }
- // visibilityFor 按身份计算可见性条件(auth.md 6.3)。
- func visibilityFor(id Identity) Visibility {
- if !id.IsLoggedIn() {
- return Visibility{Condition: VisibilityPublic}
- }
- return Visibility{
- Condition: VisibilityOwnedOrPublic,
- Args: []any{id.UserID},
- }
- }
- // Authenticate 是身份识别中间件(auth.md 6.1)。
- //
- // 顺序:
- // 1. Authorization: Bearer 优先于 cookie,Token 匹配失败直接 401;
- // 2. 否则查会话 cookie;会话无效视为未登录,继续往后走;
- // 3. 否则匿名。
- //
- // 本中间件只识别身份并注入 context,不做接口级权限判定,
- // 权限由 RequireLogin / RequireWrite / RequireTotpVerified 负责(auth.md 6.2)。
- func Authenticate(store Store) gin.HandlerFunc {
- return func(c *gin.Context) {
- ctx := c.Request.Context()
- identity := Identity{Kind: Anonymous}
- if raw := bearerToken(c); raw != "" {
- tok, err := store.FindTokenByHash(ctx, HashToken(raw))
- switch {
- case errors.Is(err, ErrNotFound):
- // Token 无效:直接拒绝,不退回 cookie 身份(auth.md 6.1 第 1 步)
- abortUnauthorized(c)
- return
- case err != nil:
- abortInternal(c, err)
- return
- }
- if !tokenUsable(tok, time.Now()) {
- abortUnauthorized(c)
- return
- }
- identity = Identity{Kind: TokenUser, UserID: tok.UserID, Scope: tok.Scope}
- // 刷新使用时间属于尽力而为,失败不影响请求
- _ = store.TouchToken(ctx, tok.ID, time.Now())
- c.Set(ctxKeyTokenID, tok.ID)
- } else {
- // gin 的 Context.Cookie 返回 (string, error):err 非空表示没有该 cookie
- raw, cookieErr := c.Cookie(CookieSession)
- if cookieErr == nil && raw != "" {
- // 会话 token 可能带 TOTP 标记,必须先剥离再哈希(见 session_token.go)
- base, totpVerified := splitSessionToken(raw)
- sess, loadErr := store.FindSessionByHash(ctx, HashToken(base))
- switch {
- case errors.Is(loadErr, ErrNotFound):
- // 会话无效:视为未登录(auth.md 6.1 第 2 步)
- case loadErr != nil:
- abortInternal(c, loadErr)
- return
- default:
- identity = Identity{
- Kind: SessionUser,
- UserID: sess.UserID,
- TotpVerified: totpVerified,
- }
- _ = store.TouchSession(ctx, sess.ID, time.Now())
- // 会话 id 需注入 context:撤销当前会话、标记 is_current 与登出都依赖它
- c.Set(ctxKeySessionID, sess.ID)
- }
- }
- }
- c.Set(ctxKeyIdentity, identity)
- c.Set(ctxKeyVisibility, visibilityFor(identity))
- c.Next()
- }
- }
- // RequireLogin 要求已登录(会话或任意 Token),否则 401(auth.md 6.2)。
- func RequireLogin() gin.HandlerFunc {
- return func(c *gin.Context) {
- if !CurrentIdentity(c).IsLoggedIn() {
- abortUnauthorized(c)
- return
- }
- c.Next()
- }
- }
- // RequireWrite 要求写权限:会话,或 scope = read_write 的 Token;
- // read Token 返回 403(auth.md 6.2)。
- func RequireWrite() gin.HandlerFunc {
- return func(c *gin.Context) {
- id := CurrentIdentity(c)
- if !id.IsLoggedIn() {
- abortUnauthorized(c)
- return
- }
- if id.Kind == TokenUser && id.Scope != ScopeReadWrite {
- abortForbidden(c)
- return
- }
- c.Next()
- }
- }
- // RequireTotpVerified 要求会话在创建时通过过 TOTP。
- // Token 一律 403;会话未通过 TOTP 也 403(auth.md 6.2、api.md 1.6)。
- func RequireTotpVerified() gin.HandlerFunc {
- return func(c *gin.Context) {
- id := CurrentIdentity(c)
- if !id.IsLoggedIn() {
- abortUnauthorized(c)
- return
- }
- if id.Kind != SessionUser || !id.TotpVerified {
- abortForbidden(c)
- return
- }
- c.Next()
- }
- }
- // RequireSession 要求调用方是会话身份,Token 一律 403。
- //
- // 凭证类资源(会话 / 授信设备 / Token / TOTP / 密码)只允许 Web 会话操作
- // (auth.md 第七节说明;api.md 三、四、五节各接口的「Token 调用返回 403」)。
- func RequireSession() gin.HandlerFunc {
- return func(c *gin.Context) {
- id := CurrentIdentity(c)
- if !id.IsLoggedIn() {
- abortUnauthorized(c)
- return
- }
- if id.Kind != SessionUser {
- abortForbidden(c)
- return
- }
- c.Next()
- }
- }
- // bearerToken 取出 Authorization: Bearer 的值;格式不对视为未提供。
- func bearerToken(c *gin.Context) string {
- header := c.GetHeader("Authorization")
- const prefix = "Bearer "
- if len(header) <= len(prefix) || header[:len(prefix)] != prefix {
- return ""
- }
- return header[len(prefix):]
- }
- // tokenUsable 判断 Token 是否可用于鉴权(auth.md 6.1):
- // 未撤销且未过期。
- func tokenUsable(t APIToken, now time.Time) bool {
- if t.RevokedAt != nil {
- return false
- }
- if t.ExpiresAt != nil && !t.ExpiresAt.After(now) {
- return false
- }
- return true
- }
- // abortUnauthorized / abortForbidden / abortInternal 走统一错误响应。
- // 具体响应构造函数在 internal/httpx,这里通过注册的回调解耦,避免包循环。
- var (
- unauthorizedHandler = defaultAbort(401, "unauthorized", "未登录或凭证无效")
- forbiddenHandler = defaultAbort(403, "forbidden", "权限不足")
- internalHandler = defaultAbort(500, "internal_error", "服务端错误")
- )
- // SetErrorHandlers 注入统一错误响应(由 internal/httpx 在启动时调用)。
- func SetErrorHandlers(unauthorized, forbidden, internal func(c *gin.Context, err error)) {
- if unauthorized != nil {
- unauthorizedHandler = func(c *gin.Context, err error) { unauthorized(c, err) }
- }
- if forbidden != nil {
- forbiddenHandler = func(c *gin.Context, err error) { forbidden(c, err) }
- }
- if internal != nil {
- internalHandler = func(c *gin.Context, err error) { internal(c, err) }
- }
- }
- func abortUnauthorized(c *gin.Context) { unauthorizedHandler(c, nil) }
- func abortForbidden(c *gin.Context) { forbiddenHandler(c, nil) }
- func abortInternal(c *gin.Context, err error) { internalHandler(c, err) }
- func defaultAbort(status int, code, msg string) func(*gin.Context, error) {
- return func(c *gin.Context, _ error) {
- c.AbortWithStatusJSON(status, gin.H{"error": code, "message": msg})
- }
- }
|