Skip to content

feat(system/client): 添加双token认证配置#212

Open
luoqiz wants to merge 1 commit into
continew-org:devfrom
luoqiz:feat-refresh-token
Open

feat(system/client): 添加双token认证配置#212
luoqiz wants to merge 1 commit into
continew-org:devfrom
luoqiz:feat-refresh-token

Conversation

@luoqiz
Copy link
Copy Markdown
Contributor

@luoqiz luoqiz commented Apr 24, 2026

PR 类型

  • 新 feature
  • Bug 修复
  • 功能增强
  • 文档变更
  • 代码样式变更
  • 重构
  • 性能改进
  • 单元测试
  • CI/CD
  • 其他

PR 目的

解决方案

PR 测试

Changelog

模块 Changelog Related issues

其他信息

提交前确认

  • PR 代码经过了完整测试,并且通过了代码规范检查
  • 已经完整填写 Changelog,并链接到了相关 issues
  • PR 代码将要提交到 dev 分支

Summary by CodeRabbit

  • 新功能
    • 增加刷新令牌机制,支持双令牌模式(访问令牌 + 刷新令牌)或单令牌模式返回
    • 新增 /auth/refreshToken 接口以使用刷新令牌换取新登录响应
    • 客户端配置新增“启用刷新令牌”和“刷新令牌超时”设置
    • 数据库初始化包含刷新令牌相关字段与默认种子数据

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

为认证系统添加刷新令牌支持:数据库变更增加刷新令牌字段;新增单/双令牌响应 DTO;引入刷新令牌端点与服务实现;认证登录逻辑委托新建的 LoginHandler 构建响应并管理刷新令牌生命周期。

Changes

Cohort / File(s) Summary
数据库变更清单
continew-server/src/main/resources/db/changelog/db.changelog-master.yaml
为 MySQL 添加 includeAll 指令以加载 db/changelog/mysql/V4.2.0;为 PostgreSQL 恢复/保留注释的 plugin_generator.sqlincludeAll 条目(仍为注释)。
数据库迁移脚本
continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql, continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql
sys_client 表添加 is_enable_refresh_tokenrefresh_token_timeout 字段,并插入/初始化一个客户端记录(id=2)以填充新字段。
认证响应模型
continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java, .../SingleTokenLoginResp.java, .../DoubleTokenLoginResp.java
LoginResp@Builder 改为 @SuperBuilder 并移除原 token 字段;新增 SingleTokenLoginResp(token, expiresIn)与 DoubleTokenLoginResp(accessToken, refreshToken, accessExpiresIn, refreshExpiresIn)。
认证常量
continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
新增常量 LOGIN_USER = "loginUser",用于在登录额外数据中存储用户上下文。
登录处理逻辑
continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java, continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
authenticate 不再直接调用 Sa-Token 构建响应,改为准备 extraData 并调用新增静态方法 LoginHandler.buildLoginResp(...),该方法负责调用 Sa-Token、根据客户端配置生成单/双令牌并管理刷新令牌存储。
控制器与服务接口/实现
continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java, .../service/AuthService.java, .../service/impl/AuthServiceImpl.java
添加 POST /auth/refreshToken 接口与 AuthService.refreshToken(String) 方法;AuthServiceImpl.refreshToken 解析并校验刷新令牌、恢复用户上下文、构建新登录响应、并清理旧访问令牌及刷新令牌。
客户端实体与 DTO
continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java, .../model/req/ClientReq.java, .../model/resp/ClientResp.java
在 ClientDO、ClientReq、ClientResp 中添加 isEnableRefreshTokenrefreshTokenTimeout 字段以支持客户端级刷新令牌配置。

Sequence Diagram(s)

sequenceDiagram
    participant Client as 客户端
    participant AuthCtrl as AuthController
    participant AuthSvc as AuthService
    participant LoginHandler as LoginHandler
    participant SaTempUtil as SaTempUtil
    participant StpUtil as StpUtil

    Client->>AuthCtrl: POST /login (凭证)
    AuthCtrl->>AuthSvc: login(...)
    AuthSvc->>LoginHandler: buildLoginResp(loginParameter, userContext, client)

    alt 启用刷新令牌
        LoginHandler->>StpUtil: login() 创建访问令牌
        LoginHandler->>StpUtil: getTokenValue() 获取访问令牌值
        LoginHandler->>SaTempUtil: create/save 刷新令牌
        LoginHandler-->>AuthSvc: DoubleTokenLoginResp(access, refresh, expiries)
    else 未启用刷新令牌
        LoginHandler->>StpUtil: login() 创建访问令牌
        LoginHandler-->>AuthSvc: SingleTokenLoginResp(token, expiresIn)
    end

    AuthSvc-->>AuthCtrl: LoginResp
    AuthCtrl-->>Client: HTTP 200 + LoginResp
Loading
sequenceDiagram
    participant Client as 客户端
    participant AuthCtrl as AuthController
    participant AuthSvc as AuthService
    participant SaTempUtil as SaTempUtil
    participant LoginHandler as LoginHandler
    participant StpUtil as StpUtil

    Client->>AuthCtrl: POST /refreshToken (refreshToken=xxx)
    AuthCtrl->>AuthSvc: refreshToken(xxx)
    AuthSvc->>SaTempUtil: parseToken(xxx)

    alt 刷新令牌有效
        SaTempUtil-->>AuthSvc: SaLoginParameter / extraData (含 LOGIN_USER)
        AuthSvc->>LoginHandler: buildLoginResp(loginParameter, userContext, client)
        LoginHandler-->>AuthSvc: 新 LoginResp
        AuthSvc->>StpUtil: kickoutByTokenValue(旧访问令牌) (可选)
        AuthSvc->>SaTempUtil: deleteToken(xxx)
        AuthSvc-->>AuthCtrl: 新 LoginResp
    else 无效或过期
        AuthSvc-->>AuthCtrl: 抛出 BusinessException
    end

    AuthCtrl-->>Client: HTTP 200 / 4xx
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 新令牌跳跃来,访问与更新双飞扬,
存取分离妥善藏,旧令牌轻轻放行箱,
登录流水稳又亮,认证路上更安心。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 标题准确总结了核心变更:添加双 token 认证配置。标题简洁明确,与变更集内容高度相关,涵盖了新增 refresh token 相关表字段、模型、API 和业务逻辑的主要目的。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java (1)

21-45: ⚠️ Potential issue | 🟡 Minor

Swagger/OpenAPI 文档不完整,需要处理多态响应类型。

token 字段下沉到 SingleTokenLoginResp/DoubleTokenLoginResp 子类后,存在一个文档层面的问题:AuthControllerlogin()/refreshToken() 方法声明返回类型为 LoginResp 基类,导致 Swagger 生成的响应 Schema 仅包含 tenantId 字段,调用方通过 OpenAPI 文档无法看到实际响应中的 token/accessToken/refreshToken 等字段。

建议在 LoginResp 基类的 @Schema 注解中显式声明多态类型,例如:

`@Schema`(description = "登录响应参数", oneOf = {SingleTokenLoginResp.class, DoubleTokenLoginResp.class})

或在 Spring Boot 3.x + SpringDoc OpenAPI 环境中使用 @Schema(discriminatorProperty = "...") 结合子类标记,确保 API 文档准确反映实际响应结构。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java`
around lines 21 - 45, LoginResp's OpenAPI schema lacks the subclass token fields
because AuthController.login()/refreshToken() return the LoginResp base type;
update the LoginResp class to declare its polymorphic subtypes so Swagger sees
the actual response shapes — add a `@Schema` on LoginResp referencing oneOf =
{SingleTokenLoginResp.class, DoubleTokenLoginResp.class} or use
`@Schema`(discriminatorProperty = "...") with discriminator values on the
subclasses (SingleTokenLoginResp, DoubleTokenLoginResp) so the generated docs
include token/accessToken/refreshToken fields for the endpoints.
♻️ Duplicate comments (1)
continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java (1)

26-53: ⚠️ Potential issue | 🟡 Minor

同样需要 @EqualsAndHashCode(callSuper = true)

SingleTokenLoginResp 同样问题:@Data 不调用父类的 equals/hashCode,父类 LoginResp 的字段(如 tenantId)会被忽略。

 `@Data`
+@EqualsAndHashCode(callSuper = true)
 `@SuperBuilder`
 public class DoubleTokenLoginResp extends LoginResp {

说明:Swagger example 中给出的 JWT 字符串为文档示例占位,非真实凭据,可忽略静态扫描告警。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java`
around lines 26 - 53, DoubleTokenLoginResp (like SingleTokenLoginResp) currently
uses Lombok's `@Data` which generates equals/hashCode without calling the
superclass, causing LoginResp fields (e.g., tenantId) to be ignored; fix by
adding Lombok's `@EqualsAndHashCode`(callSuper = true) to the DoubleTokenLoginResp
class declaration so equals/hashCode include LoginResp fields (ensure the
import/annotation is added alongside `@Data/`@SuperBuilder).
🧹 Nitpick comments (3)
continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql (1)

