e41085a878
会话安全(jti 轮换 / 重用检测 / 改密吊销 / 禁用即时下线 / 清理 / 失败登录落库): - refresh token 轮换 jti + token-family 重用检测,旧 token 重放即吊销整条会话 - 改密码、停用用户即时吊销其全部活跃会话(revoked_by 审计) - 中间件 session JOIN user 校验,禁用/删除用户带 token 请求返回 401 USER_DISABLED - 新增 login_attempts 失败登录落库 + 会话保留期清理 goroutine 授权实时 phase + 心跳回带: - LicenseGuard 改为按当前 DB 实时计算 phase(30s 每店缓存),续费/过期/被改 ~30s 内对写操作生效,无需重登 - /auth/ping 回带授权概况(ShopInfoView,与 /license/info 同构),客户端一次心跳即刷新横幅/门禁 首次使用自动试用 + code-review 修复: - 门店首次登录/续期无有效授权时自动签发 30 天 trial(快路径无锁 Count,仅首用走 FOR UPDATE 事务) - ShopInfo 区分「确无授权」与瞬时 DB 错误,避免误降级 - trial 签发后改为在事务提交后再失效 phase 缓存(修复早于提交的竞态) - 存量无 sid token 续期纳入显式上限,legacy 会话不再游离于并发配额之外 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
47 lines
1.5 KiB
Go
47 lines
1.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestReadOnly 验证只读角色的写操作被拦截、读操作放行,且 403 带机器可读 code。
|
|
func TestReadOnly(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// invoke 跑一次带 ReadOnly 的请求,返回(是否被拦截, 状态码, body)。
|
|
invoke := func(role, method string) (bool, int, map[string]any) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(method, "/products", nil)
|
|
c.Set(CtxRole, role)
|
|
ReadOnly()(c)
|
|
var body map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
|
return c.IsAborted(), w.Code, body
|
|
}
|
|
|
|
// 只读角色 + 写方法 → 403 + code=READONLY_USER
|
|
for _, m := range []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch} {
|
|
aborted, code, body := invoke("readonly", m)
|
|
assert.True(t, aborted, "readonly 用户 %s 应被拦截", m)
|
|
assert.Equal(t, http.StatusForbidden, code)
|
|
assert.Equal(t, "READONLY_USER", body["code"], "%s 应返回 code=READONLY_USER", m)
|
|
}
|
|
|
|
// 只读角色 + GET → 放行
|
|
aborted, _, _ := invoke("readonly", http.MethodGet)
|
|
assert.False(t, aborted, "readonly 用户 GET 应放行")
|
|
|
|
// 非只读角色 + 写方法 → 放行
|
|
for _, role := range []string{"operator", "admin", "superadmin"} {
|
|
aborted, _, _ := invoke(role, http.MethodPost)
|
|
assert.False(t, aborted, "%s 用户写操作应放行", role)
|
|
}
|
|
}
|