fix: 修复强制重置密码流程与表单状态

- 修正强制改密跳转及免旧密码的服务端校验

- 统一双端表单错误布局与更新按钮加载状态

- 补充认证服务和强制改密路由测试
This commit is contained in:
2026-07-30 15:03:53 +08:00
parent 03f45212ef
commit 1b40829135
20 changed files with 535 additions and 117 deletions

View File

@@ -38,5 +38,11 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -5,6 +5,9 @@ import tech.easyflow.auth.entity.LoginVO;
import java.math.BigInteger;
/**
* 账号认证与登录服务。
*/
public interface AuthService {
/**
* 登录
@@ -25,4 +28,21 @@ public interface AuthService {
* 通过账号ID登录
*/
LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds);
/**
* 修改当前账号密码。
*
* @param accountId 账号 ID
* @param currentPassword 当前密码;强制重置密码时可为空
* @param newPassword 新密码
* @param confirmPassword 确认密码
* @param loginDevice 当前登录会话设备类型
*/
void updateOwnPassword(
BigInteger accountId,
String currentPassword,
String newPassword,
String confirmPassword,
String loginDevice
);
}

View File

@@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.tenant.TenantManager;
import org.springframework.stereotype.Service;
@@ -23,15 +24,19 @@ import tech.easyflow.system.service.SysApiKeyService;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysMenuService;
import tech.easyflow.system.service.SysRoleService;
import cn.hutool.crypto.digest.BCrypt;
import tech.easyflow.system.util.SysPasswordPolicy;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 账号认证、会话创建与凭证更新服务。
*/
@Service
public class AuthServiceImpl implements AuthService, StpInterface {
@@ -100,6 +105,56 @@ public class AuthServiceImpl implements AuthService, StpInterface {
return createApiKeyLoginVO(record, timeoutSeconds);
}
/**
* 修改当前账号密码,并按数据库中的强制重置状态决定是否校验当前密码。
*
* @param accountId 账号 ID
* @param currentPassword 当前密码;强制重置密码时可为空
* @param newPassword 新密码
* @param confirmPassword 确认密码
* @param loginDevice 当前登录会话设备类型
* @throws BusinessException 账号不存在、凭证不正确、会话来源不可信或密码更新失败时抛出
*/
@Override
public void updateOwnPassword(
BigInteger accountId,
String currentPassword,
String newPassword,
String confirmPassword,
String loginDevice
) {
SysAccount record = sysAccountService.getById(accountId);
if (record == null) {
throw new BusinessException(404, 1, "账号不存在");
}
boolean passwordResetRequired = Boolean.TRUE.equals(record.getPasswordResetRequired());
if (passwordResetRequired) {
if (!AuthLoginSessionPolicy.WEB_DEVICE.equals(loginDevice)) {
throw new BusinessException(403, 3, "请使用账号密码登录后重置密码");
}
} else if (currentPassword == null
|| record.getPassword() == null
|| !BCrypt.checkpw(currentPassword, record.getPassword())) {
throw new BusinessException(400, 1, "密码不正确");
}
if (!Objects.equals(newPassword, confirmPassword)) {
throw new BusinessException(400, 2, "两次密码不一致");
}
SysPasswordPolicy.validateStrongPassword(newPassword);
SysAccount update = new SysAccount();
update.setId(accountId);
update.setPassword(BCrypt.hashpw(newPassword));
update.setPasswordResetRequired(false);
update.setModified(new Date());
update.setModifiedBy(accountId);
if (!sysAccountService.updateById(update)) {
throw new BusinessException(500, 4, "密码更新失败");
}
}
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));

View File

