Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ databaseChangeLog:
file: db/changelog/mysql/plugin/plugin_schedule.sql
- include:
file: db/changelog/mysql/plugin/plugin_generator.sql
- includeAll:
path: db/changelog/mysql/V4.2.0
# PostgreSQL
# - include:
# file: db/changelog/postgresql/main_table.sql
Expand All @@ -23,4 +25,6 @@ databaseChangeLog:
# - include:
# file: db/changelog/postgresql/plugin/plugin_schedule.sql
# - include:
# file: db/changelog/postgresql/plugin/plugin_generator.sql
# file: db/changelog/postgresql/plugin/plugin_generator.sql
# - includeAll:
# path: db/changelog/postgresql/V4.2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- liquibase formatted sql

-- changeset luoqiz:4.2.0-1
-- comment sys_client 客户端表更新
-- sys_client 添加双token列
ALTER TABLE `sys_client`
ADD COLUMN `is_enable_refresh_token` bit(1) NOT NULL DEFAULT b'0' 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`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

-- 初始化客户端数据
INSERT 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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- 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;

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"
("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);
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package top.continew.admin.auth;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode;
import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange;
Expand All @@ -25,6 +24,7 @@
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import top.continew.admin.auth.constant.AuthConstants;
import top.continew.admin.auth.model.req.LoginReq;
import top.continew.admin.auth.model.resp.LoginResp;
import top.continew.admin.common.context.RoleContext;
Expand All @@ -46,6 +46,7 @@
import top.continew.starter.extension.tenant.util.TenantUtils;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

Expand Down Expand Up @@ -136,13 +137,11 @@ protected LoginResp authenticate(UserDO user, ClientResp client) {
userContext.setClientId(client.getClientId());
userContext.setTenantId(tenantId);
// 登录并缓存用户信息
StpUtil.login(userContext.getId(), loginParameter.setExtraData(BeanUtil
.beanToMap(new UserExtraContext(ServletUtils.getRequest()))));
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);
Comment on lines +140 to +144
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.

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@

package top.continew.admin.auth;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.temp.SaTempUtil;
import jakarta.servlet.http.HttpServletRequest;
import top.continew.admin.auth.enums.AuthTypeEnum;
import top.continew.admin.auth.model.req.LoginReq;
import top.continew.admin.auth.model.resp.DoubleTokenLoginResp;
import top.continew.admin.auth.model.resp.LoginResp;
import top.continew.admin.auth.model.resp.SingleTokenLoginResp;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.system.model.resp.ClientResp;
import top.continew.starter.extension.tenant.context.TenantContextHolder;

/**
* 登录处理器
Expand Down Expand Up @@ -65,4 +72,38 @@ public interface LoginHandler<T extends LoginReq> {
* @return 认证类型
*/
AuthTypeEnum getAuthType();

/**
* 构建登录信息
*
* @param loginParameter 登录的参数
* @param userContext 用户上下文信息
* @param client 客户端信息
* @return
*/
static LoginResp buildLoginResp(SaLoginParameter loginParameter, UserContext userContext, ClientResp client) {
StpUtil.login(userContext.getId(), loginParameter);
if (Boolean.TRUE.equals(client.getIsEnableRefreshToken())) {
// 刷新令牌设置的有效时长
long refreshExpiresIn = (client.getRefreshTokenTimeout() != null && client.getRefreshTokenTimeout() > 0L)
? client.getRefreshTokenTimeout() : client.getTimeout();
String refreshToken = SaTempUtil.createToken(userContext.getId(), refreshExpiresIn, false);
// 将生成的token保存一份,方便刷新token时删除先前的token
loginParameter.setToken(StpUtil.getTokenValue());
SaTempUtil.saveToken(refreshToken, loginParameter, refreshExpiresIn);
Comment thread
luoqiz marked this conversation as resolved.
return DoubleTokenLoginResp.builder()
.accessToken(StpUtil.getTokenValue())
.accessExpiresIn(StpUtil.getTokenTimeout())
.refreshToken(refreshToken)
.refreshExpiresIn(refreshExpiresIn)
.tenantId(TenantContextHolder.isTenantEnabled() ? TenantContextHolder.getTenantId() : null)
.build();
} else {
return SingleTokenLoginResp.builder()
.token(StpUtil.getTokenValue())
.expiresIn(StpUtil.getTokenTimeout())
.tenantId(TenantContextHolder.isTenantEnabled() ? TenantContextHolder.getTenantId() : null)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class AuthConstants {
*/
public static final String LOGOUT_URI = "/auth/logout";

/**
* Map 存储登录用户信息时的 key 值
*/
public static final String LOGIN_USER = "loginUser";

private AuthConstants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
Expand Down Expand Up @@ -72,6 +73,13 @@ public LoginResp login(@RequestBody @Valid LoginReq req, HttpServletRequest requ
return authService.login(req, request);
}

@SaIgnore
@Operation(summary = "刷新token", description = "刷新token")
@PostMapping("/refreshToken")
public LoginResp refreshToken(@RequestParam("refreshToken") @NotBlank String refreshToken) {
return authService.refreshToken(refreshToken);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Operation(summary = "登出", description = "注销用户的当前登录")
@Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER)
@PostMapping("/logout")
Expand All @@ -88,8 +96,8 @@ public Object logout() {
public SocialAuthAuthorizeResp authorize(@PathVariable @EnumValue(value = SocialSourceEnum.class, message = "第三方平台无效") String source) {
AuthRequest authRequest = authRequestFactory.getAuthRequest(source);
return SocialAuthAuthorizeResp.builder()
.authorizeUrl(authRequest.authorize(AuthStateUtils.createState()))
.build();
.authorizeUrl(authRequest.authorize(AuthStateUtils.createState()))
.build();
}

@Log(ignore = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package top.continew.admin.auth.model.resp;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;

/**
* 双token模式
*/
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
public class DoubleTokenLoginResp extends LoginResp {

/**
* 访问令牌
*/
@Schema(description = "访问令牌", example = "eyJ0eXAiOiJlV1QiLCJhbGciqiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb29pbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiSjd4SUljYnU5cmNwU09vQ3Uyc1ND1BYYTYycFRjcjAifQ.KUPOYm-2wfuLUSfEEAbpGE527fzmkAJG7sMNcQ0pUZ8")
private String accessToken;

/**
* 刷新令牌
*/
@Schema(description = "刷新令牌", example = "12a5c5e1f87d4b4db614c6229c2c8916")
private String refreshToken;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 访问令牌有效时长(秒)
*/
@Schema(description = "访问令牌有效时长(秒)", example = "1800")
private Long accessExpiresIn;

/**
* 刷新令牌有效时长(秒)
*/
@Schema(description = "刷新令牌有效时长(秒)", example = "2592000")
private Long refreshExpiresIn;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
package top.continew.admin.auth.model.resp;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.SuperBuilder;

import java.io.Serial;
import java.io.Serializable;
Expand All @@ -30,19 +30,13 @@
* @since 2022/12/21 20:42
*/
@Data
@Builder
@SuperBuilder
@Schema(description = "登录响应参数")
public class LoginResp implements Serializable {

@Serial
private static final long serialVersionUID = 1L;

/**
* 令牌
*/
@Schema(description = "令牌", example = "eyJ0eXAiOiJlV1QiLCJhbGciqiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb29pbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiSjd4SUljYnU5cmNwU09vQ3Uyc1ND1BYYTYycFRjcjAifQ.KUPOYm-2wfuLUSfEEAbpGE527fzmkAJG7sMNcQ0pUZ8")
private String token;

/**
* 租户 ID
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package top.continew.admin.auth.model.resp;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;

/**
* 单token模式,只有访问的token
*/
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
public class SingleTokenLoginResp extends LoginResp {

/**
* 访问令牌
*/
@Schema(description = "访问令牌", example = "eyJ0eXAiOiJlV1QiLCJhbGciqiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb29pbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiSjd4SUljYnU5cmNwU09vQ3Uyc1ND1BYYTYycFRjcjAifQ.KUPOYm-2wfuLUSfEEAbpGE527fzmkAJG7sMNcQ0pUZ8")
private String token;

/**
* 有效时长(秒)
*/
@Schema(description = "有效时长(秒)", example = "8600")
private Long expiresIn;

}
Comment thread
luoqiz marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ public interface AuthService {
* @return 路由树
*/
List<RouteResp> buildRouteTree(Long userId);

/**
* 刷新token
*
* @param refreshToken 原认证的 Refresh Token
* @return 登录响应参数
*/
LoginResp refreshToken(String refreshToken);
}
Loading
Loading