1-21: PostgreSQL changeset 的若干小问题与幂等性建议。

  1. Line 8 缩进与 line 7 不一致(两空格 vs 四空格),建议统一。
  2. Line 17 INSERT INTO "sys_client" 未带 schema 前缀,而 line 6 ALTER TABLE "public"."sys_client" 带了,建议保持一致,避免在非默认 search_path 场景下出现歧义。
  3. id = 2 为硬编码主键,若目标环境已存在该行(例如此前手工初始化/从其它环境同步),该 INSERT 将失败并导致整条 changeset 回滚(包含 ALTER)。建议改为幂等写法。
  4. 整个 changeset 未提供 -- rollback 指令,Liquibase 回滚时只能依赖自动推断,对 DDL + DML 的组合不可靠。
建议的 diff
 -- liquibase formatted sql
 
 -- changeset luoqiz:4.2.0-1
 -- comment sys_client 客户端表更新
 -- sys_client 添加双token列
 ALTER TABLE "public"."sys_client"
     ADD COLUMN "is_enable_refresh_token" bool NOT NULL DEFAULT false,
-  ADD COLUMN "refresh_token_timeout" int8 DEFAULT 2592000;
+    ADD COLUMN "refresh_token_timeout" int8 DEFAULT 2592000;
 
 COMMENT
 ON COLUMN "public"."sys_client"."is_enable_refresh_token" IS '是否启用refresh token (true: 启用;  false: 禁用)';
 
 COMMENT
 ON COLUMN "public"."sys_client"."refresh_token_timeout" IS 'Refresh token有效期(单位:秒; 值必须大于0,否则取token的有效时长)';
 
 -- 初始化客户端数据
-INSERT INTO "sys_client"
+INSERT INTO "public"."sys_client"
 ("id", "client_id", "client_type", "auth_type", "active_timeout", "timeout", "status", "create_user", "create_time",
  "is_enable_refresh_token", "refresh_token_timeout")
 VALUES (2, 'ef51c9a3e9046c4f2ea45142c8a8344b', 'XCX', '["ACCOUNT", "EMAIL", "PHONE", "SOCIAL"]', 1800, 86400, 1, 1,
-        NOW(), true, 2592000);
+        NOW(), true, 2592000)
+ON CONFLICT ("id") DO NOTHING;
+
+-- rollback DELETE FROM "public"."sys_client" WHERE "id" = 2;
+-- rollback ALTER TABLE "public"."sys_client" DROP COLUMN "refresh_token_timeout";
+-- rollback ALTER TABLE "public"."sys_client" DROP COLUMN "is_enable_refresh_token";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql`
around lines 1 - 21, Fix formatting and make the changeset idempotent and
rollback-safe: normalize indentation for the ADD COLUMN lines, qualify the
INSERT with the same schema ("public"."sys_client"), and replace the hard-coded
INSERT that may fail with an idempotent upsert (e.g., use INSERT ... ON CONFLICT
(id) DO NOTHING or INSERT ... WHERE NOT EXISTS (SELECT 1 FROM
"public"."sys_client" WHERE id = 2) or better upsert by unique client_id) so
re-running won’t error; finally add an explicit rollback block that drops the
two columns ("is_enable_refresh_token", "refresh_token_timeout") and removes the
seeded row (by id or client_id) to ensure clean rollback of both DDL and DML.
continew-server/src/main/resources/db/changelog/db.changelog-master.yaml (1)

14-15: includeAll 按文件名字典序加载,建议规范 changeset 文件命名。

Liquibase includeAll 按字典序加载目录下的文件;当前使用的 v4.2.0_1_change.sql 若后续出现 v4.2.0_10_xxx.sql,会被排在 v4.2.0_2_xxx.sql 之前,可能导致与作者预期不一致的执行顺序。建议后续新增文件时用零填充(如 v4.2.0_001_change.sql),或在 includeAll 中显式配置 resourceComparator 以规避排序陷阱。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@continew-server/src/main/resources/db/changelog/db.changelog-master.yaml`
around lines 14 - 15, 当前使用的 includeAll (path: db/changelog/mysql/V4.2.0)
会按字典序加载文件,导致类似 v4.2.0_10_xxx.sql 排序在 v4.2.0_2_xxx.sql 之前,建议修复:要么统一重命名现有和后续
changeset 文件(例如将 v4.2.0_1_change.sql 改为 v4.2.0_001_change.sql,使用零填充以保证数值顺序),要么在
includeAll 配置中指定 resourceComparator 来控制加载顺序(例如按数字或自定义比较器);定位关键词 includeAll、path
db/changelog/mysql/V4.2.0、v4.2.0_1_change.sql 和 resourceComparator
来修改相应配置或重命名文件。
continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql (1)

6-15: 建议让 INSERT 幂等并补充 rollback 声明。

  1. Line 11-15 直接插入 id = 2,若目标环境已存在该主键(例如此前已手工写入或从其它环境同步),changeset 会因唯一键冲突失败,且上方 ALTER TABLE 一并回滚,产生“部分执行但全部失败”的困扰。建议改为 INSERT ... ON DUPLICATE KEY UPDATE id = idINSERT IGNORE,使之幂等。
  2. Liquibase 建议为每个 changeset 显式提供 -- rollback 指令,目前仅依赖自动推断,组合 DDL + DML 时不稳妥。
  3. Line 8 缩进与 line 7 不一致,建议统一格式。
建议的 diff
 ALTER TABLE `sys_client`
     ADD COLUMN `is_enable_refresh_token` bit NOT NULL DEFAULT FALSE COMMENT '是否启用refresh token (true: 启用;  false: 禁用)' AFTER `timeout`,
-ADD COLUMN `refresh_token_timeout` bigint NULL DEFAULT 2592000 COMMENT 'Refresh token有效期(单位:秒; 值必须大于0,否则取token的有效时长)' AFTER `is_enable_refresh_token`;
+    ADD COLUMN `refresh_token_timeout` bigint NULL DEFAULT 2592000 COMMENT 'Refresh token有效期(单位:秒; 值必须大于0,否则取token的有效时长)' AFTER `is_enable_refresh_token`;
 
 -- 初始化客户端数据
-INSERT INTO `sys_client`
+INSERT IGNORE INTO `sys_client`
 (`id`, `client_id`, `client_type`, `auth_type`, `active_timeout`, `timeout`, `status`, `create_user`, `create_time`,
  `is_enable_refresh_token`, `refresh_token_timeout`)
 VALUES (2, 'ef51c9a3e9046c4f2ea45142c8a8344b', 'XCX', '["ACCOUNT", "EMAIL", "PHONE", "SOCIAL"]', 1800, 86400, 1, 1,
         NOW(), b'1', 2592000);
+
+-- rollback DELETE FROM `sys_client` WHERE `id` = 2;
+-- rollback ALTER TABLE `sys_client` DROP COLUMN `refresh_token_timeout`, DROP COLUMN `is_enable_refresh_token`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql`
around lines 6 - 15, 当前 changeset 对 sys_client 的 INSERT 使用固定主键 id=2
会在目标库已存在该主键时报错并回滚,同时未提供显式 rollback。将 INSERT INTO `sys_client` ... VALUES(...)
修改为幂等写法(例如使用 INSERT ... ON DUPLICATE KEY UPDATE id = id 或 INSERT
IGNORE)以避免唯一键冲突,并在同一 changeset 中补充明确的 rollback 描述(rollback 应撤销 INSERT 的行并回退对表
`sys_client` 的列改动 `is_enable_refresh_token` 和
`refresh_token_timeout`,例如删除列与删除该行),另外统一调整 ALTER/ADD 语句的缩进(涉及 `ALTER TABLE
\`sys_client\`` 和新增列 `is_enable_refresh_token`/`refresh_token_timeout`)以保持格式一致。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java`:
- Around line 75-80: The refreshToken controller method currently accepts a
plain String parameter (public LoginResp refreshToken(String refreshToken)) with
no Spring binding annotation so POST JSON bodies will yield null; update the
method to explicitly bind the input—either change the signature to accept a DTO
with `@RequestBody` (e.g., RefreshTokenReq req) and extract req.getRefreshToken(),
or annotate the existing parameter with `@RequestParam` and validation (e.g.,
`@RequestParam` `@NotBlank` String refreshToken); also add a Swagger `@Parameter` or
Operation note for the chosen approach and update AuthController.refreshToken
and any related DTO (e.g., RefreshTokenReq) accordingly.

In `@continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java`:
- Around line 84-109: Rename the method bulderLoginResp to buildLoginResp and
update its two call sites in AuthServiceImpl and AbstractLoginHandler; inside
buildLoginResp replace the boolean check client.getIsEnableRefreshToken() with
Boolean.TRUE.equals(client.getIsEnableRefreshToken()), guard the nullable
refresh token timeout by reading Long rtTimeout =
client.getRefreshTokenTimeout() and compute refreshExpiresIn = (rtTimeout !=
null && rtTimeout > 0L) ? rtTimeout : client.getTimeout(), and reduce repeated
calls by caching StpUtil.getTokenValue(), StpUtil.getTokenTimeout(), and
tenantId (TenantContextHolder.isTenantEnabled() ?
TenantContextHolder.getTenantId() : null) into local variables before building
the response. Ensure loginParameter.setToken(...) still runs when saving the
refresh token.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java`:
- Around line 26-42: Add Lombok's `@EqualsAndHashCode`(callSuper = true) to the
SingleTokenLoginResp class so its generated equals/hashCode include parent
LoginResp fields, and update the expiresIn field's `@Schema` example from "8600"
to "86400" to reflect one day in seconds; target the class declaration
annotation list (SingleTokenLoginResp) and the expiresIn field's `@Schema`
attribute.