@@ -0,0 +1,176 @@
package tech.easyflow.auth.service.impl;
import cn.hutool.crypto.digest.BCrypt;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.SysAccountService;
import java.lang.reflect.Field;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link AuthServiceImpl} 密码修改规则测试。
*/
public class AuthServiceImplPasswordTest {
private static final BigInteger ACCOUNT_ID = BigInteger.valueOf(10);
private static final String CURRENT_PASSWORD = "Current!123";
private static final String NEW_PASSWORD = "Changed!456";
/**
* 验证强制重置状态下Web 会话无需提交当前密码即可更新。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void shouldAllowForcedResetWithoutCurrentPasswordForWebSession() throws Exception {
SysAccountService accountService = mock(SysAccountService.class);
AuthServiceImpl authService = createService(accountService);
SysAccount account = buildAccount(true);
when(accountService.getById(ACCOUNT_ID)).thenReturn(account);
when(accountService.updateById(any(SysAccount.class))).thenReturn(true);
authService.updateOwnPassword(
ACCOUNT_ID,
null,
NEW_PASSWORD,
NEW_PASSWORD,
AuthLoginSessionPolicy.WEB_DEVICE
);
ArgumentCaptor<SysAccount> updateCaptor = ArgumentCaptor.forClass(SysAccount.class);
verify(accountService).updateById(updateCaptor.capture());
SysAccount update = updateCaptor.getValue();
assertTrue(BCrypt.checkpw(NEW_PASSWORD, update.getPassword()));
assertFalse(update.getPasswordResetRequired());
assertEquals(ACCOUNT_ID, update.getModifiedBy());
}
/**
* 验证普通改密仍需校验当前密码。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void shouldRequireCurrentPasswordForRegularChange() throws Exception {
SysAccountService accountService = mock(SysAccountService.class);
AuthServiceImpl authService = createService(accountService);
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(false));
BusinessException exception = expectBusinessException(() -> authService.updateOwnPassword(
ACCOUNT_ID,
"Wrong!123",
NEW_PASSWORD,
NEW_PASSWORD,
AuthLoginSessionPolicy.WEB_DEVICE
));
assertEquals(400, exception.getHttpStatus());
assertEquals("密码不正确", exception.getMessage());
verify(accountService, never()).updateById(any(SysAccount.class));
}
/**
* 验证 API Key 会话不能使用免当前密码的强制重置流程。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void shouldRejectApiKeyForcedReset() throws Exception {
SysAccountService accountService = mock(SysAccountService.class);
AuthServiceImpl authService = createService(accountService);
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(true));
BusinessException exception = expectBusinessException(() -> authService.updateOwnPassword(
ACCOUNT_ID,
null,
NEW_PASSWORD,
NEW_PASSWORD,
AuthLoginSessionPolicy.API_KEY_DEVICE
));
assertEquals(403, exception.getHttpStatus());
assertEquals("请使用账号密码登录后重置密码", exception.getMessage());
verify(accountService, never()).updateById(any(SysAccount.class));
}
/**
* 验证普通改密在当前密码正确时仍可成功更新。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void shouldUpdateRegularPasswordWithCorrectCurrentPassword() throws Exception {
SysAccountService accountService = mock(SysAccountService.class);
AuthServiceImpl authService = createService(accountService);
when(accountService.getById(ACCOUNT_ID)).thenReturn(buildAccount(false));
when(accountService.updateById(any(SysAccount.class))).thenReturn(true);
authService.updateOwnPassword(
ACCOUNT_ID,
CURRENT_PASSWORD,
NEW_PASSWORD,
NEW_PASSWORD,
AuthLoginSessionPolicy.WEB_DEVICE
);
verify(accountService).updateById(any(SysAccount.class));
}
/**
* 构造测试账号。
*
* @param passwordResetRequired 是否要求重置密码
* @return 测试账号
*/
private SysAccount buildAccount(boolean passwordResetRequired) {
SysAccount account = new SysAccount();
account.setId(ACCOUNT_ID);
account.setPassword(BCrypt.hashpw(CURRENT_PASSWORD));
account.setPasswordResetRequired(passwordResetRequired);
return account;
}
/**
* 创建并注入账号服务。
*
* @param accountService 账号服务
* @return 认证服务
* @throws Exception 注入测试依赖失败
*/
private AuthServiceImpl createService(SysAccountService accountService) throws Exception {
AuthServiceImpl authService = new AuthServiceImpl();
Field field = AuthServiceImpl.class.getDeclaredField("sysAccountService");
field.setAccessible(true);
field.set(authService, accountService);
return authService;
}
/**
* 执行操作并返回业务异常。
*
* @param action 待执行操作
* @return 捕获的业务异常
*/
private BusinessException expectBusinessException(Runnable action) {
try {
action.run();
fail("预期抛出 BusinessException");
return null;
} catch (BusinessException exception) {
return exception;
}
}
}