feat: 完成分享、单会话与发布审批改造

- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈

- Web 新登录替换旧会话,并保持 API Key 会话隔离

- 发布审批增加必填说明并在审批详情展示

- 账号重置与导入改用可配置默认强密码
This commit is contained in:
2026-07-23 16:09:31 +08:00
parent caa1f07b66
commit 5a42826d44
71 changed files with 3191 additions and 132 deletions

View File

@@ -0,0 +1,48 @@
package tech.easyflow.system.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import tech.easyflow.system.util.SysPasswordPolicy;
/**
* 账号安全配置。
*/
@Component
@ConfigurationProperties(prefix = "easyflow.security.account")
public class AccountSecurityProperties implements InitializingBean {
/**
* 账号重置和导入时使用的默认强密码。
*/
private String defaultResetPassword = "!QAZ2wsx";
/**
* 获取默认重置密码。
*
* @return 默认重置密码
*/
public String getDefaultResetPassword() {
return defaultResetPassword;
}
/**
* 设置默认重置密码。
*
* @param defaultResetPassword 默认重置密码
*/
public void setDefaultResetPassword(String defaultResetPassword) {
this.defaultResetPassword = defaultResetPassword;
}
/**
* 应用启动时校验默认密码符合系统强密码策略。
*/
@Override
public void afterPropertiesSet() {
SysPasswordPolicy.validateStrongPassword(
defaultResetPassword == null ? null : defaultResetPassword.trim()
);
defaultResetPassword = defaultResetPassword.trim();
}
}

View File

@@ -0,0 +1,27 @@
package tech.easyflow.system.permission.resource;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
/**
* 为特定业务场景补充资源访问授权的扩展点。
*/
public interface ResourceAccessGrantProvider {
/**
* 判断当前业务上下文是否补充授予资源动作权限。
*
* @param loginAccount 当前登录账号
* @param resourceType 资源类型
* @param resource 资源对象
* @param action 资源动作
* @return 授予权限时返回 {@code true}
*/
boolean grants(
LoginAccount loginAccount,
CategoryResourceType resourceType,
VisibilityResource resource,
ResourceAction action
);
}

View File

@@ -1,5 +1,6 @@
package tech.easyflow.system.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
@@ -7,6 +8,7 @@ import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.permission.resource.ResourceAccessGrantProvider;
import tech.easyflow.system.permission.resource.VisibilityResource;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;
@@ -14,7 +16,12 @@ import tech.easyflow.system.service.SysDeptService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
/**
* 基于租户、创建者、分类授权和可见范围的统一资源权限实现。
*/
@Service
public class ResourceAccessServiceImpl implements ResourceAccessService {
@@ -24,11 +31,20 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
@Resource
private SysDeptService sysDeptService;
@Autowired(required = false)
private List<ResourceAccessGrantProvider> grantProviders = Collections.emptyList();
/**
* {@inheritDoc}
*/
@Override
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
return canAccess(SaTokenUtil.getLoginAccount(), resourceType, resource, action);
}
/**
* {@inheritDoc}
*/
@Override
public boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
if (resource == null) {
@@ -38,6 +54,10 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
return false;
}
BigInteger accountId = loginAccount.getId();
// 分享访问需要先完成密钥校验与审计,即使当前账号同时也是资源创建者或超管。
if (hasExtendedGrant(loginAccount, resourceType, resource, action)) {
return true;
}
if (categoryPermissionService.isSuperAdmin(loginAccount)) {
return true;
}
@@ -60,10 +80,36 @@ public class ResourceAccessServiceImpl implements ResourceAccessService {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) {
if (!canAccess(resourceType, resource, action)) {
throw new BusinessException(message == null ? "无权限访问该资源" : message);
throw new BusinessException(403, 403, message == null ? "无权限访问该资源" : message);
}
}
/**
* 判断业务扩展授权是否允许当前动作。
*
* @param loginAccount 当前登录账号
* @param resourceType 资源类型
* @param resource 资源对象
* @param action 资源动作
* @return 任一扩展授权允许时返回 {@code true}
*/
private boolean hasExtendedGrant(
LoginAccount loginAccount,
CategoryResourceType resourceType,
VisibilityResource resource,
ResourceAction action
) {
for (ResourceAccessGrantProvider provider : grantProviders) {
if (provider.grants(loginAccount, resourceType, resource, action)) {
return true;
}
}
return false;
}
}