In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`:
- Around line 134-151: Refresh flow is unsafe: parseToken->cast->use stale
UserContext->kickout old token before creating new one and delete refresh token
unconditionally; fix by (1) safely parse and type-check SaTempUtil.parseToken
result and loginParameter (SaLoginParameter) and its extra UserContext with
instanceof checks and throw a BusinessException with distinct error codes for
expired vs invalid tokens; (2) reload fresh user data from userService (e.g.,
fetch by user id from parsed loginParameter) and rebuild UserContext before
setting UserContextHolder; (3) validate client via
clientService.getByClientId(userContext.getClientId()) for
null/disabled/refresh-disabled like login(...) does; (4) create the new session
/ LoginResp via LoginHandler.bulderLoginResp only after successful validation,
then atomically kick old session with
StpUtil.kickoutByTokenValue(loginParameter.getToken()) and remove old refresh
token with SaTempUtil.deleteToken(refreshToken) in the success/finally path so
failures don’t leave user logged out without a new token; also consider
concurrent-refresh protection by ensuring deleteToken/kickout happens after new
token creation or using a CAS/lock around refreshToken handling.

In
`@continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java`:
- Around line 79-90: Update the validation message on the isEnableRefreshToken
field to accurately reflect what is being validated by changing the `@NotNull`
message to something like "是否启用 Refresh Token 不能为空", and add a conditional
non-null validation for refreshTokenTimeout using the same pattern as
replacedRange/overflowLogoutMode (apply `@SpelNotNull` on refreshTokenTimeout with
a condition checking isEnableRefreshToken == true) so that when
isEnableRefreshToken is true the refreshTokenTimeout cannot be null.

In
`@continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java`:
- Around line 86-98: The ExcelProperty order is duplicated (order = 7) for
timeout, isEnableRefreshToken and refreshTokenTimeout and the Boolean field name
is problematic for JSON; rename the Boolean field isEnableRefreshToken to
enableRefreshToken (update any usages) to avoid Lombok/Jackson isXxx vs xxx
mismatch, remove the "is" prefix from the property and its annotations, and
reassign unique incremental ExcelProperty order values for enableRefreshToken
and refreshTokenTimeout (and shift any subsequent fields' order accordingly) to
eliminate the conflict; ensure any code referencing
getIsEnableRefreshToken/setIsEnableRefreshToken is updated to the new
getter/setter names for enableRefreshToken.

---

Outside diff comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java`:
- Around line 21-45: LoginResp's OpenAPI schema lacks the subclass token fields
because AuthController.login()/refreshToken() return the LoginResp base type;
update the LoginResp class to declare its polymorphic subtypes so Swagger sees
the actual response shapes — add a `@Schema` on LoginResp referencing oneOf =
{SingleTokenLoginResp.class, DoubleTokenLoginResp.class} or use
`@Schema`(discriminatorProperty = "...") with discriminator values on the
subclasses (SingleTokenLoginResp, DoubleTokenLoginResp) so the generated docs
include token/accessToken/refreshToken fields for the endpoints.

---

Duplicate comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java`:
- Around line 26-53: DoubleTokenLoginResp (like SingleTokenLoginResp) currently
uses Lombok's `@Data` which generates equals/hashCode without calling the
superclass, causing LoginResp fields (e.g., tenantId) to be ignored; fix by
adding Lombok's `@EqualsAndHashCode`(callSuper = true) to the DoubleTokenLoginResp
class declaration so equals/hashCode include LoginResp fields (ensure the
import/annotation is added alongside `@Data/`@SuperBuilder).

---

Nitpick comments:
In `@continew-server/src/main/resources/db/changelog/db.changelog-master.yaml`:
- Around line 14-15: 当前使用的 includeAll (path: db/changelog/mysql/V4.2.0)
会按字典序加载文件,导致类似 v4.2.0_10_xxx.sql 排序在 v4.2.0_2_xxx.sql 之前,建议修复:要么统一重命名现有和后续
changeset 文件(例如将 v4.2.0_1_change.sql 改为 v4.2.0_001_change.sql,使用零填充以保证数值顺序),要么在
includeAll 配置中指定 resourceComparator 来控制加载顺序(例如按数字或自定义比较器);定位关键词 includeAll、path
db/changelog/mysql/V4.2.0、v4.2.0_1_change.sql 和 resourceComparator
来修改相应配置或重命名文件。

In
`@continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql`:
- Around line 6-15: 当前 changeset 对 sys_client 的 INSERT 使用固定主键 id=2
会在目标库已存在该主键时报错并回滚,同时未提供显式 rollback。将 INSERT INTO `sys_client` ... VALUES(...)
修改为幂等写法(例如使用 INSERT ... ON DUPLICATE KEY UPDATE id = id 或 INSERT
IGNORE)以避免唯一键冲突,并在同一 changeset 中补充明确的 rollback 描述(rollback 应撤销 INSERT 的行并回退对表
`sys_client` 的列改动 `is_enable_refresh_token` 和
`refresh_token_timeout`,例如删除列与删除该行),另外统一调整 ALTER/ADD 语句的缩进(涉及 `ALTER TABLE
\`sys_client\`` 和新增列 `is_enable_refresh_token`/`refresh_token_timeout`)以保持格式一致。

In
`@continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql`:
- Around line 1-21: Fix formatting and make the changeset idempotent and
rollback-safe: normalize indentation for the ADD COLUMN lines, qualify the
INSERT with the same schema ("public"."sys_client"), and replace the hard-coded
INSERT that may fail with an idempotent upsert (e.g., use INSERT ... ON CONFLICT
(id) DO NOTHING or INSERT ... WHERE NOT EXISTS (SELECT 1 FROM
"public"."sys_client" WHERE id = 2) or better upsert by unique client_id) so
re-running won’t error; finally add an explicit rollback block that drops the
two columns ("is_enable_refresh_token", "refresh_token_timeout") and removes the
seeded row (by id or client_id) to ensure clean rollback of both DDL and DML.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a51cfe35-7739-4d24-97f1-16d91a40b1ce

📥 Commits

Reviewing files that changed from the base of the PR and between f6075ca and f361a38.

📒 Files selected for processing (15)
  • continew-server/src/main/resources/db/changelog/db.changelog-master.yaml
  • continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql
  • continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/service/AuthService.java
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java
  • continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java
  • continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java

Comment thread continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java (1)

41-41: ⚠️ Potential issue | 🟡 Minor

expiresIn@Schema 示例值疑似有误

example = "8600" 看起来应为 "86400"(一天 = 86400 秒),与常见的 token 有效期惯例一致,避免在 OpenAPI 文档中误导调用方。

