feat: 支持账号导入与强制改密

- 新增账号导入模板下载、导入校验和默认密码重置标记

- 支持管理员重置密码并在登录后强制跳转修改密码

- 管理端与用户中心接入强密码校验和密码重置流程
This commit is contained in:
2026-03-18 21:56:05 +08:00
parent 14c78d54f5
commit 5d3c7d8692
40 changed files with 1720 additions and 142 deletions

View File

@@ -15,6 +15,11 @@ public class LoginVO {
*/
private String avatar;
/**
* 是否强制修改密码
*/
private boolean forceChangePassword;
public String getToken() {
return token;
}
@@ -38,4 +43,12 @@ public class LoginVO {
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public boolean isForceChangePassword() {
return forceChangePassword;
}
public void setForceChangePassword(boolean forceChangePassword) {
this.forceChangePassword = forceChangePassword;
}
}

View File

@@ -88,6 +88,7 @@ public class AuthServiceImpl implements AuthService, StpInterface {
res.setToken(StpUtil.getTokenValue());
res.setNickname(record.getNickname());
res.setAvatar(record.getAvatar());
res.setForceChangePassword(Boolean.TRUE.equals(record.getPasswordResetRequired()));
return res;
}

View File

@@ -43,6 +43,12 @@ public class SysAccountBase extends DateEntity implements Serializable {
@Column(comment = "密码")
private String password;
/**
* 是否需要重置密码
*/
@Column(comment = "是否需要重置密码")
private Boolean passwordResetRequired;
/**
* 账户类型
*/
@@ -149,6 +155,14 @@ public class SysAccountBase extends DateEntity implements Serializable {
this.password = password;
}
public Boolean getPasswordResetRequired() {
return passwordResetRequired;
}
public void setPasswordResetRequired(Boolean passwordResetRequired) {
this.passwordResetRequired = passwordResetRequired;
}
public Integer getAccountType() {
return accountType;
}

View File

@@ -0,0 +1,47 @@
package tech.easyflow.system.entity.vo;
/**
* 用户导入失败行。
*/
public class SysAccountImportErrorRowVo {
private Integer rowNumber;
private String deptCode;
private String loginName;
private String reason;
public Integer getRowNumber() {
return rowNumber;
}
public void setRowNumber(Integer rowNumber) {
this.rowNumber = rowNumber;
}
public String getDeptCode() {
return deptCode;
}
public void setDeptCode(String deptCode) {
this.deptCode = deptCode;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}

View File

@@ -0,0 +1,50 @@
package tech.easyflow.system.entity.vo;
import java.util.ArrayList;
import java.util.List;
/**
* 用户导入结果。
*/
public class SysAccountImportResultVo {
private int successCount = 0;
private int errorCount = 0;
private int totalCount = 0;
private List<SysAccountImportErrorRowVo> errorRows = new ArrayList<>();
public int getSuccessCount() {
return successCount;
}
public void setSuccessCount(int successCount) {
this.successCount = successCount;
}
public int getErrorCount() {
return errorCount;
}
public void setErrorCount(int errorCount) {
this.errorCount = errorCount;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public List<SysAccountImportErrorRowVo> getErrorRows() {
return errorRows;
}
public void setErrorRows(List<SysAccountImportErrorRowVo> errorRows) {
this.errorRows = errorRows;
}
}

View File

@@ -1,7 +1,13 @@
package tech.easyflow.system.service;
import com.mybatisflex.core.service.IService;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
import java.io.OutputStream;
import java.math.BigInteger;
/**
* 用户表 服务层。
@@ -14,4 +20,10 @@ public interface SysAccountService extends IService<SysAccount> {
void syncRelations(SysAccount entity);
SysAccount getByUsername(String userKey);
void resetPassword(BigInteger accountId, BigInteger operatorId);
SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount);
void writeImportTemplate(OutputStream outputStream);
}

View File

@@ -1,25 +1,54 @@
package tech.easyflow.system.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.BCrypt;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.read.listener.ReadListener;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.constant.enums.EnumAccountType;
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.entity.SysAccount;
import tech.easyflow.system.entity.SysAccountPosition;
import tech.easyflow.system.entity.SysAccountRole;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.entity.SysPosition;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo;
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
import tech.easyflow.system.mapper.SysAccountMapper;
import tech.easyflow.system.mapper.SysAccountPositionMapper;
import tech.easyflow.system.mapper.SysAccountRoleMapper;
import tech.easyflow.system.mapper.SysDeptMapper;
import tech.easyflow.system.mapper.SysPositionMapper;
import tech.easyflow.system.mapper.SysRoleMapper;
import tech.easyflow.system.service.SysAccountService;
import javax.annotation.Resource;
import java.time.Duration;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@@ -34,6 +63,18 @@ 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_CODE = "部门编码";
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号";
private static final String IMPORT_HEAD_NICKNAME = "昵称";
private static final String IMPORT_HEAD_MOBILE = "手机号";
private static final String IMPORT_HEAD_EMAIL = "邮箱";
private static final String IMPORT_HEAD_STATUS = "状态";
private static final String IMPORT_HEAD_ROLE_KEYS = "角色编码";
private static final String IMPORT_HEAD_POSITION_CODES = "岗位编码";
private static final String IMPORT_HEAD_REMARK = "备注";
@Resource
private SysAccountRoleMapper sysAccountRoleMapper;
@@ -42,7 +83,13 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
@Resource
private SysRoleMapper sysRoleMapper;
@Resource
private SysPositionMapper sysPositionMapper;
@Resource
private SysDeptMapper sysDeptMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@Resource
private PlatformTransactionManager transactionManager;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -55,7 +102,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT,
() -> {
//sync roleIds
List<BigInteger> roleIds = entity.getRoleIds();
if (roleIds != null) {
QueryWrapper delW = QueryWrapper.create();
@@ -79,7 +125,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
}
}
//sync positionIds
List<BigInteger> positionIds = entity.getPositionIds();
if (positionIds != null) {
QueryWrapper delW = QueryWrapper.create();
@@ -112,4 +157,483 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
w.eq(SysAccount::getLoginName, userKey);
return getOne(w);
}
@Override
public void resetPassword(BigInteger accountId, BigInteger operatorId) {
SysAccount record = getById(accountId);
if (record == null) {
throw new BusinessException("用户不存在");
}
Integer accountType = record.getAccountType();
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
throw new BusinessException("不能重置超级管理员密码");
}
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
throw new BusinessException("不能重置租户管理员密码");
}
SysAccount update = new SysAccount();
update.setId(accountId);
update.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
update.setPasswordResetRequired(true);
update.setModified(new Date());
update.setModifiedBy(operatorId);
updateById(update);
StpUtil.kickout(accountId);
}
@Override
public SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount) {
validateImportFile(file);
List<SysAccountImportRow> rows = parseImportRows(file);
SysAccountImportResultVo result = new SysAccountImportResultVo();
result.setTotalCount(rows.size());
if (rows.isEmpty()) {
return result;
}
Map<String, SysDept> deptMap = buildDeptCodeMap();
Map<String, SysRole> roleMap = buildRoleKeyMap();
Map<String, SysPosition> positionMap = buildPositionCodeMap();
for (SysAccountImportRow row : rows) {
try {
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptMap, roleMap, positionMap));
result.setSuccessCount(result.getSuccessCount() + 1);
} catch (Exception e) {
result.setErrorCount(result.getErrorCount() + 1);
appendImportError(result, row, extractImportErrorMessage(e));
}
}
return result;
}
@Override
public void writeImportTemplate(OutputStream outputStream) {
EasyExcel.write(outputStream)
.head(buildImportHeadList())
.sheet("模板")
.doWrite(new ArrayList<>());
}
private void executeInRowTransaction(Runnable action) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.executeWithoutResult(status -> {
try {
action.run();
} catch (RuntimeException e) {
status.setRollbackOnly();
throw e;
}
});
}
private void importSingleRow(SysAccountImportRow row,
LoginAccount loginAccount,
Map<String, SysDept> deptMap,
Map<String, SysRole> roleMap,
Map<String, SysPosition> positionMap) {
String deptCode = trimToNull(row.getDeptCode());
String loginName = trimToNull(row.getLoginName());
String nickname = trimToNull(row.getNickname());
if (deptCode == null) {
throw new BusinessException("部门编码不能为空");
}
if (loginName == null) {
throw new BusinessException("登录账号不能为空");
}
if (nickname == null) {
throw new BusinessException("昵称不能为空");
}
SysDept dept = deptMap.get(deptCode);
if (dept == null) {
throw new BusinessException("部门编码不存在: " + deptCode);
}
ensureLoginNameNotExists(loginName);
List<BigInteger> roleIds = resolveRoleIds(row.getRoleKeys(), roleMap);
List<BigInteger> positionIds = resolvePositionIds(row.getPositionCodes(), positionMap);
SysAccount entity = new SysAccount();
entity.setDeptId(dept.getId());
entity.setTenantId(loginAccount.getTenantId());
entity.setLoginName(loginName);
entity.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
entity.setPasswordResetRequired(true);
entity.setAccountType(EnumAccountType.NORMAL.getCode());
entity.setNickname(nickname);
entity.setMobile(trimToNull(row.getMobile()));
entity.setEmail(trimToNull(row.getEmail()));
entity.setStatus(parseStatus(row.getStatus()));
entity.setRemark(trimToNull(row.getRemark()));
entity.setCreated(new Date());
entity.setCreatedBy(loginAccount.getId());
entity.setModified(new Date());
entity.setModifiedBy(loginAccount.getId());
entity.setRoleIds(roleIds);
entity.setPositionIds(positionIds);
save(entity);
syncRelations(entity);
}
private void ensureLoginNameNotExists(String loginName) {
QueryWrapper wrapper = QueryWrapper.create();
wrapper.eq(SysAccount::getLoginName, loginName);
if (count(wrapper) > 0) {
throw new BusinessException("登录账号已存在: " + loginName);
}
}
private Integer parseStatus(String rawStatus) {
String status = trimToNull(rawStatus);
if (status == null) {
return EnumDataStatus.AVAILABLE.getCode();
}
if ("1".equals(status) || "已启用".equals(status) || "启用".equals(status)) {
return EnumDataStatus.AVAILABLE.getCode();
}
if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
return EnumDataStatus.UNAVAILABLE.getCode();
}
throw new BusinessException("状态不合法,仅支持 1/0 或 已启用/未启用");
}
private List<BigInteger> resolveRoleIds(String roleKeysText, Map<String, SysRole> roleMap) {
List<String> roleKeys = splitCodes(roleKeysText);
if (roleKeys.isEmpty()) {
return Collections.emptyList();
}
List<BigInteger> roleIds = new ArrayList<>(roleKeys.size());
for (String roleKey : roleKeys) {
SysRole role = roleMap.get(roleKey);
if (role == null) {
throw new BusinessException("角色编码不存在: " + roleKey);
}
roleIds.add(role.getId());
}
return roleIds;
}
private List<BigInteger> resolvePositionIds(String positionCodesText, Map<String, SysPosition> positionMap) {
List<String> positionCodes = splitCodes(positionCodesText);
if (positionCodes.isEmpty()) {
return Collections.emptyList();
}
List<BigInteger> positionIds = new ArrayList<>(positionCodes.size());
for (String positionCode : positionCodes) {
SysPosition position = positionMap.get(positionCode);
if (position == null) {
throw new BusinessException("岗位编码不存在: " + positionCode);
}
positionIds.add(position.getId());
}
return positionIds;
}
private List<String> splitCodes(String rawCodes) {
String codes = trimToNull(rawCodes);
if (codes == null) {
return Collections.emptyList();
}
String[] values = codes.split("[,]");
List<String> result = new ArrayList<>();
Set<String> uniqueValues = new LinkedHashSet<>();
for (String value : values) {
String trimmed = trimToNull(value);
if (trimmed != null && uniqueValues.add(trimmed)) {
result.add(trimmed);
}
}
return result;
}
private Map<String, SysDept> buildDeptCodeMap() {
List<SysDept> deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create());
Map<String, SysDept> deptMap = new HashMap<>();
for (SysDept dept : deptList) {
String deptCode = trimToNull(dept.getDeptCode());
if (deptCode != null) {
deptMap.putIfAbsent(deptCode, dept);
}
}
return deptMap;
}
private Map<String, SysRole> buildRoleKeyMap() {
List<SysRole> roleList = sysRoleMapper.selectListByQuery(QueryWrapper.create());
Map<String, SysRole> roleMap = new HashMap<>();
for (SysRole role : roleList) {
String roleKey = trimToNull(role.getRoleKey());
if (roleKey != null) {
roleMap.putIfAbsent(roleKey, role);
}
}
return roleMap;
}
private Map<String, SysPosition> buildPositionCodeMap() {
List<SysPosition> positionList = sysPositionMapper.selectListByQuery(QueryWrapper.create());
Map<String, SysPosition> positionMap = new HashMap<>();
for (SysPosition position : positionList) {
String positionCode = trimToNull(position.getPositionCode());
if (positionCode != null) {
positionMap.putIfAbsent(positionCode, position);
}
}
return positionMap;
}
private List<SysAccountImportRow> parseImportRows(MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
SysAccountExcelReadListener listener = new SysAccountExcelReadListener();
FastExcel.read(inputStream, listener)
.sheet()
.doRead();
return listener.getRows();
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException("用户导入文件解析失败");
}
}
private void validateImportFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException("导入文件不能为空");
}
if (file.getSize() > MAX_IMPORT_FILE_SIZE_BYTES) {
throw new BusinessException("导入文件大小不能超过10MB");
}
String fileName = file.getOriginalFilename();
String lowerName = fileName == null ? "" : fileName.toLowerCase();
if (!(lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls"))) {
throw new BusinessException("仅支持 xlsx/xls 文件");
}
}
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, String reason) {
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
errorRow.setRowNumber(row.getRowNumber());
errorRow.setDeptCode(row.getDeptCode());
errorRow.setLoginName(row.getLoginName());
errorRow.setReason(reason);
result.getErrorRows().add(errorRow);
}
private String extractImportErrorMessage(Exception e) {
if (e == null) {
return "导入失败";
}
if (e.getCause() != null && StringUtil.hasText(e.getCause().getMessage())) {
return e.getCause().getMessage();
}
if (StringUtil.hasText(e.getMessage())) {
return e.getMessage();
}
return "导入失败";
}
private List<List<String>> buildImportHeadList() {
List<List<String>> headList = new ArrayList<>(9);
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_CODE));
headList.add(Collections.singletonList(IMPORT_HEAD_LOGIN_NAME));
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_KEYS));
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_CODES));
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
return headList;
}
private String trimToNull(String value) {
if (!StringUtil.hasText(value)) {
return null;
}
return value.trim();
}
private static class SysAccountImportRow {
private Integer rowNumber;
private String deptCode;
private String loginName;
private String nickname;
private String mobile;
private String email;
private String status;
private String roleKeys;
private String positionCodes;
private String remark;
public Integer getRowNumber() {
return rowNumber;
}
public void setRowNumber(Integer rowNumber) {
this.rowNumber = rowNumber;
}
public String getDeptCode() {
return deptCode;
}
public void setDeptCode(String deptCode) {
this.deptCode = deptCode;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getRoleKeys() {
return roleKeys;
}
public void setRoleKeys(String roleKeys) {
this.roleKeys = roleKeys;
}
public String getPositionCodes() {
return positionCodes;
}
public void setPositionCodes(String positionCodes) {
this.positionCodes = positionCodes;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
private class SysAccountExcelReadListener implements ReadListener<LinkedHashMap<Integer, Object>> {
private final Map<String, Integer> headIndex = new HashMap<>();
private final List<SysAccountImportRow> rows = new ArrayList<>();
private int sheetRowNo;
@Override
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
sheetRowNo++;
String deptCode = getCellValue(data, IMPORT_HEAD_DEPT_CODE);
String loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
String email = getCellValue(data, IMPORT_HEAD_EMAIL);
String status = getCellValue(data, IMPORT_HEAD_STATUS);
String roleKeys = getCellValue(data, IMPORT_HEAD_ROLE_KEYS);
String positionCodes = getCellValue(data, IMPORT_HEAD_POSITION_CODES);
String remark = getCellValue(data, IMPORT_HEAD_REMARK);
if (!StringUtil.hasText(deptCode)
&& !StringUtil.hasText(loginName)
&& !StringUtil.hasText(nickname)
&& !StringUtil.hasText(mobile)
&& !StringUtil.hasText(email)
&& !StringUtil.hasText(status)
&& !StringUtil.hasText(roleKeys)
&& !StringUtil.hasText(positionCodes)
&& !StringUtil.hasText(remark)) {
return;
}
if (rows.size() >= MAX_IMPORT_ROWS) {
throw new BusinessException("单次最多导入5000个用户");
}
SysAccountImportRow row = new SysAccountImportRow();
row.setRowNumber(sheetRowNo + 1);
row.setDeptCode(deptCode);
row.setLoginName(loginName);
row.setNickname(nickname);
row.setMobile(mobile);
row.setEmail(email);
row.setStatus(status);
row.setRoleKeys(roleKeys);
row.setPositionCodes(positionCodes);
row.setRemark(remark);
rows.add(row);
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
String headValue = entry.getValue() == null ? null : entry.getValue().getStringValue();
String header = trimToNull(headValue);
if (header != null) {
headIndex.put(header, entry.getKey());
}
}
List<String> requiredHeads = List.of(
IMPORT_HEAD_DEPT_CODE,
IMPORT_HEAD_LOGIN_NAME,
IMPORT_HEAD_NICKNAME,
IMPORT_HEAD_MOBILE,
IMPORT_HEAD_EMAIL,
IMPORT_HEAD_STATUS,
IMPORT_HEAD_ROLE_KEYS,
IMPORT_HEAD_POSITION_CODES,
IMPORT_HEAD_REMARK
);
for (String requiredHead : requiredHeads) {
if (!headIndex.containsKey(requiredHead)) {
throw new BusinessException("导入模板表头不正确,必须包含:" + String.join("", requiredHeads));
}
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// no-op
}
public List<SysAccountImportRow> getRows() {
return rows;
}
private String getCellValue(Map<Integer, Object> row, String headName) {
Integer index = headIndex.get(headName);
if (index == null) {
return null;
}
Object value = row.get(index);
return value == null ? null : String.valueOf(value).trim();
}
}
}

View File

@@ -0,0 +1,30 @@
package tech.easyflow.system.util;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.util.regex.Pattern;
/**
* 用户密码策略。
*/
public final class SysPasswordPolicy {
public static final String STRONG_PASSWORD_MESSAGE = "密码必须至少8位且包含大写字母、小写字母、数字和特殊字符";
private static final Pattern STRONG_PASSWORD_PATTERN = Pattern.compile(
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{8,}$"
);
private SysPasswordPolicy() {
}
public static boolean isStrongPassword(String password) {
return password != null && STRONG_PASSWORD_PATTERN.matcher(password).matches();
}
public static void validateStrongPassword(String password) {
if (!isStrongPassword(password)) {
throw new BusinessException(STRONG_PASSWORD_MESSAGE);
}
}
}

View File

@@ -0,0 +1,20 @@
package tech.easyflow.system.util;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.common.web.exceptions.BusinessException;
public class SysPasswordPolicyTest {
@Test
public void shouldAcceptStrongPassword() {
Assert.assertTrue(SysPasswordPolicy.isStrongPassword("Abcd1234!"));
SysPasswordPolicy.validateStrongPassword("Abcd1234!");
}
@Test(expected = BusinessException.class)
public void shouldRejectWeakPassword() {
Assert.assertFalse(SysPasswordPolicy.isStrongPassword("123456"));
SysPasswordPolicy.validateStrongPassword("123456");
}
}