refreshTokenLogic_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package pub
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "testing"
  7. "time"
  8. authHelper "perms-system-server/internal/logic/auth"
  9. permModel "perms-system-server/internal/model/perm"
  10. productmemberModel "perms-system-server/internal/model/productmember"
  11. userModel "perms-system-server/internal/model/user"
  12. "perms-system-server/internal/response"
  13. "perms-system-server/internal/testutil"
  14. "perms-system-server/internal/types"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. func insertRefreshTestUser(t *testing.T, ctx context.Context, username, password string, status, isSuperAdmin int64) (int64, func()) {
  19. t.Helper()
  20. svcCtx := newTestSvcCtx()
  21. conn := testutil.GetTestSqlConn()
  22. now := time.Now().Unix()
  23. hashed := testutil.HashPassword(password)
  24. res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  25. Username: username,
  26. Password: hashed,
  27. Nickname: username,
  28. Avatar: sql.NullString{},
  29. Email: username + "@test.com",
  30. Phone: "13800000000",
  31. Remark: "",
  32. DeptId: 0,
  33. IsSuperAdmin: isSuperAdmin,
  34. MustChangePassword: 2,
  35. Status: status,
  36. CreateTime: now,
  37. UpdateTime: now,
  38. })
  39. require.NoError(t, err)
  40. id, _ := res.LastInsertId()
  41. cleanup := func() {
  42. testutil.CleanTable(ctx, conn, "`sys_user`", id)
  43. }
  44. return id, cleanup
  45. }
  46. // TC-0026: 正常刷新(refreshToken从header获取,原样返回不重新生成)
  47. func TestRefreshToken_Normal(t *testing.T) {
  48. ctx := context.Background()
  49. svcCtx := newTestSvcCtx()
  50. username := testutil.UniqueId()
  51. password := "TestPass123"
  52. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  53. t.Cleanup(cleanUser)
  54. refreshToken, err := authHelper.GenerateRefreshToken(
  55. svcCtx.Config.Auth.RefreshSecret,
  56. svcCtx.Config.Auth.RefreshExpire,
  57. userId, "", 0,
  58. )
  59. require.NoError(t, err)
  60. logic := NewRefreshTokenLogic(ctx, svcCtx)
  61. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  62. Authorization: "Bearer " + refreshToken,
  63. })
  64. require.NoError(t, err)
  65. require.NotNil(t, resp)
  66. assert.NotEmpty(t, resp.AccessToken)
  67. assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken")
  68. assert.NotEqual(t, resp.AccessToken, resp.RefreshToken, "accessToken和refreshToken应不同")
  69. assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳")
  70. assert.Equal(t, userId, resp.UserInfo.UserId)
  71. assert.Equal(t, username, resp.UserInfo.Username)
  72. }
  73. // TC-0027: 不带productCode(回退)
  74. func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
  75. ctx := context.Background()
  76. svcCtx := newTestSvcCtx()
  77. conn := testutil.GetTestSqlConn()
  78. username := testutil.UniqueId()
  79. password := "TestPass123"
  80. pc := testutil.UniqueId()
  81. now := time.Now().Unix()
  82. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  83. t.Cleanup(cleanUser)
  84. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  85. t.Cleanup(cleanProduct)
  86. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  87. ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: now, UpdateTime: now,
  88. })
  89. require.NoError(t, err)
  90. pmId, _ := pmRes.LastInsertId()
  91. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) })
  92. permCode := testutil.UniqueId()
  93. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  94. ProductCode: pc, Name: "refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now,
  95. })
  96. require.NoError(t, err)
  97. permId, _ := permRes.LastInsertId()
  98. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  99. refreshToken, err := authHelper.GenerateRefreshToken(
  100. svcCtx.Config.Auth.RefreshSecret,
  101. svcCtx.Config.Auth.RefreshExpire,
  102. userId, pc, 0,
  103. )
  104. require.NoError(t, err)
  105. logic := NewRefreshTokenLogic(ctx, svcCtx)
  106. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  107. Authorization: "Bearer " + refreshToken,
  108. })
  109. require.NoError(t, err)
  110. require.NotNil(t, resp)
  111. assert.Equal(t, "ADMIN", resp.UserInfo.MemberType)
  112. assert.Contains(t, resp.UserInfo.Perms, permCode)
  113. }
  114. // TC-0028: token无效
  115. func TestRefreshToken_InvalidToken(t *testing.T) {
  116. ctx := context.Background()
  117. svcCtx := newTestSvcCtx()
  118. logic := NewRefreshTokenLogic(ctx, svcCtx)
  119. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  120. Authorization: "Bearer invalid.token.string",
  121. })
  122. require.Nil(t, resp)
  123. require.Error(t, err)
  124. var codeErr *response.CodeError
  125. require.True(t, errors.As(err, &codeErr))
  126. assert.Equal(t, 401, codeErr.Code())
  127. assert.Equal(t, "refreshToken无效或已过期", codeErr.Error())
  128. }
  129. // TC-0029: 用户已被删除 —— M-1 修复后必须区分"不存在"(401) 与"冻结"(403)。
  130. //
  131. // 修复前:Loader 对不存在用户返回空壳 UserDetails(Status=0),RefreshToken 走到"账号已被冻结"分支 (403),
  132. //
  133. // 将"用户不存在"与"账号冻结"两个语义混淆,监控告警与运维处置策略无法区分。
  134. //
  135. // 修复后:Loader 返回 (ud, nil) 且 ud.Username == "",RefreshToken 显式回 401 "用户不存在或已被删除"。
  136. //
  137. // 这样客户端/前端才能走"注销本地会话 + 返回登录页"的终态流程,而不是提示"账号已冻结请联系管理员"。
  138. func TestRefreshToken_UserDeleted(t *testing.T) {
  139. ctx := context.Background()
  140. svcCtx := newTestSvcCtx()
  141. nonExistentUserId := int64(999999999)
  142. refreshToken, err := authHelper.GenerateRefreshToken(
  143. svcCtx.Config.Auth.RefreshSecret,
  144. svcCtx.Config.Auth.RefreshExpire,
  145. nonExistentUserId, "", 0,
  146. )
  147. require.NoError(t, err)
  148. logic := NewRefreshTokenLogic(ctx, svcCtx)
  149. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  150. Authorization: "Bearer " + refreshToken,
  151. })
  152. require.Nil(t, resp)
  153. require.Error(t, err)
  154. var codeErr *response.CodeError
  155. require.True(t, errors.As(err, &codeErr))
  156. assert.Equal(t, 401, codeErr.Code(), "M-1:用户不存在必须走 401,不得与冻结态 (403) 混淆")
  157. assert.Equal(t, "用户不存在或已被删除", codeErr.Error())
  158. }
  159. // TC-0030: 账号冻结
  160. func TestRefreshToken_AccountFrozen(t *testing.T) {
  161. ctx := context.Background()
  162. svcCtx := newTestSvcCtx()
  163. username := testutil.UniqueId()
  164. password := "TestPass123"
  165. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 2, 2)
  166. t.Cleanup(cleanUser)
  167. refreshToken, err := authHelper.GenerateRefreshToken(
  168. svcCtx.Config.Auth.RefreshSecret,
  169. svcCtx.Config.Auth.RefreshExpire,
  170. userId, "", 0,
  171. )
  172. require.NoError(t, err)
  173. logic := NewRefreshTokenLogic(ctx, svcCtx)
  174. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  175. Authorization: "Bearer " + refreshToken,
  176. })
  177. require.Nil(t, resp)
  178. require.Error(t, err)
  179. var codeErr *response.CodeError
  180. require.True(t, errors.As(err, &codeErr))
  181. assert.Equal(t, 403, codeErr.Code())
  182. assert.Equal(t, "账号已被冻结", codeErr.Error())
  183. }
  184. // TC-0032: 尝试切换产品被拒绝
  185. func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) {
  186. ctx := context.Background()
  187. svcCtx := newTestSvcCtx()
  188. username := testutil.UniqueId()
  189. password := "TestPass123"
  190. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  191. t.Cleanup(cleanUser)
  192. refreshToken, err := authHelper.GenerateRefreshToken(
  193. svcCtx.Config.Auth.RefreshSecret,
  194. svcCtx.Config.Auth.RefreshExpire,
  195. userId, "product_a", 0,
  196. )
  197. require.NoError(t, err)
  198. logic := NewRefreshTokenLogic(ctx, svcCtx)
  199. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  200. Authorization: "Bearer " + refreshToken,
  201. ProductCode: "product_b",
  202. })
  203. require.Nil(t, resp)
  204. require.Error(t, err)
  205. var codeErr *response.CodeError
  206. require.True(t, errors.As(err, &codeErr))
  207. assert.Equal(t, 400, codeErr.Code())
  208. assert.Equal(t, "刷新令牌不允许切换产品", codeErr.Error())
  209. }
  210. // TC-0033: TokenVersion不匹配时拒绝刷新
  211. func TestRefreshToken_TokenVersionMismatch(t *testing.T) {
  212. ctx := context.Background()
  213. svcCtx := newTestSvcCtx()
  214. username := testutil.UniqueId()
  215. password := "TestPass123"
  216. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  217. t.Cleanup(cleanUser)
  218. refreshToken, err := authHelper.GenerateRefreshToken(
  219. svcCtx.Config.Auth.RefreshSecret,
  220. svcCtx.Config.Auth.RefreshExpire,
  221. userId, "", 999,
  222. )
  223. require.NoError(t, err)
  224. logic := NewRefreshTokenLogic(ctx, svcCtx)
  225. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  226. Authorization: "Bearer " + refreshToken,
  227. })
  228. require.Nil(t, resp)
  229. require.Error(t, err)
  230. var codeErr *response.CodeError
  231. require.True(t, errors.As(err, &codeErr))
  232. assert.Equal(t, 401, codeErr.Code())
  233. assert.Equal(t, "登录状态已失效,请重新登录", codeErr.Error())
  234. }
  235. // TC-0034: 使用accessToken作为refreshToken被拒绝
  236. func TestRefreshToken_AccessTokenRejected(t *testing.T) {
  237. ctx := context.Background()
  238. svcCtx := newTestSvcCtx()
  239. username := testutil.UniqueId()
  240. password := "TestPass123"
  241. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  242. t.Cleanup(cleanUser)
  243. accessToken, err := authHelper.GenerateAccessToken(
  244. svcCtx.Config.Auth.RefreshSecret,
  245. svcCtx.Config.Auth.AccessExpire,
  246. userId, username, "", "", 0,
  247. )
  248. require.NoError(t, err)
  249. logic := NewRefreshTokenLogic(ctx, svcCtx)
  250. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  251. Authorization: "Bearer " + accessToken,
  252. })
  253. require.Nil(t, resp)
  254. require.Error(t, err)
  255. var codeErr *response.CodeError
  256. require.True(t, errors.As(err, &codeErr))
  257. assert.Equal(t, 401, codeErr.Code())
  258. assert.Equal(t, "refreshToken无效或已过期", codeErr.Error())
  259. }
  260. // TC-0035: 产品成员已移除时拒绝刷新
  261. func TestRefreshToken_MemberRemovedRejected(t *testing.T) {
  262. ctx := context.Background()
  263. svcCtx := newTestSvcCtx()
  264. conn := testutil.GetTestSqlConn()
  265. username := testutil.UniqueId()
  266. password := "TestPass123"
  267. pc := testutil.UniqueId()
  268. now := time.Now().Unix()
  269. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  270. t.Cleanup(cleanUser)
  271. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  272. t.Cleanup(cleanProduct)
  273. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  274. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now,
  275. })
  276. require.NoError(t, err)
  277. pmId, _ := pmRes.LastInsertId()
  278. refreshToken, err := authHelper.GenerateRefreshToken(
  279. svcCtx.Config.Auth.RefreshSecret,
  280. svcCtx.Config.Auth.RefreshExpire,
  281. userId, pc, 0,
  282. )
  283. require.NoError(t, err)
  284. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  285. logic := NewRefreshTokenLogic(ctx, svcCtx)
  286. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  287. Authorization: "Bearer " + refreshToken,
  288. })
  289. require.Nil(t, resp)
  290. require.Error(t, err)
  291. var codeErr *response.CodeError
  292. require.True(t, errors.As(err, &codeErr))
  293. assert.Equal(t, 403, codeErr.Code())
  294. assert.Equal(t, "您已不是该产品的成员", codeErr.Error())
  295. }
  296. // TC-0031: 超管+productCode(refreshToken原样返回)
  297. func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
  298. ctx := context.Background()
  299. svcCtx := newTestSvcCtx()
  300. conn := testutil.GetTestSqlConn()
  301. username := testutil.UniqueId()
  302. password := "TestPass123"
  303. pc := testutil.UniqueId()
  304. now := time.Now().Unix()
  305. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 1)
  306. t.Cleanup(cleanUser)
  307. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  308. t.Cleanup(cleanProduct)
  309. permCode := testutil.UniqueId()
  310. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  311. ProductCode: pc, Name: "sa_refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now,
  312. })
  313. require.NoError(t, err)
  314. permId, _ := permRes.LastInsertId()
  315. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  316. refreshToken, err := authHelper.GenerateRefreshToken(
  317. svcCtx.Config.Auth.RefreshSecret,
  318. svcCtx.Config.Auth.RefreshExpire,
  319. userId, pc, 0,
  320. )
  321. require.NoError(t, err)
  322. logic := NewRefreshTokenLogic(ctx, svcCtx)
  323. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  324. Authorization: "Bearer " + refreshToken,
  325. ProductCode: pc,
  326. })
  327. require.NoError(t, err)
  328. require.NotNil(t, resp)
  329. assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken")
  330. assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType)
  331. assert.Contains(t, resp.UserInfo.Perms, permCode)
  332. assert.Equal(t, int64(1), resp.UserInfo.IsSuperAdmin)
  333. }