jwtauthMiddleware_test.go 31 KB

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