View File

@@ -22,6 +22,7 @@ import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.config.AccountSecurityProperties;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysAccountPosition;
import tech.easyflow.system.entity.SysAccountRole;
@@ -71,7 +72,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private static final String ACCOUNT_RELATION_LOCK_KEY_PREFIX = "easyflow:lock:sys:account:relation:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
private static final String DEFAULT_RESET_PASSWORD = "123456";
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
private static final int MAX_IMPORT_ROWS = 5000;
private static final String IMPORT_HEAD_DEPT_NAME = "部门名称*";
@@ -110,6 +110,8 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private RedisLockExecutor redisLockExecutor;
@Resource
private PlatformTransactionManager transactionManager;
@Resource
private AccountSecurityProperties accountSecurityProperties;
/**
* 批量解析账号展示名称。
@@ -238,7 +240,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
validateResetPasswordAllowed(record);
SysAccount update = new SysAccount();
update.setId(accountId);
update.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
update.setPassword(BCrypt.hashpw(accountSecurityProperties.getDefaultResetPassword()));
update.setPasswordResetRequired(true);
update.setModified(new Date());
update.setModifiedBy(operatorId);
@@ -421,7 +423,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
entity.setDeptId(dept.getId());
entity.setTenantId(loginAccount.getTenantId());
entity.setLoginName(loginName);
entity.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
entity.setPassword(BCrypt.hashpw(accountSecurityProperties.getDefaultResetPassword()));
entity.setPasswordResetRequired(true);
entity.setAccountType(EnumAccountType.NORMAL.getCode());
entity.setNickname(nickname);
@@ -686,7 +688,11 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
rows.add(List.of("可选字段", "手机号、邮箱、状态、角色名称、岗位名称、备注"));
rows.add(List.of("状态可选值", "可留空,或填写 1/0/已启用/启用/未启用/停用/禁用"));
rows.add(List.of("多值分隔", "角色名称、岗位名称支持使用英文逗号,或中文逗号,分隔多个名称"));
rows.add(List.of("导入后初始密码", "导入成功的账号默认密码为 123456首次登录需要修改密码"));
rows.add(List.of(
"导入后初始密码",
"导入成功的账号默认密码为 " + accountSecurityProperties.getDefaultResetPassword()
+ ",首次登录需要修改密码"
));
rows.add(List.of("示例行", "市场部 | zhangsan | 张三 | 13800138000 | zhangsan@example.com | 已启用 | 普通员工,审批专员 | 产品经理 | 示例导入"));
return rows;
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.system.config;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.common.web.exceptions.BusinessException;
/**
* {@link AccountSecurityProperties} 测试。
*/
public class AccountSecurityPropertiesTest {
/**
* 验证默认重置密码符合强密码策略。
*/
@Test
public void shouldAcceptDefaultStrongResetPassword() {
AccountSecurityProperties properties = new AccountSecurityProperties();
properties.afterPropertiesSet();
Assert.assertEquals("!QAZ2wsx", properties.getDefaultResetPassword());
}
/**
* 验证弱默认密码会阻止应用启动。
*/
@Test(expected = BusinessException.class)
public void shouldRejectWeakResetPassword() {
AccountSecurityProperties properties = new AccountSecurityProperties();
properties.setDefaultResetPassword("123456");
properties.afterPropertiesSet();
}
/**
* 验证空默认密码会阻止应用启动。
*/
@Test(expected = BusinessException.class)
public void shouldRejectBlankResetPassword() {
AccountSecurityProperties properties = new AccountSecurityProperties();
properties.setDefaultResetPassword(" ");
properties.afterPropertiesSet();
}
}