🔧 建议修正
-    `@Schema`(description = "有效时长(秒)", example = "8600")
+    `@Schema`(description = "有效时长(秒)", example = "86400")
     private Long expiresIn;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java`
at line 41, Update the `@Schema` example for the expiresIn field in
SingleTokenLoginResp to the correct value: change the example from "8600" to
"86400" (one day in seconds) so the OpenAPI documentation for the expiresIn
property reflects the intended token lifetime; locate the expiresIn field and
its `@Schema` annotation in the SingleTokenLoginResp class and replace the example
value accordingly.
continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java (1)

134-151: ⚠️ Potential issue | 🔴 Critical

此前指出的健壮性/安全隐患仍未解决

对照上一轮评审,这段代码没有实质改动,下列问题依然存在(按严重度排序):

  1. client 没有非空/禁用校验就直接用(Line 144 → Line 147)。如果 userContext.getClientId() 对应的客户端已被删除或禁用,clientService.getByClientId(...) 会返回 null,随后 LoginHandler.buildLoginRespclient.getIsEnableRefreshToken() 会直接 NPE;被禁用的客户端理应拒绝刷新而不是继续签发新令牌。建议与 login(...) 保持同样的三条校验(非空 / 未禁用 / 仍启用 refresh token)。
  2. 先踢旧 Token、后建新会话(Line 146 先于 Line 147)。一旦 buildLoginResp 抛异常(DB 异常、Sa-Token 异常等),旧访问令牌已失效但新令牌未返回,用户被"静默登出"。同时 Line 149 的 SaTempUtil.deleteToken(refreshToken) 也要在成功路径末尾执行,异常路径下 Refresh Token 还能被再次使用,形成不一致窗口。建议顺序调整为:建新会话 → 成功后再 kickout 旧 access token + deleteToken 旧 refresh token
  3. 使用登录时快照的 UserContext(Line 142-143)。权限、角色、密码过期天数、启用状态等在登录后可能已变化,refresh 却无限复用旧快照,相当于权限回收被绕过。应基于 userContext.getId() 重新加载并重建 UserContext
  4. ClassCastException 风险(Line 141-142)。parseToken 反序列化结果若因版本升级/字段改动不匹配,两个强转都会抛出非业务异常。建议 instanceof 判断后抛 BusinessException
  5. 过期与无效无法区分(Line 136-139)。SaTempUtil.parseToken() 对两种情况都返回 null,当前统一报 无效的Refresh Token,前端无法区分是否需要引导用户重新登录 vs 直接提示过期。
  6. 并发刷新可签发两份新会话。同一个 Refresh Token 被并发调用两次时两个请求都能通过 parseToken,建议把 deleteToken 前置到 buildLoginResp 之前(或借助分布式锁 / CAS),确保并发中只有一个能继续。

这些全部是之前评审已指出但未处理的项,留作跟踪。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`
around lines 134 - 151, Refresh-token handling must be hardened: first validate
parseToken return and types (use instanceof on the result to ensure it's a
SaLoginParameter and that its extra is a UserContext, otherwise throw
BusinessException), then reload current UserContext from persistence using
userContext.getId() instead of reusing the login snapshot; next fetch the client
via clientService.getByClientId(...) and enforce the same three checks as login
(client non-null, not disabled, refresh-token feature enabled) and throw
BusinessException if any fail; reorder operations so you construct the new
session/response via LoginHandler.buildLoginResp(...) before calling
StpUtil.kickoutByTokenValue(...) or SaTempUtil.deleteToken(...), and only delete
the refresh token after buildLoginResp succeeds; finally, add concurrency
protection around deleteToken (e.g., distributed lock or CAS) to avoid
double-refresh races and, if possible, differentiate null/expired tokens from
invalid ones by invoking Sa-Token validation APIs or explicit checks so you can
return distinct BusinessException messages.
🧹 Nitpick comments (2)
continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java (1)

24-30: 建议与父类 LoginResp 保持元数据一致。

当前类头信息较为单薄,与父类 LoginResp(含 @author@since、类级 @SchemaserialVersionUID)和兄弟类 SingleTokenLoginResp 不一致,建议补齐:

  • 类级 @Schema(description = "双令牌登录响应参数"):父类上的 @Schema 不会被 springdoc 继承到子类,子类在 OpenAPI 文档中会缺失描述。
  • Javadoc 补充 @author / @since,与项目其他 DTO 风格保持一致。
  • 由于父类实现了 Serializable,建议子类显式声明 serialVersionUID,避免 JVM 版本差异导致的隐式计算不一致。
♻️ 建议的调整
 /**
- * 双token模式
+ * 双令牌登录响应参数
+ *
+ * `@author` luoqiz
+ * `@since` 2026/4/24
  */
 `@Data`
 `@SuperBuilder`
 `@EqualsAndHashCode`(callSuper = true)
