jwtauthMiddleware_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  1. package middleware_test
  2. import (
  3. "context"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "database/sql"
  7. "encoding/base64"
  8. "encoding/json"
  9. "github.com/golang-jwt/jwt/v4"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/zeromicro/go-zero/core/stores/redis"
  13. "github.com/zeromicro/go-zero/rest/httpx"
  14. "net/http"
  15. "net/http/httptest"
  16. "perms-system-server/internal/consts"
  17. "perms-system-server/internal/loaders"
  18. "perms-system-server/internal/middleware"
  19. "perms-system-server/internal/model"
  20. productModel "perms-system-server/internal/model/product"
  21. productmemberModel "perms-system-server/internal/model/productmember"
  22. "perms-system-server/internal/model/user"
  23. userModel "perms-system-server/internal/model/user"
  24. "perms-system-server/internal/response"
  25. "perms-system-server/internal/testutil"
  26. "testing"
  27. "time"
  28. )
  29. const testAccessSecret = "test-middleware-secret"
  30. func generateTestToken(secret string, expireSeconds int64, claims *middleware.Claims) string {
  31. now := time.Now()
  32. claims.RegisteredClaims = jwt.RegisteredClaims{
  33. ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)),
  34. IssuedAt: jwt.NewNumericDate(now),
  35. }
  36. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  37. tokenStr, _ := token.SignedString([]byte(secret))
  38. return tokenStr
  39. }
  40. func newTestMiddleware() (*middleware.JwtAuthMiddleware, *loaders.UserDetailsLoader) {
  41. cfg := testutil.GetTestConfig()
  42. conn := testutil.GetTestSqlConn()
  43. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  44. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  45. loader := loaders.NewUserDetailsLoader(rds, testutil.GetTestCachePrefix(), models)
  46. m := middleware.NewJwtAuthMiddleware(testAccessSecret, loader)
  47. return m, loader
  48. }
  49. func createTestUser(t *testing.T, username string) (int64, func()) {
  50. t.Helper()
  51. ctx := context.Background()
  52. conn := testutil.GetTestSqlConn()
  53. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  54. now := time.Now().Unix()
  55. u := &user.SysUser{
  56. Username: username,
  57. Password: testutil.HashPassword("test123"),
  58. Nickname: "test_nick",
  59. Avatar: sql.NullString{Valid: false},
  60. Email: "[email protected]",
  61. Phone: "13800000000",
  62. Remark: "",
  63. DeptId: 0,
  64. IsSuperAdmin: consts.IsSuperAdminNo,
  65. MustChangePassword: consts.MustChangePasswordNo,
  66. Status: consts.StatusEnabled,
  67. CreateTime: now,
  68. UpdateTime: now,
  69. }
  70. result, err := models.SysUserModel.Insert(ctx, u)
  71. require.NoError(t, err)
  72. userId, err := result.LastInsertId()
  73. require.NoError(t, err)
  74. cleanup := func() {
  75. testutil.CleanTable(ctx, conn, "sys_user", userId)
  76. }
  77. return userId, cleanup
  78. }
  79. func init() {
  80. response.Setup()
  81. }
  82. // TC-0258: `Authorization: Bearer {valid}`
  83. func TestJwtAuthMiddleware_Handle(t *testing.T) {
  84. m, _ := newTestMiddleware()
  85. t.Run("valid token", func(t *testing.T) {
  86. username := "mw_valid_" + testutil.UniqueId()
  87. userId, cleanup := createTestUser(t, username)
  88. defer cleanup()
  89. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  90. TokenType: consts.TokenTypeAccess,
  91. UserId: userId,
  92. Username: username,
  93. ProductCode: "",
  94. })
  95. var capturedCtx context.Context
  96. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  97. capturedCtx = r.Context()
  98. w.WriteHeader(http.StatusOK)
  99. })
  100. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  101. req.Header.Set("Authorization", "Bearer "+tokenStr)
  102. rr := httptest.NewRecorder()
  103. handler.ServeHTTP(rr, req)
  104. assert.Equal(t, http.StatusOK, rr.Code)
  105. assert.Equal(t, userId, middleware.GetUserId(capturedCtx))
  106. assert.Equal(t, "", middleware.GetProductCode(capturedCtx))
  107. details := middleware.GetUserDetails(capturedCtx)
  108. require.NotNil(t, details)
  109. assert.Equal(t, username, details.Username)
  110. assert.Equal(t, "", details.MemberType)
  111. assert.False(t, details.IsSuperAdmin)
  112. })
  113. t.Run("no authorization header", func(t *testing.T) {
  114. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  115. t.Fatal("should not reach handler")
  116. })
  117. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  118. rr := httptest.NewRecorder()
  119. handler.ServeHTTP(rr, req)
  120. var body response.Body
  121. err := json.Unmarshal(rr.Body.Bytes(), &body)
  122. require.NoError(t, err)
  123. assert.Equal(t, 401, body.Code)
  124. assert.Equal(t, "未登录", body.Msg)
  125. })
  126. t.Run("no Bearer prefix", func(t *testing.T) {
  127. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  128. t.Fatal("should not reach handler")
  129. })
  130. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  131. req.Header.Set("Authorization", "Basic some-token")
  132. rr := httptest.NewRecorder()
  133. handler.ServeHTTP(rr, req)
  134. var body response.Body
  135. err := json.Unmarshal(rr.Body.Bytes(), &body)
  136. require.NoError(t, err)
  137. assert.Equal(t, 401, body.Code)
  138. assert.Equal(t, "token格式错误", body.Msg)
  139. })
  140. t.Run("invalid token", func(t *testing.T) {
  141. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  142. t.Fatal("should not reach handler")
  143. })
  144. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  145. req.Header.Set("Authorization", "Bearer invalid-token-string")
  146. rr := httptest.NewRecorder()
  147. handler.ServeHTTP(rr, req)
  148. var body response.Body
  149. err := json.Unmarshal(rr.Body.Bytes(), &body)
  150. require.NoError(t, err)
  151. assert.Equal(t, 401, body.Code)
  152. assert.Equal(t, "token无效或已过期", body.Msg)
  153. })
  154. t.Run("wrong secret", func(t *testing.T) {
  155. tokenStr := generateTestToken("wrong-secret", 3600, &middleware.Claims{
  156. TokenType: consts.TokenTypeAccess,
  157. UserId: 1,
  158. })
  159. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  160. t.Fatal("should not reach handler")
  161. })
  162. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  163. req.Header.Set("Authorization", "Bearer "+tokenStr)
  164. rr := httptest.NewRecorder()
  165. handler.ServeHTTP(rr, req)
  166. var body response.Body
  167. err := json.Unmarshal(rr.Body.Bytes(), &body)
  168. require.NoError(t, err)
  169. assert.Equal(t, 401, body.Code)
  170. assert.Equal(t, "token无效或已过期", body.Msg)
  171. })
  172. t.Run("expired token", func(t *testing.T) {
  173. tokenStr := generateTestToken(testAccessSecret, -10, &middleware.Claims{
  174. TokenType: consts.TokenTypeAccess,
  175. UserId: 1,
  176. })
  177. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  178. t.Fatal("should not reach handler")
  179. })
  180. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  181. req.Header.Set("Authorization", "Bearer "+tokenStr)
  182. rr := httptest.NewRecorder()
  183. handler.ServeHTTP(rr, req)
  184. var body response.Body
  185. err := json.Unmarshal(rr.Body.Bytes(), &body)
  186. require.NoError(t, err)
  187. assert.Equal(t, 401, body.Code)
  188. assert.Equal(t, "token无效或已过期", body.Msg)
  189. })
  190. // TC-0264: refresh token 不应被中间件接受
  191. t.Run("refresh token rejected", func(t *testing.T) {
  192. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  193. TokenType: consts.TokenTypeRefresh,
  194. UserId: 100,
  195. })
  196. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  197. t.Fatal("should not reach handler")
  198. })
  199. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  200. req.Header.Set("Authorization", "Bearer "+tokenStr)
  201. rr := httptest.NewRecorder()
  202. handler.ServeHTTP(rr, req)
  203. var body response.Body
  204. err := json.Unmarshal(rr.Body.Bytes(), &body)
  205. require.NoError(t, err)
  206. assert.Equal(t, 401, body.Code)
  207. assert.Equal(t, "token无效或类型错误", body.Msg)
  208. })
  209. t.Run("frozen user rejected", func(t *testing.T) {
  210. username := "mw_frozen_" + testutil.UniqueId()
  211. userId, cleanup := createTestUser(t, username)
  212. defer cleanup()
  213. ctx := context.Background()
  214. conn := testutil.GetTestSqlConn()
  215. now := time.Now().Unix()
  216. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  217. err := models.SysUserModel.Update(ctx, &user.SysUser{
  218. Id: userId,
  219. Username: username,
  220. Password: testutil.HashPassword("test123"),
  221. Nickname: "test_nick",
  222. Avatar: sql.NullString{Valid: false},
  223. Email: "[email protected]",
  224. Phone: "13800000000",
  225. DeptId: 0,
  226. IsSuperAdmin: consts.IsSuperAdminNo,
  227. MustChangePassword: consts.MustChangePasswordNo,
  228. Status: consts.StatusDisabled,
  229. CreateTime: now,
  230. UpdateTime: now,
  231. })
  232. require.NoError(t, err)
  233. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  234. TokenType: consts.TokenTypeAccess,
  235. UserId: userId,
  236. Username: username,
  237. ProductCode: "",
  238. })
  239. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  240. t.Fatal("should not reach handler")
  241. })
  242. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  243. req.Header.Set("Authorization", "Bearer "+tokenStr)
  244. rr := httptest.NewRecorder()
  245. handler.ServeHTTP(rr, req)
  246. var body response.Body
  247. err = json.Unmarshal(rr.Body.Bytes(), &body)
  248. require.NoError(t, err)
  249. assert.Equal(t, 403, body.Code)
  250. assert.Equal(t, "账号已被冻结", body.Msg)
  251. })
  252. }
  253. // TC-0306: ctx含userId=100
  254. func TestGetUserId(t *testing.T) {
  255. ctx := context.Background()
  256. assert.Equal(t, int64(0), middleware.GetUserId(ctx))
  257. ctx = middleware.WithUserDetails(ctx, &loaders.UserDetails{UserId: 42})
  258. assert.Equal(t, int64(42), middleware.GetUserId(ctx))
  259. ctx2 := context.Background()
  260. assert.Equal(t, int64(0), middleware.GetUserId(ctx2))
  261. }
  262. // TC-0275: 空ctx
  263. func TestGetProductCode(t *testing.T) {
  264. ctx := context.Background()
  265. assert.Equal(t, "", middleware.GetProductCode(ctx))
  266. ctx = middleware.WithUserDetails(ctx, &loaders.UserDetails{ProductCode: "p1"})
  267. assert.Equal(t, "p1", middleware.GetProductCode(ctx))
  268. }
  269. // TC-0309: GetUserDetails 返回完整用户信息
  270. func TestGetUserDetails(t *testing.T) {
  271. ctx := context.Background()
  272. assert.Nil(t, middleware.GetUserDetails(ctx))
  273. expected := &loaders.UserDetails{
  274. UserId: 42,
  275. Username: "admin",
  276. ProductCode: "p1",
  277. MemberType: "ADMIN",
  278. IsSuperAdmin: true,
  279. }
  280. ctx = middleware.WithUserDetails(ctx, expected)
  281. got := middleware.GetUserDetails(ctx)
  282. require.NotNil(t, got)
  283. assert.Equal(t, expected.UserId, got.UserId)
  284. assert.Equal(t, expected.Username, got.Username)
  285. assert.Equal(t, expected.ProductCode, got.ProductCode)
  286. assert.Equal(t, expected.MemberType, got.MemberType)
  287. assert.Equal(t, expected.IsSuperAdmin, got.IsSuperAdmin)
  288. }
  289. // TC-0263: claims类型断言失败(防御性分支)
  290. // jwt.ParseWithClaims(tokenStr, &Claims{}, keyFunc) 始终将 token.Claims 设为 *Claims,
  291. // 且解析失败时 Handle 已在 err!=nil 分支提前返回,因此 !ok 分支不可达。
  292. func TestJwtAuthMiddleware_Handle_ClaimsTypeAssertionUnreachable(t *testing.T) {
  293. t.Skip("defensive branch: unreachable via jwt.ParseWithClaims — claims is always *Claims")
  294. }
  295. // suppress unused import warning for httpx
  296. var _ = httpx.Error
  297. func TestJwtAuthMiddleware_TokenVersionCheckedBeforeProductStatus(t *testing.T) {
  298. ctx := context.Background()
  299. conn := testutil.GetTestSqlConn()
  300. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  301. now := time.Now().Unix()
  302. // 1) 创建"已禁用"的产品
  303. pCode := "mw_ord_" + testutil.UniqueId()
  304. pRes, err := models.SysProductModel.Insert(ctx, &productModel.SysProduct{
  305. Code: pCode, Name: pCode, AppKey: pCode + "_k", AppSecret: "s",
  306. Status: consts.StatusDisabled, CreateTime: now, UpdateTime: now,
  307. })
  308. require.NoError(t, err)
  309. pId, _ := pRes.LastInsertId()
  310. // 2) 创建启用中的用户,TokenVersion=5
  311. username := "mw_ord_u_" + testutil.UniqueId()
  312. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  313. Username: username, Password: "x", Nickname: "n",
  314. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  315. MustChangePassword: 2, Status: consts.StatusEnabled, TokenVersion: 5,
  316. CreateTime: now, UpdateTime: now,
  317. })
  318. require.NoError(t, err)
  319. userId, _ := uRes.LastInsertId()
  320. // 3) 该用户是禁用产品的 ADMIN 成员
  321. mRes, err := models.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  322. ProductCode: pCode, UserId: userId, MemberType: consts.MemberTypeAdmin,
  323. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  324. })
  325. require.NoError(t, err)
  326. mId, _ := mRes.LastInsertId()
  327. t.Cleanup(func() {
  328. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  329. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  330. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  331. })
  332. m, _ := newTestMiddleware()
  333. // 携带 stale TokenVersion=3 的 access token(DB 是 5)+ 禁用产品 code
  334. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  335. TokenType: consts.TokenTypeAccess,
  336. UserId: userId,
  337. Username: username,
  338. ProductCode: pCode,
  339. TokenVersion: 3,
  340. })
  341. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  342. t.Fatal("should not reach handler")
  343. })
  344. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  345. req.Header.Set("Authorization", "Bearer "+tokenStr)
  346. rr := httptest.NewRecorder()
  347. handler.ServeHTTP(rr, req)
  348. var body response.Body
  349. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  350. assert.Equal(t, 401, body.Code,
  351. "L-B:TokenVersion 失配必须先于产品禁用被识别(返回 401 而非 403)")
  352. assert.Equal(t, "登录状态已失效,请重新登录", body.Msg,
  353. "L-B:文案必须是'登录状态已失效'而不是'该产品已被禁用',否则用户会被无关信息误导")
  354. }
  355. // TC-0750: -B 修复回归 —— TokenVersion 匹配但产品被禁用,仍应返回 403 "该产品已被禁用"。
  356. // 保证修复未把所有场景都吞成 401。
  357. func TestJwtAuthMiddleware_ProductDisabledAfterVersionOk(t *testing.T) {
  358. ctx := context.Background()
  359. conn := testutil.GetTestSqlConn()
  360. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  361. now := time.Now().Unix()
  362. pCode := "mw_ord2_" + testutil.UniqueId()
  363. pRes, err := models.SysProductModel.Insert(ctx, &productModel.SysProduct{
  364. Code: pCode, Name: pCode, AppKey: pCode + "_k", AppSecret: "s",
  365. Status: consts.StatusDisabled, CreateTime: now, UpdateTime: now,
  366. })
  367. require.NoError(t, err)
  368. pId, _ := pRes.LastInsertId()
  369. username := "mw_ord2_u_" + testutil.UniqueId()
  370. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  371. Username: username, Password: "x", Nickname: "n",
  372. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  373. MustChangePassword: 2, Status: consts.StatusEnabled, TokenVersion: 0,
  374. CreateTime: now, UpdateTime: now,
  375. })
  376. require.NoError(t, err)
  377. userId, _ := uRes.LastInsertId()
  378. mRes, err := models.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  379. ProductCode: pCode, UserId: userId, MemberType: consts.MemberTypeAdmin,
  380. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  381. })
  382. require.NoError(t, err)
  383. mId, _ := mRes.LastInsertId()
  384. t.Cleanup(func() {
  385. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  386. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  387. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  388. })
  389. m, _ := newTestMiddleware()
  390. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  391. TokenType: consts.TokenTypeAccess,
  392. UserId: userId,
  393. Username: username,
  394. ProductCode: pCode,
  395. TokenVersion: 0,
  396. })
  397. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  398. t.Fatal("should not reach handler")
  399. })
  400. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  401. req.Header.Set("Authorization", "Bearer "+tokenStr)
  402. rr := httptest.NewRecorder()
  403. handler.ServeHTTP(rr, req)
  404. var body response.Body
  405. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  406. assert.Equal(t, 403, body.Code)
  407. assert.Equal(t, "该产品已被禁用", body.Msg)
  408. }
  409. // --- 鉴权优先级完整矩阵(-B 延伸,TC-0754 ~ TC-0758)---
  410. // 代码中顺序: Username empty -> Status disabled -> TokenVersion mismatch -> ProductStatus -> MemberType。
  411. // 这 5 个用例用"同时踩两个坑"的组合方式, 严格断言哪个错误文案胜出, 形成鉴权优先级的冻结矩阵。
  412. // TC-0754: 用户已被删除 + TokenVersion 失配 -> 优先返回 401 "用户不存在或已被删除"。
  413. // 场景: 攻击者拿着 stale token + 账号已被删除, 服务端必须先识别出用户不存在而不是"登录已失效",
  414. // 否则会把"软删除"语义泄漏成"用户登出"从而引导攻击者再次尝试重登。
  415. func TestJwtAuthMiddleware_UserDeletedBeatsTokenVersion(t *testing.T) {
  416. ctx := context.Background()
  417. conn := testutil.GetTestSqlConn()
  418. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  419. now := time.Now().Unix()
  420. username := "mw_del_" + testutil.UniqueId()
  421. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  422. Username: username, Password: "x", Nickname: "n",
  423. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  424. MustChangePassword: 2, Status: consts.StatusEnabled, TokenVersion: 5,
  425. CreateTime: now, UpdateTime: now,
  426. })
  427. require.NoError(t, err)
  428. userId, _ := uRes.LastInsertId()
  429. require.NoError(t, models.SysUserModel.Delete(ctx, userId))
  430. m, _ := newTestMiddleware()
  431. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  432. TokenType: consts.TokenTypeAccess, UserId: userId, Username: username,
  433. TokenVersion: 1,
  434. })
  435. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unreachable") })
  436. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  437. req.Header.Set("Authorization", "Bearer "+tokenStr)
  438. rr := httptest.NewRecorder()
  439. handler.ServeHTTP(rr, req)
  440. var body response.Body
  441. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  442. assert.Equal(t, 401, body.Code,
  443. "L-B 矩阵: Username empty 必须在 TokenVersion 之前裁决")
  444. assert.Equal(t, "用户不存在或已被删除", body.Msg,
  445. "L-B 矩阵: 用户被删除时文案不可退化成 '登录已失效',否则泄漏软删除语义")
  446. }
  447. // TC-0755: 账号被冻结 + TokenVersion 失配 + 产品被禁用 -> 胜出应是 403 "账号已被冻结"。
  448. // 三重 failing condition 叠加, 验证"账号级"问题比"会话级"(TokenVersion) 和"产品级"(ProductStatus) 优先级更高。
  449. func TestJwtAuthMiddleware_FrozenBeatsEverything(t *testing.T) {
  450. ctx := context.Background()
  451. conn := testutil.GetTestSqlConn()
  452. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  453. now := time.Now().Unix()
  454. pCode := "mw_fz_" + testutil.UniqueId()
  455. pRes, err := models.SysProductModel.Insert(ctx, &productModel.SysProduct{
  456. Code: pCode, Name: pCode, AppKey: pCode + "_k", AppSecret: "s",
  457. Status: consts.StatusDisabled, CreateTime: now, UpdateTime: now,
  458. })
  459. require.NoError(t, err)
  460. pId, _ := pRes.LastInsertId()
  461. username := "mw_fz_u_" + testutil.UniqueId()
  462. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  463. Username: username, Password: "x", Nickname: "n",
  464. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  465. MustChangePassword: 2, Status: consts.StatusDisabled, TokenVersion: 9,
  466. CreateTime: now, UpdateTime: now,
  467. })
  468. require.NoError(t, err)
  469. userId, _ := uRes.LastInsertId()
  470. mRes, err := models.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  471. ProductCode: pCode, UserId: userId, MemberType: consts.MemberTypeAdmin,
  472. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  473. })
  474. require.NoError(t, err)
  475. mId, _ := mRes.LastInsertId()
  476. t.Cleanup(func() {
  477. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  478. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  479. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  480. })
  481. m, _ := newTestMiddleware()
  482. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  483. TokenType: consts.TokenTypeAccess, UserId: userId, Username: username,
  484. ProductCode: pCode, TokenVersion: 1,
  485. })
  486. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unreachable") })
  487. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  488. req.Header.Set("Authorization", "Bearer "+tokenStr)
  489. rr := httptest.NewRecorder()
  490. handler.ServeHTTP(rr, req)
  491. var body response.Body
  492. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  493. assert.Equal(t, 403, body.Code,
  494. "L-B 矩阵: 账号冻结(403) 胜出, 而非 TokenVersion(401) 或 ProductStatus(403/禁用)")
  495. assert.Equal(t, "账号已被冻结", body.Msg,
  496. "L-B 矩阵: 冻结文案必须先于 '登录已失效'/'产品禁用' 返回给客户端")
  497. }
  498. // TC-0756: TokenVersion OK + 产品启用 + 非超管 + MemberType 为空 -> 403 "您已不是该产品的有效成员"。
  499. // 场景: 用户曾是产品成员, 后被移除, 但老 token 未过期; 本用例保证"移除成员"的写路径会被读路径识别。
  500. func TestJwtAuthMiddleware_NonMemberRejected(t *testing.T) {
  501. ctx := context.Background()
  502. conn := testutil.GetTestSqlConn()
  503. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  504. now := time.Now().Unix()
  505. pCode := "mw_nm_" + testutil.UniqueId()
  506. pRes, err := models.SysProductModel.Insert(ctx, &productModel.SysProduct{
  507. Code: pCode, Name: pCode, AppKey: pCode + "_k", AppSecret: "s",
  508. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  509. })
  510. require.NoError(t, err)
  511. pId, _ := pRes.LastInsertId()
  512. username := "mw_nm_u_" + testutil.UniqueId()
  513. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  514. Username: username, Password: "x", Nickname: "n",
  515. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  516. MustChangePassword: 2, Status: consts.StatusEnabled, TokenVersion: 0,
  517. CreateTime: now, UpdateTime: now,
  518. })
  519. require.NoError(t, err)
  520. userId, _ := uRes.LastInsertId()
  521. t.Cleanup(func() {
  522. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  523. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  524. })
  525. m, _ := newTestMiddleware()
  526. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  527. TokenType: consts.TokenTypeAccess, UserId: userId, Username: username,
  528. ProductCode: pCode, TokenVersion: 0,
  529. })
  530. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unreachable") })
  531. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  532. req.Header.Set("Authorization", "Bearer "+tokenStr)
  533. rr := httptest.NewRecorder()
  534. handler.ServeHTTP(rr, req)
  535. var body response.Body
  536. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  537. assert.Equal(t, 403, body.Code)
  538. assert.Equal(t, "您已不是该产品的有效成员", body.Msg,
  539. "L-B 矩阵: MemberType 空 + 非超管 + 产品启用 必须精确命中'不是有效成员'文案")
  540. }
  541. // TC-0757: 超级管理员 + ProductCode 携带 + MemberType 空 -> 正常放行。
  542. // 超管"旁路"分支不能被移除, 否则超管在产品上下文会被错误踢出。
  543. func TestJwtAuthMiddleware_SuperAdminBypassesMemberCheck(t *testing.T) {
  544. ctx := context.Background()
  545. conn := testutil.GetTestSqlConn()
  546. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  547. now := time.Now().Unix()
  548. pCode := "mw_sa_" + testutil.UniqueId()
  549. pRes, err := models.SysProductModel.Insert(ctx, &productModel.SysProduct{
  550. Code: pCode, Name: pCode, AppKey: pCode + "_k", AppSecret: "s",
  551. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  552. })
  553. require.NoError(t, err)
  554. pId, _ := pRes.LastInsertId()
  555. username := "mw_sa_u_" + testutil.UniqueId()
  556. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  557. Username: username, Password: "x", Nickname: "n",
  558. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminYes,
  559. MustChangePassword: 2, Status: consts.StatusEnabled, TokenVersion: 0,
  560. CreateTime: now, UpdateTime: now,
  561. })
  562. require.NoError(t, err)
  563. userId, _ := uRes.LastInsertId()
  564. t.Cleanup(func() {
  565. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  566. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  567. })
  568. m, _ := newTestMiddleware()
  569. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  570. TokenType: consts.TokenTypeAccess, UserId: userId, Username: username,
  571. ProductCode: pCode, TokenVersion: 0,
  572. })
  573. var reached bool
  574. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) {
  575. reached = true
  576. w.WriteHeader(http.StatusOK)
  577. })
  578. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  579. req.Header.Set("Authorization", "Bearer "+tokenStr)
  580. rr := httptest.NewRecorder()
  581. handler.ServeHTTP(rr, req)
  582. assert.Equal(t, http.StatusOK, rr.Code, "L-B 矩阵: 超管必须放行, 即使在产品上下文无 MemberType")
  583. assert.True(t, reached, "请求必须到达业务 handler")
  584. }
  585. // TC-0758: 无 ProductCode 时, Frozen 用户 + TokenVersion 失配 -> 403 "账号已被冻结"。
  586. // 验证即使不走产品相关分支, Status 检查仍先于 TokenVersion 裁决。
  587. func TestJwtAuthMiddleware_FrozenBeatsTokenVersionNoProduct(t *testing.T) {
  588. ctx := context.Background()
  589. conn := testutil.GetTestSqlConn()
  590. models := model.NewModels(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  591. now := time.Now().Unix()
  592. username := "mw_fz2_u_" + testutil.UniqueId()
  593. uRes, err := models.SysUserModel.Insert(ctx, &userModel.SysUser{
  594. Username: username, Password: "x", Nickname: "n",
  595. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  596. MustChangePassword: 2, Status: consts.StatusDisabled, TokenVersion: 7,
  597. CreateTime: now, UpdateTime: now,
  598. })
  599. require.NoError(t, err)
  600. userId, _ := uRes.LastInsertId()
  601. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  602. m, _ := newTestMiddleware()
  603. tokenStr := generateTestToken(testAccessSecret, 3600, &middleware.Claims{
  604. TokenType: consts.TokenTypeAccess, UserId: userId, Username: username,
  605. TokenVersion: 0,
  606. })
  607. handler := m.Handle(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unreachable") })
  608. req := httptest.NewRequest(http.MethodPost, "/test", nil)
  609. req.Header.Set("Authorization", "Bearer "+tokenStr)
  610. rr := httptest.NewRecorder()
  611. handler.ServeHTTP(rr, req)
  612. var body response.Body
  613. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  614. assert.Equal(t, 403, body.Code)
  615. assert.Equal(t, "账号已被冻结", body.Msg)
  616. }
  617. // 覆盖目标:ParseWithHMAC 上移到 middleware 层作为唯一入口。
  618. // 本测试直接在 middleware 外部测试包内验证:
  619. // (1) 正常 HS256 token 放行
  620. // (2) alg=RS256(公钥→HMAC 共享密钥混淆)显式拒绝
  621. // (3) alg=none 拒绝
  622. // (4) 错误 secret 签名拒绝
  623. // (5) 非 Claims 结构的 claims 同样正确解析(保证函数与具体 claims 类型解耦)
  624. const ln1Secret = "ln1-centralized-secret"
  625. func b64urlLN1(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
  626. func forgeTokenLN1(t *testing.T, alg string, claims any, signKey string) string {
  627. t.Helper()
  628. header := map[string]string{"alg": alg, "typ": "JWT"}
  629. hBytes, err := json.Marshal(header)
  630. require.NoError(t, err)
  631. pBytes, err := json.Marshal(claims)
  632. require.NoError(t, err)
  633. signingInput := b64urlLN1(hBytes) + "." + b64urlLN1(pBytes)
  634. mac := hmac.New(sha256.New, []byte(signKey))
  635. mac.Write([]byte(signingInput))
  636. return signingInput + "." + b64urlLN1(mac.Sum(nil))
  637. }
  638. func validAccessClaimsLN1() middleware.Claims {
  639. now := time.Now()
  640. return middleware.Claims{
  641. TokenType: consts.TokenTypeAccess,
  642. UserId: 42,
  643. Username: "ln1_u",
  644. ProductCode: "ln1_p",
  645. MemberType: consts.MemberTypeAdmin,
  646. TokenVersion: 0,
  647. RegisteredClaims: jwt.RegisteredClaims{
  648. ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
  649. IssuedAt: jwt.NewNumericDate(now),
  650. },
  651. }
  652. }
  653. // TC-1003: middleware 层 ParseWithHMAC 能正确解析合法 HS256 token。
  654. func TestMiddlewareParseWithHMAC_LN1_HS256Valid(t *testing.T) {
  655. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, validAccessClaimsLN1())
  656. tok, err := signed.SignedString([]byte(ln1Secret))
  657. require.NoError(t, err)
  658. parsed, err := middleware.ParseWithHMAC(tok, ln1Secret, &middleware.Claims{})
  659. require.NoError(t, err)
  660. require.True(t, parsed.Valid)
  661. claims, ok := parsed.Claims.(*middleware.Claims)
  662. require.True(t, ok)
  663. assert.Equal(t, int64(42), claims.UserId)
  664. assert.Equal(t, consts.TokenTypeAccess, claims.TokenType)
  665. }
  666. // TC-1004: middleware 层 ParseWithHMAC 必须拒绝 alg=RS256 伪造(公钥→HMAC 混淆)。
  667. func TestMiddlewareParseWithHMAC_LN1_RS256HeaderRejected(t *testing.T) {
  668. forged := forgeTokenLN1(t, "RS256", validAccessClaimsLN1(), ln1Secret)
  669. _, err := middleware.ParseWithHMAC(forged, ln1Secret, &middleware.Claims{})
  670. require.Error(t, err, "必须拒绝 alg=RS256 伪造 token")
  671. assert.Contains(t, err.Error(), "unexpected signing method",
  672. "HMAC 断言失败必须产出可错误信息,方便 SOC 定位攻击尝试")
  673. }
  674. // TC-1005: middleware 层 ParseWithHMAC 必须拒绝 alg=none。
  675. func TestMiddlewareParseWithHMAC_LN1_AlgNoneRejected(t *testing.T) {
  676. header := map[string]string{"alg": "none", "typ": "JWT"}
  677. hBytes, _ := json.Marshal(header)
  678. pBytes, _ := json.Marshal(validAccessClaimsLN1())
  679. forged := b64urlLN1(hBytes) + "." + b64urlLN1(pBytes) + "."
  680. _, err := middleware.ParseWithHMAC(forged, ln1Secret, &middleware.Claims{})
  681. require.Error(t, err, "alg=none 不可通过 HMAC 唯一入口")
  682. }
  683. // TC-1006: 错误 secret 签发的合法结构 HS256 token 必须被拒绝。
  684. func TestMiddlewareParseWithHMAC_LN1_WrongSecretRejected(t *testing.T) {
  685. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, validAccessClaimsLN1())
  686. tok, err := signed.SignedString([]byte("attacker-guess"))
  687. require.NoError(t, err)
  688. _, err = middleware.ParseWithHMAC(tok, ln1Secret, &middleware.Claims{})
  689. require.Error(t, err, "签名校验失败必须 fail-close")
  690. }
  691. // TC-1007: ParseWithHMAC 可以为任意 jwt.Claims 结构体工作(不绑 Claims 类型),
  692. // 保证 gRPC VerifyToken、RefreshToken、HTTP 中间件等所有调用点可以共用该入口。
  693. func TestMiddlewareParseWithHMAC_LN1_ArbitraryClaimsType(t *testing.T) {
  694. type customClaims struct {
  695. Role string `json:"role"`
  696. jwt.RegisteredClaims
  697. }
  698. c := customClaims{
  699. Role: "admin",
  700. RegisteredClaims: jwt.RegisteredClaims{
  701. ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
  702. IssuedAt: jwt.NewNumericDate(time.Now()),
  703. },
  704. }
  705. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
  706. tok, err := signed.SignedString([]byte(ln1Secret))
  707. require.NoError(t, err)
  708. parsed, err := middleware.ParseWithHMAC(tok, ln1Secret, &customClaims{})
  709. require.NoError(t, err)
  710. parsedClaims, ok := parsed.Claims.(*customClaims)
  711. require.True(t, ok)
  712. assert.Equal(t, "admin", parsedClaims.Role,
  713. "唯一入口必须对任意 claims 类型解耦,保证所有调用方可以复用")
  714. }