fix: 修复强制重置密码流程与表单状态
- 修正强制改密跳转及免旧密码的服务端校验 - 统一双端表单错误布局与更新按钮加载状态 - 补充认证服务和强制改密路由测试
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user