+@Schema(description = "双令牌登录响应参数")
 public class DoubleTokenLoginResp extends LoginResp {
 
+    `@Serial`
+    private static final long serialVersionUID = 1L;
+
     /**
      * 访问令牌
      */

相应需补充 import:

 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.SuperBuilder;
+
+import java.io.Serial;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java`
around lines 24 - 30, Add the same metadata to DoubleTokenLoginResp as in
LoginResp/SingleTokenLoginResp: add a class-level `@Schema`(description =
"双令牌登录响应参数"), include Javadoc tags `@author` and `@since` above the class, and
declare a private static final long serialVersionUID with a stable value; ensure
imports for io.swagger.v3.oas.annotations.media.Schema and java.io.Serializable
are present and keep the class annotation set (`@Data`, `@SuperBuilder`,
`@EqualsAndHashCode`(callSuper = true)) intact so OpenAPI docs and serialization
behavior match the parent and sibling DTOs.
continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java (1)

76-81: 接口绑定已到位,可补充参数文档

@RequestParam("refreshToken") @NotBlank`` 已经解决了之前评论指出的绑定缺失问题,功能上没有问题。可选优化:

  • @Operation 的 summary/description 都是 刷新token,与其它接口(如 登录)的风格一致性一般,可以改为 刷新 Token / 使用 Refresh Token 换取新的访问令牌,方便 Swagger 文档阅读。
  • 缺少 @Parameter 描述,建议补一个便于前端联调。
  • 如果未来希望前端以 JSON Body 方式传参(移动端/网关场景常见),可以考虑改为 @RequestBody RefreshTokenReq
♻️ 可选优化
-    `@SaIgnore`
-    `@Operation`(summary = "刷新token", description = "刷新token")
-    `@PostMapping`("/refreshToken")
-    public LoginResp refreshToken(`@RequestParam`("refreshToken") `@NotBlank` String refreshToken) {
-        return authService.refreshToken(refreshToken);
-    }
+    `@SaIgnore`
+    `@Operation`(summary = "刷新 Token", description = "使用 Refresh Token 换取新的访问令牌")
+    `@Parameter`(name = "refreshToken", description = "刷新令牌", required = true, in = ParameterIn.QUERY)
+    `@PostMapping`("/refreshToken")
+    public LoginResp refreshToken(`@RequestParam`("refreshToken") `@NotBlank` String refreshToken) {
+        return authService.refreshToken(refreshToken);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java`
around lines 76 - 81, Update the AuthController.refreshToken endpoint metadata
to improve API docs: change `@Operation` summary/description to clearer text like
"刷新 Token" and "使用 Refresh Token 换取新的访问令牌", and add a `@Parameter` annotation
describing the refreshToken parameter for Swagger; keep the existing
`@RequestParam`("refreshToken") `@NotBlank` String refreshToken signature (or
optionally convert to `@RequestBody` RefreshTokenReq in future if switching to
JSON body).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@continew-server/src/main/resources/config/application-dev.yml`:
- Around line 15-17: Revert the personal-host and plaintext credentials in
application-dev.yml by replacing the hardcoded DB and Redis defaults
(DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PWD and Redis host/port/password/database)
with local-safe placeholders (e.g., 127.0.0.1 and non-sensitive defaults or
empty values) and restore the prior team dev defaults; ensure consumers rely on
environment variables or a non-committed override file (e.g.,
application-local.yml) for personal credentials, add that override filename to
.gitignore if missing, and if the exposed DB/Redis credentials are still active
rotate those credentials immediately.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java`:
- Around line 35-42: The `@Schema` example for refreshToken in class
DoubleTokenLoginResp currently duplicates accessToken; change the refreshToken
example to a distinct placeholder value (e.g.,
"refresh-token-placeholder-<unique>") so Swagger/OpenAPI shows different
examples for accessToken and refreshToken and to avoid static-scan confusion;
update the `@Schema` annotation on the refreshToken field (symbol: refreshToken)
accordingly while leaving accessToken's example unchanged.

---

Duplicate comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java`:
- Line 41: Update the `@Schema` example for the expiresIn field in
SingleTokenLoginResp to the correct value: change the example from "8600" to
"86400" (one day in seconds) so the OpenAPI documentation for the expiresIn
property reflects the intended token lifetime; locate the expiresIn field and
its `@Schema` annotation in the SingleTokenLoginResp class and replace the example
value accordingly.

In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`:
- Around line 134-151: Refresh-token handling must be hardened: first validate
parseToken return and types (use instanceof on the result to ensure it's a
SaLoginParameter and that its extra is a UserContext, otherwise throw
BusinessException), then reload current UserContext from persistence using
userContext.getId() instead of reusing the login snapshot; next fetch the client
via clientService.getByClientId(...) and enforce the same three checks as login
(client non-null, not disabled, refresh-token feature enabled) and throw
BusinessException if any fail; reorder operations so you construct the new
session/response via LoginHandler.buildLoginResp(...) before calling
StpUtil.kickoutByTokenValue(...) or SaTempUtil.deleteToken(...), and only delete
the refresh token after buildLoginResp succeeds; finally, add concurrency
protection around deleteToken (e.g., distributed lock or CAS) to avoid
double-refresh races and, if possible, differentiate null/expired tokens from
invalid ones by invoking Sa-Token validation APIs or explicit checks so you can
return distinct BusinessException messages.

---

Nitpick comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java`:
- Around line 76-81: Update the AuthController.refreshToken endpoint metadata to
improve API docs: change `@Operation` summary/description to clearer text like "刷新
Token" and "使用 Refresh Token 换取新的访问令牌", and add a `@Parameter` annotation
describing the refreshToken parameter for Swagger; keep the existing
`@RequestParam`("refreshToken") `@NotBlank` String refreshToken signature (or
optionally convert to `@RequestBody` RefreshTokenReq in future if switching to
JSON body).

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java`:
- Around line 24-30: Add the same metadata to DoubleTokenLoginResp as in
LoginResp/SingleTokenLoginResp: add a class-level `@Schema`(description =
"双令牌登录响应参数"), include Javadoc tags `@author` and `@since` above the class, and
declare a private static final long serialVersionUID with a stable value; ensure
imports for io.swagger.v3.oas.annotations.media.Schema and java.io.Serializable
are present and keep the class annotation set (`@Data`, `@SuperBuilder`,
`@EqualsAndHashCode`(callSuper = true)) intact so OpenAPI docs and serialization
behavior match the parent and sibling DTOs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e1dea10-b894-493d-87ad-f72e6610a412

📥 Commits

Reviewing files that changed from the base of the PR and between f361a38 and 9ab8438.

📒 Files selected for processing (7)
  • continew-server/src/main/resources/config/application-dev.yml
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java

Comment thread continew-server/src/main/resources/config/application-dev.yml Outdated
@luoqiz luoqiz force-pushed the feat-refresh-token branch from c9285bd to f8e111f Compare April 24, 2026 05:05
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java (1)

134-158: ⚠️ Potential issue | 🔴 Critical

刷新流程仍存在多处严重健壮性缺陷,且 finally 写法新增了「刷新失败即彻底登出」的事故路径

1. finally 块的清理语义反了(新增的严重风险)
kickoutByTokenValue(oldToken) + SaTempUtil.deleteToken(refreshToken) 放入 finally,意味着只要 bulderLoginResp(...) 中途抛出异常(例如下一条提到的 client == null 导致 client.getIsEnableRefreshToken() NPE、DB 抖动、StpUtil.login 内部异常等),就会出现两种糟糕的后果:

  • 抛异常发生在 StpUtil.login 之前:用户旧 Access Token 被踢出 + Refresh Token 被删除,但没有拿到新会话,彻底登出且无法自救。
  • 抛异常发生在 StpUtil.login 之后、return 之前:后台已经创建了新会话,但客户端没收到响应;旧会话被销毁,Refresh Token 也被销毁,服务器上残留一个客户端永远不会使用的孤儿会话。

正确的语义是「只有刷新成功后才清理旧凭据」。把两行清理移出 finally,放在 return 之前(或包装到成功路径末尾的原子步骤中)。

2. client 未判空 / 未校验状态(过去评审已提出,仍未处理)
clientService.getByClientId(userContext.getClientId()) 可能返回 null;也没有校验 DisEnableStatusEnum.DISABLEisEnableRefreshTokenlogin(...) 方法在 Line 80-81 已有成熟校验,刷新路径应对齐,否则「禁用客户端」或「已关闭刷新令牌」的情况仍可刷新成功。

3. 类型强转未做 instanceof 防御(过去评审已提出,仍未处理)
(SaLoginParameter) refreshTokenInfo(UserContext) loginParameter.getExtra(AuthConstants.LOGIN_USER) 在序列化结构变更 / 不同版本 Token 混用时会抛裸 ClassCastException

4. UserContext 陈旧导致权限回收绕过(过去评审已提出,仍未处理)
直接复用登录时快照的 UserContext(权限、角色、启用状态、租户等)。用户被禁用、角色被调整后,仍可凭旧 Refresh Token 无限刷出含原权限的 Access Token。应通过 userContext.getId() 重新加载用户并重建 UserContext(与 AbstractLoginHandler.authenticate 一致的路径),同时校验用户启用状态。

5. 过期 vs 无效无法区分(过去评审已提出,仍未处理)
SaTempUtil.parseToken 对过期与非法 Token 均返回 null,统一抛 BusinessException("无效的Refresh Token") 会让前端失去"过期请重新登录"与"非法令牌"场景的差异化处理。建议使用专用错误码。

🛠️ 建议的修复方向
 `@Override`
 public LoginResp refreshToken(String refreshToken) {
     Object refreshTokenInfo = SaTempUtil.parseToken(refreshToken);
-    if (refreshTokenInfo == null) {
-        throw new BusinessException("无效的Refresh Token");
+    if (!(refreshTokenInfo instanceof SaLoginParameter loginParameter)) {
+        // 过期或无效,建议使用独立错误码以便前端区分
+        throw new BusinessException("Refresh Token 已过期或无效");
     }
-    String oldToken = null;
-    try {
-        // 获取登录时设置的参数
-        SaLoginParameter loginParameter = (SaLoginParameter) refreshTokenInfo;
-        UserContext userContext = (UserContext) loginParameter.getExtra(AuthConstants.LOGIN_USER);
-        UserContextHolder.setContext(userContext);
-        ClientResp client = clientService.getByClientId(userContext.getClientId());
-        oldToken = loginParameter.getToken();
-        LoginResp loginResp = LoginHandler.buildLoginResp(loginParameter, userContext, client);
-        return loginResp;
-    } finally {
-        if (oldToken != null) {
-            // 删除原先的 Token
-            StpUtil.kickoutByTokenValue(oldToken);
-        }
-        // 删除原先的 Refresh token
-        SaTempUtil.deleteToken(refreshToken);
-    }
+    Object loginUser = loginParameter.getExtra(AuthConstants.LOGIN_USER);
+    if (!(loginUser instanceof UserContext staleContext)) {
+        throw new BusinessException("Refresh Token 数据结构异常");
+    }
+    // 校验客户端(对齐 login(...) 的校验)
+    ClientResp client = clientService.getByClientId(staleContext.getClientId());
+    ValidationUtils.throwIfNull(client, "客户端不存在");
+    ValidationUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(client.getStatus()), "客户端已禁用");
+    ValidationUtils.throwIf(!Boolean.TRUE.equals(client.getIsEnableRefreshToken()), "该客户端未开启刷新令牌");
+    // TODO: 根据 staleContext.getId() 重新从库中加载用户并重建 UserContext,
+    //       校验启用状态、重新拉取权限/角色,避免使用陈旧快照导致权限回收被绕过。
+    UserContext userContext = reloadUserContext(staleContext, client);
+    UserContextHolder.setContext(userContext);
+    String oldToken = loginParameter.getToken();
+    // 仅在成功构建新会话后,再清理旧凭据(非 finally)
+    LoginResp loginResp = LoginHandler.buildLoginResp(loginParameter, userContext, client);
+    if (oldToken != null) {
+        StpUtil.kickoutByTokenValue(oldToken);
+    }
+    SaTempUtil.deleteToken(refreshToken);
+    return loginResp;
 }

若有并发刷新担忧(同一 Refresh Token 被并发调用两次),可在 parseToken 成功后立即 deleteToken,并通过删除动作的结果/缓存原子操作进行「最多一次消费」语义的判定,再继续后续构建。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`
around lines 134 - 158, The refreshToken method currently parses the refresh
token and performs cleanup in a finally block causing logout on any intermediate
failure; fix by: validate parse result with instanceof checks for
SaLoginParameter and its extra as UserContext, reload fresh UserContext from
user id (not reuse the serialized snapshot) and validate user enabled status,
call clientService.getByClientId(userContext.getClientId()) and assert client !=
null and client is enabled and client.getIsEnableRefreshToken() before
proceeding, perform the build/login steps (LoginHandler.buildLoginResp /
StpUtil.login) and only after successful completion remove the old token via
StpUtil.kickoutByTokenValue(oldToken) and delete the refresh token via
SaTempUtil.deleteToken(refreshToken) (move those out of finally into the success
path), and change error handling from a single "无效的Refresh Token" to distinct
error codes/messages for expired vs invalid tokens (use parseToken result or
SaTempUtil semantics) and consider deleting the temp token immediately after
successful parse for at-most-once consumption to guard against concurrent
refresh calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`:
- Around line 134-158: The refreshToken method currently parses the refresh
token and performs cleanup in a finally block causing logout on any intermediate
failure; fix by: validate parse result with instanceof checks for
SaLoginParameter and its extra as UserContext, reload fresh UserContext from
user id (not reuse the serialized snapshot) and validate user enabled status,
call clientService.getByClientId(userContext.getClientId()) and assert client !=
null and client is enabled and client.getIsEnableRefreshToken() before
proceeding, perform the build/login steps (LoginHandler.buildLoginResp /
StpUtil.login) and only after successful completion remove the old token via
StpUtil.kickoutByTokenValue(oldToken) and delete the refresh token via
SaTempUtil.deleteToken(refreshToken) (move those out of finally into the success
path), and change error handling from a single "无效的Refresh Token" to distinct
error codes/messages for expired vs invalid tokens (use parseToken result or
SaTempUtil semantics) and consider deleting the temp token immediately after
successful parse for at-most-once consumption to guard against concurrent
refresh calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff436e0b-e22c-4441-8e71-76b7c056c312

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab8438 and e533100.

📒 Files selected for processing (2)
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java (1)

76-84: Javadoc 不完整 & 静态工具方法放在接口中欠妥

  1. @return 标签内容为空,Checkstyle / IDE 会提示,请补充说明(例如 "登录响应参数,启用刷新令牌时返回 DoubleTokenLoginResp,否则返回 SingleTokenLoginResp")。
  2. buildLoginResp 是一个与 LoginHandler 契约无直接关系的工具方法,目前以 static 形式挂在接口上,调用方 AbstractLoginHandler / AuthServiceImpl 都需通过 LoginHandler.buildLoginResp(...) 调用,语义上更像"登录响应构造器"。建议抽到独立的工具类(如 LoginRespBuilder 或放入 AbstractLoginHandler 作为 protected static),让 LoginHandler 接口保持单一职责。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java`
around lines 76 - 84, The Javadoc for buildLoginResp in the LoginHandler
interface is incomplete (missing `@return`) and the static utility method doesn't
belong on the interface; update the Javadoc to describe the returned LoginResp
(e.g., "登录响应参数,启用刷新令牌时返回 DoubleTokenLoginResp,否则返回 SingleTokenLoginResp") and
move buildLoginResp out of the LoginHandler interface into a dedicated
utility/constructor location (create a LoginRespBuilder class or make it a
protected static method on AbstractLoginHandler) then update callers
(AbstractLoginHandler and AuthServiceImpl) to call the new
LoginRespBuilder.build(...) or AbstractLoginHandler.buildLoginResp(...)
accordingly.
continew-server/src/main/resources/db/changelog/db.changelog-master.yaml (1)

14-15: includeAll 目录加载顺序依赖文件名排序,建议固化命名约定

Liquibase 的 includeAll 按文件名自然顺序加载目录下所有 changelog 文件。当前 v4.2.0_1_change.sql 命名能正确排序,但后续往 V4.2.0 目录中追加文件时(如 v4.2.0_10_xxx.sql),字典序可能与预期执行顺序不一致。建议:

  • 采用固定位数的序号(如 v4.2.0_001_change.sql),或
  • 显式指定 relativeToChangelogFile: true 并在文档中明确命名规范,避免后续协作者破坏执行顺序。

此外建议同步在 PostgreSQL 代码块中将 includeAll 取消注释(目前仍注释,可能导致 PG 环境下刷新令牌列缺失)——或至少在 README/升级文档中说明需手动启用。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@continew-server/src/main/resources/db/changelog/db.changelog-master.yaml`
around lines 14 - 15, 当前 db.changelog-master.yaml 中使用 includeAll: path:
db/changelog/mysql/V4.2.0 依赖文件名自然排序,可能导致像 v4.2.0_10_xxx.sql
排序异常;请在文档和模板中固化命名规则(例如 改为 v4.2.0_001_change.sql 固定位数序号)或改为显式使用带序号前缀的命名约定,并在仓库
README/升级文档中写明;同时检查 PostgreSQL 部分(当前为注释状态的 includeAll 块),将其取消注释以同步包含相应 changelog
或在文档中明确说明需要手动启用 includeAll,确保 refresh_token 列在 PG 环境下不会缺失。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql`:
- Around line 6-8: Update the new column definition on table sys_client to match
existing boolean-style columns: change the type for is_enable_refresh_token from
bit to bit(1) and change its default from DEFAULT FALSE to DEFAULT b'0' so it
matches is_concurrent's bit(1) and DEFAULT b'1' style; adjust the ALTER TABLE
statement that adds is_enable_refresh_token accordingly and keep
refresh_token_timeout as-is.

In `@continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java`:
- Around line 90-93: The refresh-token flow currently serializes loginParameter
(which contains a cached UserContext) via SaTempUtil.saveToken and then in
AuthServiceImpl#refreshToken restores UserContext from
loginParameter.getExtra(AuthConstants.LOGIN_USER), allowing stale or disabled
user state to persist; change the refresh logic to ignore the cached UserContext
from loginParameter and instead reload the current UserContext from the database
by user id (or at minimum verify the user's enabled/tenant/soft-delete status)
before issuing a new access token, updating references around loginParameter,
SaTempUtil.saveToken, and AuthServiceImpl#refreshToken to use the freshly
loaded/validated UserContext.

---

Nitpick comments:
In `@continew-server/src/main/resources/db/changelog/db.changelog-master.yaml`:
- Around line 14-15: 当前 db.changelog-master.yaml 中使用 includeAll: path:
db/changelog/mysql/V4.2.0 依赖文件名自然排序,可能导致像 v4.2.0_10_xxx.sql
排序异常;请在文档和模板中固化命名规则(例如 改为 v4.2.0_001_change.sql 固定位数序号)或改为显式使用带序号前缀的命名约定,并在仓库
README/升级文档中写明;同时检查 PostgreSQL 部分(当前为注释状态的 includeAll 块),将其取消注释以同步包含相应 changelog
或在文档中明确说明需要手动启用 includeAll,确保 refresh_token 列在 PG 环境下不会缺失。

In `@continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java`:
- Around line 76-84: The Javadoc for buildLoginResp in the LoginHandler
interface is incomplete (missing `@return`) and the static utility method doesn't
belong on the interface; update the Javadoc to describe the returned LoginResp
(e.g., "登录响应参数,启用刷新令牌时返回 DoubleTokenLoginResp,否则返回 SingleTokenLoginResp") and
move buildLoginResp out of the LoginHandler interface into a dedicated
utility/constructor location (create a LoginRespBuilder class or make it a
protected static method on AbstractLoginHandler) then update callers
(AbstractLoginHandler and AuthServiceImpl) to call the new
LoginRespBuilder.build(...) or AbstractLoginHandler.buildLoginResp(...)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b752908-cfb6-4e32-b097-765c20120fd1

📥 Commits

Reviewing files that changed from the base of the PR and between e533100 and f8e111f.

📒 Files selected for processing (15)
  • continew-server/src/main/resources/db/changelog/db.changelog-master.yaml
  • continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql
  • continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/service/AuthService.java
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java
  • continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java
  • continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java
✅ Files skipped from review due to trivial changes (2)
  • continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java
🚧 Files skipped from review as they are similar to previous changes (8)
  • continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java
  • continew-system/src/main/java/top/continew/admin/auth/service/AuthService.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java
  • continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java

@luoqiz luoqiz force-pushed the feat-refresh-token branch from f8e111f to 3b0f6e7 Compare April 24, 2026 06:27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java (1)

32-45: ⚠️ Potential issue | 🟡 Minor

注意:LoginResp 字段重构为继承体系,需确认前端反序列化兼容性。

LoginResp 基类现仅保留 tenantId 字段,token/expiresIn 已迁移至子类 SingleTokenLoginResp(单 token 模式),而 DoubleTokenLoginResp 则采用 accessToken/refreshToken(双 token 模式)。运行时根据 client.getIsEnableRefreshToken 配置动态返回相应子类,这对前端反序列化可能造成影响:

  • 若前端强类型化为 LoginResp 并直接访问 token/expiresIn 字段,会因字段不存在而报错。
  • 若前端采用多态反序列化或使用 SingleTokenLoginResp/DoubleTokenLoginResp 子类型,则不受影响。

请确认:(1) 前端反序列化策略已适配此继承体系(如支持多态或直接使用子类);(2) CHANGELOG 已补充此变更说明;(3) API 文档已明确标注响应类型为子类而非基类。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java`
around lines 32 - 45, LoginResp was refactored into a base class and now only
contains tenantId while token/expiresIn moved to SingleTokenLoginResp and
accessToken/refreshToken to DoubleTokenLoginResp; verify and update consumers
and docs: ensure frontend deserialization handles polymorphism or uses the
correct subtype (SingleTokenLoginResp/DoubleTokenLoginResp) depending on
client.getIsEnableRefreshToken, update API docs to state responses are
subtype-specific rather than LoginResp, and add a CHANGELOG entry describing
this breaking change so integrators know to switch from accessing
token/expiresIn on LoginResp to the appropriate subclass fields.
♻️ Duplicate comments (1)
continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java (1)

134-154: ⚠️ Potential issue | 🔴 Critical

刷新令牌流程仍有多项健壮性/安全性问题未完成修复。

  1. client 为空 / 已禁用未校验:Line 144 clientService.getByClientId(userContext.getClientId()) 后直接传入 buildLoginResp,若客户端被删除 / 禁用 / 关闭了 refresh token,仍会签发新令牌,且在 buildLoginResp 内对 client.getIsEnableRefreshToken() 的调用会直接 NPE。应比照 login(...) 方法对 clientnullDisEnableStatusEnum.DISABLEisEnableRefreshToken 的校验。
  2. UserContext 陈旧:直接从 loginParameter.getExtra(AuthConstants.LOGIN_USER) 取登录时快照,刷新窗口内用户角色/权限/禁用状态/租户变化不会生效,相当于权限回收被绕过。
  3. ClassCastException 风险:Line 141、142 的强转没有类型判断(且取决于 Sa-Token 的序列化器,反序列化后多为 LinkedHashMap),建议增加 instanceof 保护并抛出业务异常。
  4. 并发刷新未防护SaTempUtil.deleteToken(refreshToken) 在 Line 152 才执行,同一 refresh token 的并发请求都能通过 parseToken 得到有效结果,会生成两份新会话。建议在 parseToken 成功后立即 deleteToken(并结合删除结果判定),再进入 buildLoginResp
  5. 过期与无效无法区分parseToken 返回 null 时无法区分“过期”与“无效”,前端难以针对性提示;建议引入专用错误码。

上述问题在前次评审已提出,本次 commit 未体现修复。

🛠️ 建议修改骨架
 `@Override`
 public LoginResp refreshToken(String refreshToken) {
     Object refreshTokenInfo = SaTempUtil.parseToken(refreshToken);
-    if (refreshTokenInfo == null) {
-        throw new BusinessException("无效的Refresh Token");
-    }
-    // 获取登录时设置的参数
-    SaLoginParameter loginParameter = (SaLoginParameter) refreshTokenInfo;
-    UserContext userContext = (UserContext) loginParameter.getExtra(AuthConstants.LOGIN_USER);
-    UserContextHolder.setContext(userContext);
-    ClientResp client = clientService.getByClientId(userContext.getClientId());
-    String oldToken = loginParameter.getToken();
-    LoginResp loginResp = LoginHandler.buildLoginResp(loginParameter, userContext, client);
-    if (oldToken != null) {
-        // 删除原先的 Token
-        StpUtil.kickoutByTokenValue(oldToken);
-    }
-    // 删除原先的 Refresh token
-    SaTempUtil.deleteToken(refreshToken);
-    return loginResp;
+    if (!(refreshTokenInfo instanceof SaLoginParameter loginParameter)) {
+        throw new BusinessException("Refresh Token 已失效或无效");
+    }
+    // 先删 refresh token,防止并发重复刷新
+    SaTempUtil.deleteToken(refreshToken);
+    Object cached = loginParameter.getExtra(AuthConstants.LOGIN_USER);
+    Long userId = (cached instanceof UserContext uc) ? uc.getId() : null;
+    if (userId == null) {
+        throw new BusinessException("Refresh Token 已失效或无效");
+    }
+    // 重新加载用户与客户端,校验状态
+    UserContext userContext = userService.reloadUserContext(userId); // 需新增/复用
+    ClientResp client = clientService.getByClientId(userContext.getClientId());
+    ValidationUtils.throwIfNull(client, "客户端不存在");
+    ValidationUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(client.getStatus()), "客户端已禁用");
+    ValidationUtils.throwIf(!Boolean.TRUE.equals(client.getIsEnableRefreshToken()), "客户端未启用 Refresh Token");
+    UserContextHolder.setContext(userContext);
+    String oldToken = loginParameter.getToken();
+    LoginResp loginResp = LoginHandler.buildLoginResp(loginParameter, userContext, client);
+    if (oldToken != null) {
+        StpUtil.kickoutByTokenValue(oldToken);
+    }
+    return loginResp;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`
around lines 134 - 154, The refreshToken method must harden several failure
modes: after calling SaTempUtil.parseToken(refreshToken) validate the parsed
object types with instanceof (ensure it's a SaLoginParameter and the extracted
extra is a UserContext) and throw a BusinessException on mismatch; immediately
attempt SaTempUtil.deleteToken(refreshToken) right after successful parse and
proceed only if deletion indicates the token was valid to prevent concurrent
double-refresh; re-fetch current user state (do not use the snapshot from
loginParameter.getExtra) and set UserContextHolder with the fresh UserContext so
role/permission/disabled/tenant changes are honored; validate client returned
from clientService.getByClientId(userContext.getClientId()) for null,
DisEnableStatusEnum.DISABLE and client.getIsEnableRefreshToken() before calling
LoginHandler.buildLoginResp; and map parseToken null results to distinct
BusinessException error codes/messages for “expired” vs “invalid” so the
front-end can react appropriately.
🧹 Nitpick comments (2)
continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql (1)

6-8: 列定义的缩进不一致。

Line 7 使用 4 空格缩进,Line 8 使用 2 空格缩进,建议保持一致以便阅读。

♻️ 建议修改
 ALTER TABLE "public"."sys_client"
     ADD COLUMN "is_enable_refresh_token" bool NOT NULL DEFAULT false,
-  ADD COLUMN "refresh_token_timeout" int8 DEFAULT 2592000;
+    ADD COLUMN "refresh_token_timeout" int8 DEFAULT 2592000;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql`
around lines 6 - 8, The ALTER TABLE block for "public"."sys_client" has
inconsistent indentation between the two column lines; update the SQL so both
ADD COLUMN lines for "is_enable_refresh_token" and "refresh_token_timeout" use
the same indentation level (choose and apply a consistent number of spaces,
e.g., 2 or 4) to improve readability around the ALTER TABLE
"public"."sys_client" statement.
continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java (1)

71-79: 字段注释与实际语义不完全一致。

refreshTokenTimeout 的 Javadoc 写的是“默认 2,592,000”,但实际实现(LoginHandler.buildLoginResp)使用的语义是:当值 <= 0 或为 null 时,回退为 timeout。建议与 ClientResp@Schema(description = "Refresh Token 有效期(单位:秒; 小于0,则和 Token 有效期相同)") 的描述保持一致,避免读者误解。

♻️ 建议修改
     /**
-     * Refresh Token 有效期(单位:秒,默认 2,592,000)
+     * Refresh Token 有效期(单位:秒,小于等于 0 或为 null 时取 Token 有效期)
      */
     private Long refreshTokenTimeout;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java`
around lines 71 - 79, Update the Javadoc for the ClientDO.refreshTokenTimeout
field to match its actual semantics used in LoginHandler.buildLoginResp and the
description in ClientResp: state the unit (seconds), and that null or values <=
0 mean "use the same timeout as the access Token" (i.e., fallback to token
timeout) rather than claiming a fixed default of 2,592,000; reference the field
name refreshTokenTimeout and related LoginHandler.buildLoginResp behavior so
readers understand the fallback rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java`:
- Around line 140-144: The refresh-token flow risks ClassCastException because
UserContext is stored in extraData and later cast via (UserContext)
loginParameter.getExtra(AuthConstants.LOGIN_USER) (see
AuthServiceImpl#refreshToken and AbstractLoginHandler), and storing full
UserContext in the refresh token bloats data and stale permissions; fix by
storing only the minimal identifier(s) (e.g., userId) in
loginParameter.extraData instead of the full UserContext when building the token
in AbstractLoginHandler (remove putting UserContext into extraData), and update
AuthServiceImpl#refreshToken to read the userId from
loginParameter.getExtra(AuthConstants.LOGIN_USER) (or a new key like
AuthConstants.LOGIN_USER_ID), then rehydrate the full UserContext from the
database or user service before building the new token, ensuring any
parse/deserialization via SaTempUtil.parseToken / SaLoginParameter never
requires casting a deserialized map to UserContext.

---

Outside diff comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java`:
- Around line 32-45: LoginResp was refactored into a base class and now only
contains tenantId while token/expiresIn moved to SingleTokenLoginResp and
accessToken/refreshToken to DoubleTokenLoginResp; verify and update consumers
and docs: ensure frontend deserialization handles polymorphism or uses the
correct subtype (SingleTokenLoginResp/DoubleTokenLoginResp) depending on
client.getIsEnableRefreshToken, update API docs to state responses are
subtype-specific rather than LoginResp, and add a CHANGELOG entry describing
this breaking change so integrators know to switch from accessing
token/expiresIn on LoginResp to the appropriate subclass fields.

---

Duplicate comments:
In
`@continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java`:
- Around line 134-154: The refreshToken method must harden several failure
modes: after calling SaTempUtil.parseToken(refreshToken) validate the parsed
object types with instanceof (ensure it's a SaLoginParameter and the extracted
extra is a UserContext) and throw a BusinessException on mismatch; immediately
attempt SaTempUtil.deleteToken(refreshToken) right after successful parse and
proceed only if deletion indicates the token was valid to prevent concurrent
double-refresh; re-fetch current user state (do not use the snapshot from
loginParameter.getExtra) and set UserContextHolder with the fresh UserContext so
role/permission/disabled/tenant changes are honored; validate client returned
from clientService.getByClientId(userContext.getClientId()) for null,
DisEnableStatusEnum.DISABLE and client.getIsEnableRefreshToken() before calling
LoginHandler.buildLoginResp; and map parseToken null results to distinct
BusinessException error codes/messages for “expired” vs “invalid” so the
front-end can react appropriately.

---

Nitpick comments:
In
`@continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql`:
- Around line 6-8: The ALTER TABLE block for "public"."sys_client" has
inconsistent indentation between the two column lines; update the SQL so both
ADD COLUMN lines for "is_enable_refresh_token" and "refresh_token_timeout" use
the same indentation level (choose and apply a consistent number of spaces,
e.g., 2 or 4) to improve readability around the ALTER TABLE
"public"."sys_client" statement.

In
`@continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java`:
- Around line 71-79: Update the Javadoc for the ClientDO.refreshTokenTimeout
field to match its actual semantics used in LoginHandler.buildLoginResp and the
description in ClientResp: state the unit (seconds), and that null or values <=
0 mean "use the same timeout as the access Token" (i.e., fallback to token
timeout) rather than claiming a fixed default of 2,592,000; reference the field
name refreshTokenTimeout and related LoginHandler.buildLoginResp behavior so
readers understand the fallback rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 541cf9ea-9c1a-483e-a652-7dcef702a177

📥 Commits

Reviewing files that changed from the base of the PR and between f8e111f and 3b0f6e7.

📒 Files selected for processing (15)
  • continew-server/src/main/resources/db/changelog/db.changelog-master.yaml
  • continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql
  • continew-server/src/main/resources/db/changelog/postgresql/V4.2.0/v4.2.0_1_change.sql
  • continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/LoginHandler.java
  • continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/DoubleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/LoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/auth/service/AuthService.java
  • continew-system/src/main/java/top/continew/admin/auth/service/impl/AuthServiceImpl.java
  • continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java
  • continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java
✅ Files skipped from review due to trivial changes (2)
  • continew-system/src/main/java/top/continew/admin/auth/constant/AuthConstants.java
  • continew-server/src/main/resources/db/changelog/mysql/V4.2.0/v4.2.0_1_change.sql
🚧 Files skipped from review as they are similar to previous changes (5)
  • continew-system/src/main/java/top/continew/admin/auth/service/AuthService.java
  • continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java
  • continew-system/src/main/java/top/continew/admin/auth/model/resp/SingleTokenLoginResp.java
  • continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java
  • continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java

Comment on lines +140 to +144
Map<String, Object> extraData = BeanUtil.beanToMap(new UserExtraContext(ServletUtils.getRequest()));
extraData.put(AuthConstants.LOGIN_USER, userContext);
loginParameter.setExtraData(extraData);
UserContextHolder.setContext(userContext);
return LoginResp.builder()
.token(StpUtil.getTokenValue())
.tenantId(TenantContextHolder.isTenantEnabled() ? TenantContextHolder.getTenantId() : null)
.build();
return LoginHandler.buildLoginResp(loginParameter, userContext, client);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 确认 Sa-Token 的序列化器配置,判断 getExtra 取出的实际类型
rg -nP --type=java -C3 'SaJsonTemplate|SaSerializerTemplate|sa-token.*serializer'
rg -nP --type=yaml -C3 'sa-token|satoken'

Repository: continew-org/continew-admin

Length of output: 3762


🏁 Script executed:

# Find AuthServiceImpl and check refreshToken method
fd -type f -name "AuthServiceImpl.java" | head -5

Repository: continew-org/continew-admin

Length of output: 242


🏁 Script executed:

# Find UserContext class definition
fd -type f -name "UserContext.java" | head -5

Repository: continew-org/continew-admin

Length of output: 242


🏁 Script executed:

# Check Sa-Token version and serializer configuration in pom.xml
rg -nP --type=xml 'sa-token|satoken' | head -20

Repository: continew-org/continew-admin

Length of output: 343


🏁 Script executed:

# Check for Sa-Token serializer configuration in Java code
rg -nP --type=java 'SaSerializerTemplate|SaJsonTemplate|setJsonSerializer|parseToken' | head -20

Repository: continew-org/continew-admin

Length of output: 446


🏁 Script executed:

# Look for refreshToken implementation and the cast operation
rg -nP --type=java -A5 'refreshToken.*LoginParameter' | head -30

Repository: continew-org/continew-admin

Length of output: 53


🏁 Script executed:

# Read AuthServiceImpl.java around line 136-142 for refreshToken
find . -name "AuthServiceImpl.java" -exec cat -n {} \; | sed -n '130,150p'

Repository: continew-org/continew-admin

Length of output: 1157


🏁 Script executed:

# Find UserContext class
find . -name "UserContext.java" -exec cat -n {} \;

Repository: continew-org/continew-admin

Length of output: 4868


🏁 Script executed:

# Find UserExtraContext class
find . -name "UserExtraContext.java" -exec cat -n {} \;

Repository: continew-org/continew-admin

Length of output: 2618


🏁 Script executed:

# Check Sa-Token dependency version and check for serializer configuration
cat continew-common/pom.xml | grep -A2 -B2 "sa-token"

Repository: continew-org/continew-admin

Length of output: 249


🏁 Script executed:

# Look for AbstractLoginHandler.java to verify lines 140-144
find . -name "AbstractLoginHandler.java" -exec cat -n {} \;

Repository: continew-org/continew-admin

Length of output: 7798


🏁 Script executed:

# Find SaTempUtil or check for custom Sa-Token serializer configuration
find . -name "*SaTempUtil*" -o -name "*satoken*" -type f | grep -i java | head -10

Repository: continew-org/continew-admin

Length of output: 53


🏁 Script executed:

# Search for Sa-Token configuration/customization in the starter package
find . -path "*/continew-starter-auth-satoken/*" -name "*.java" | head -20

Repository: continew-org/continew-admin

Length of output: 53


🏁 Script executed:

# Check if there's any custom JSON deserialization for LoginParameter
rg -nP --type=java "JsonDeserializer|@JsonDeserialize|readValue.*SaLoginParameter" | head -20

Repository: continew-org/continew-admin

Length of output: 53


🏁 Script executed:

# Check continew pom.xml for sa-token version specification
find . -name "pom.xml" -exec grep -l "sa-token" {} \; | xargs grep -A5 "sa-token"

Repository: continew-org/continew-admin

Length of output: 325


UserContext 反序列化存在类型强转风险。

代码在 AuthServiceImpl#refreshToken 第 142 行直接进行类型强转:(UserContext) loginParameter.getExtra(AuthConstants.LOGIN_USER)。当 Sa-Token 通过 SaTempUtil.parseToken() 反序列化 SaLoginParameter 时,若使用默认的 JSON 序列化器,extraData 中的 UserContext 对象将被反序列化为 LinkedHashMap,导致类型强转失败抛出 ClassCastException

此外,将完整的 UserContext(包含权限集合、角色集合等大量数据)存储在 refresh token 中,会显著增加存储占用,且可能导致权限不及时更新的问题。建议仅在 refresh token 中保存 userId 等必要字段,在刷新时从数据库重建完整的 UserContext,确保权限和角色信息始终为最新状态。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java`
around lines 140 - 144, The refresh-token flow risks ClassCastException because
UserContext is stored in extraData and later cast via (UserContext)
loginParameter.getExtra(AuthConstants.LOGIN_USER) (see
AuthServiceImpl#refreshToken and AbstractLoginHandler), and storing full
UserContext in the refresh token bloats data and stale permissions; fix by
storing only the minimal identifier(s) (e.g., userId) in
loginParameter.extraData instead of the full UserContext when building the token
in AbstractLoginHandler (remove putting UserContext into extraData), and update
AuthServiceImpl#refreshToken to read the userId from
loginParameter.getExtra(AuthConstants.LOGIN_USER) (or a new key like
AuthConstants.LOGIN_USER_ID), then rehydrate the full UserContext from the
database or user service before building the new token, ensuring any
parse/deserialization via SaTempUtil.parseToken / SaLoginParameter never
requires casting a deserialized map to UserContext.

@luoqiz luoqiz force-pushed the feat-refresh-token branch from 3b0f6e7 to 2a22d06 Compare April 24, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant