diff --git a/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java b/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java index af42d93d7..f11cb5bd1 100644 --- a/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java +++ b/continew-admin-common/src/main/java/top/continew/admin/common/constant/RegexConstants.java @@ -34,6 +34,11 @@ public class RegexConstants { */ public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{6,32}$"; + /** + * 密码正则严格版(长度为 8 到 32 位,包含至少1个大写字母、1个小写字母、1个数字,1个特殊字符) + */ + public static final String PASSWORD_STRICT = "^\\S*(?=\\S{8,32})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*$"; + /** * 通用编码正则(长度为 2 到 30 位,可以包含字母、数字,下划线,以字母开头) */ diff --git a/continew-admin-system/src/main/java/top/continew/admin/auth/model/resp/UserInfoResp.java b/continew-admin-system/src/main/java/top/continew/admin/auth/model/resp/UserInfoResp.java index 4656c2fff..dfb87d6f4 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/auth/model/resp/UserInfoResp.java +++ b/continew-admin-system/src/main/java/top/continew/admin/auth/model/resp/UserInfoResp.java @@ -98,6 +98,12 @@ public class UserInfoResp implements Serializable { @Schema(description = "最后一次修改密码时间", example = "2023-08-08 08:08:08", type = "string") private LocalDateTime pwdResetTime; + /** + * 密码是否已过期 + */ + @Schema(description = "密码是否已过期", example = "true") + private Boolean passwordExpired; + /** * 创建时间 */ diff --git a/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java index 1aba3a647..32989861f 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java +++ b/continew-admin-system/src/main/java/top/continew/admin/auth/service/impl/LoginServiceImpl.java @@ -20,10 +20,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.TreeNodeConfig; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.RandomUtil; -import cn.hutool.core.util.ReUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.*; import cn.hutool.json.JSONUtil; import lombok.RequiredArgsConstructor; import me.zhyd.oauth.model.AuthUser; @@ -32,6 +29,7 @@ import top.continew.admin.auth.model.resp.RouteResp; import top.continew.admin.auth.service.LoginService; import top.continew.admin.auth.service.PermissionService; +import top.continew.admin.common.constant.CacheConstants; import top.continew.admin.common.constant.RegexConstants; import top.continew.admin.common.constant.SysConstants; import top.continew.admin.common.enums.DisEnableStatusEnum; @@ -41,6 +39,7 @@ import top.continew.admin.common.model.dto.LoginUser; import top.continew.admin.common.util.helper.LoginHelper; import top.continew.admin.system.enums.MessageTemplateEnum; +import top.continew.admin.system.enums.OptionCodeEnum; import top.continew.admin.system.model.entity.DeptDO; import top.continew.admin.system.model.entity.RoleDO; import top.continew.admin.system.model.entity.UserDO; @@ -48,11 +47,13 @@ import top.continew.admin.system.model.req.MessageReq; import top.continew.admin.system.model.resp.MenuResp; import top.continew.admin.system.service.*; +import top.continew.starter.cache.redisson.util.RedisUtils; import top.continew.starter.core.autoconfigure.project.ProjectProperties; import top.continew.starter.core.util.validate.CheckUtils; import top.continew.starter.extension.crud.annotation.TreeField; import top.continew.starter.extension.crud.util.TreeUtils; +import java.time.Duration; import java.time.LocalDateTime; import java.util.*; @@ -76,16 +77,41 @@ public class LoginServiceImpl implements LoginService { private final UserSocialService userSocialService; private final MessageService messageService; private final PasswordEncoder passwordEncoder; + private final OptionService optionService; @Override public String accountLogin(String username, String password) { UserDO user = userService.getByUsername(username); - CheckUtils.throwIfNull(user, "用户名或密码不正确"); - CheckUtils.throwIf(!passwordEncoder.matches(password, user.getPassword()), "用户名或密码不正确"); + boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword()); + isPasswordLocked(username, isError); + CheckUtils.throwIf(isError, "用户名或密码错误"); this.checkUserStatus(user); return this.login(user); } + /** + * 检测用户是否被密码锁定 + * + * @param username 用户名 + */ + private void isPasswordLocked(String username, boolean isError) { + // 不锁定账户 + int maxErrorCount = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_ERROR_COUNT); + if (maxErrorCount <= 0) { + return; + } + String key = CacheConstants.USER_KEY_PREFIX + "PASSWORD-ERROR:" + username; + Long currentErrorCount = RedisUtils.get(key); + currentErrorCount = currentErrorCount == null ? 0 : currentErrorCount; + int lockMinutes = optionService.getValueByCode2Int(OptionCodeEnum.PASSWORD_LOCK_MINUTES); + if (isError) { + // 密码错误自增次数,并重置时间 + currentErrorCount = currentErrorCount + 1; + RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); + } + CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账户锁定 {} 分钟", maxErrorCount, lockMinutes); + } + @Override public String phoneLogin(String phone) { UserDO user = userService.getByPhone(phone); diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java b/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java new file mode 100644 index 000000000..4233799c5 --- /dev/null +++ b/continew-admin-system/src/main/java/top/continew/admin/system/enums/OptionCodeEnum.java @@ -0,0 +1,63 @@ +/* + * 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.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 参数枚举 + * + * @author Kils + * @since 2024/05/09 11:25 + */ +@Getter +@RequiredArgsConstructor +public enum OptionCodeEnum { + + /** + * 密码是否允许包含正反序帐户名 + */ + PASSWORD_CONTAIN_NAME("password_contain_name", "密码不允许包含正反序帐户名"), + /** + * 密码错误锁定帐户次数 + */ + PASSWORD_ERROR_COUNT("password_error_count", "密码错误锁定帐户次数"), + /** + * 密码有效期 + */ + PASSWORD_EXPIRATION_DAYS("password_expiration_days", "密码有效期"), + /** + * 密码是否允许包含正反序帐户名 + */ + PASSWORD_LOCK_MINUTES("password_lock_minutes", "密码错误锁定帐户的时间"), + /** + * 密码最小长度 + */ + PASSWORD_MIN_LENGTH("password_min_length", "密码最小长度"), + /** + * 密码是否必须包含特殊字符 + */ + PASSWORD_SPECIAL_CHAR("password_special_char", "密码是否必须包含特殊字符"), + /** + * 修改密码最短间隔 + */ + PASSWORD_UPDATE_INTERVAL("password_update_interval", "修改密码最短间隔"); + + private final String value; + private final String description; +} diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java index 4043ea61a..fcc1c47df 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java +++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/OptionService.java @@ -17,7 +17,9 @@ package top.continew.admin.system.service; import java.util.List; +import java.util.function.Function; +import top.continew.admin.system.enums.OptionCodeEnum; import top.continew.admin.system.model.query.OptionQuery; import top.continew.admin.system.model.req.OptionReq; import top.continew.admin.system.model.req.OptionResetValueReq; @@ -52,4 +54,21 @@ public interface OptionService { * @param req 重置信息 */ void resetValue(OptionResetValueReq req); + + /** + * 根据code获取int参数值 + * + * @param code code + * @return 参数值 + */ + int getValueByCode2Int(OptionCodeEnum code); + + /** + * 根据code获取参数值 + * + * @param code code + * @param mapper 类型转换 ex:value -> Integer.parseInt(value) + * @return 参数值 + */ + T getValueByCode(OptionCodeEnum code, Function mapper); } \ No newline at end of file diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/UserService.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/UserService.java index 0c7413ed7..cb253739c 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/system/service/UserService.java +++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/UserService.java @@ -28,6 +28,7 @@ import top.continew.starter.extension.crud.service.BaseService; import top.continew.starter.data.mybatis.plus.service.IService; +import java.time.LocalDateTime; import java.util.List; /** @@ -137,4 +138,12 @@ public interface UserService extends BaseService deptIds); + + /** + * 密码是否已过期 + * + * @param pwdResetTime 上次重置密码时间 + * @return 是否过期 + */ + Boolean isPasswordExpired(LocalDateTime pwdResetTime); } diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java index c501b7dbc..a9304f9b6 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java +++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java @@ -17,9 +17,14 @@ package top.continew.admin.system.service.impl; import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import top.continew.admin.common.constant.CacheConstants; +import top.continew.admin.system.enums.OptionCodeEnum; import top.continew.admin.system.mapper.OptionMapper; import top.continew.admin.system.model.entity.OptionDO; import top.continew.admin.system.model.query.OptionQuery; @@ -29,9 +34,11 @@ import top.continew.admin.system.service.OptionService; import top.continew.starter.cache.redisson.util.RedisUtils; import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.validate.CheckUtils; import top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper; import java.util.List; +import java.util.function.Function; /** * 参数业务实现 @@ -61,4 +68,27 @@ public void resetValue(OptionResetValueReq req) { RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); baseMapper.lambdaUpdate().set(OptionDO::getValue, null).in(OptionDO::getCode, req.getCode()).update(); } + + @Override + public int getValueByCode2Int(OptionCodeEnum code) { + return this.getValueByCode(code, Integer::parseInt); + } + + @Override + public T getValueByCode(OptionCodeEnum code, Function mapper) { + String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code.getValue()); + if (StrUtil.isNotBlank(value)) { + return mapper.apply(value); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(OptionDO::getCode, code.getValue()) + .select(OptionDO::getValue, OptionDO::getDefaultValue); + OptionDO optionDO = baseMapper.selectOne(queryWrapper); + CheckUtils.throwIf(ObjUtil.isEmpty(optionDO), "配置 [{}] 不存在", code); + value = StrUtil.nullToDefault(optionDO.getValue(), optionDO.getDefaultValue()); + CheckUtils.throwIf(StrUtil.isBlank(value), "配置 [{}] 不存在", code); + RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code.getValue(), value); + return mapper.apply(value); + } + } \ No newline at end of file diff --git a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java index c161dd708..707c58f32 100644 --- a/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java +++ b/continew-admin-system/src/main/java/top/continew/admin/system/service/impl/UserServiceImpl.java @@ -18,8 +18,10 @@ import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import com.alicp.jetcache.anno.CacheInvalidate; import com.alicp.jetcache.anno.CacheType; @@ -38,6 +40,7 @@ import org.springframework.web.multipart.MultipartFile; import top.continew.admin.auth.service.OnlineUserService; import top.continew.admin.common.constant.CacheConstants; +import top.continew.admin.common.constant.RegexConstants; import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.util.helper.LoginHelper; import top.continew.admin.system.mapper.UserMapper; @@ -53,18 +56,18 @@ import top.continew.admin.system.service.*; import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.util.validate.CheckUtils; +import top.continew.starter.core.util.validate.ValidationUtils; import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.service.CommonUserService; import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; import java.time.LocalDateTime; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; +import static top.continew.admin.system.enums.OptionCodeEnum.*; + /** * 用户业务实现 * @@ -81,6 +84,7 @@ public class UserServiceImpl extends BaseServiceImpl getUserInfo() { UserInfoResp userInfoResp = BeanUtil.copyProperties(userDetailResp, UserInfoResp.class); userInfoResp.setPermissions(loginUser.getPermissions()); userInfoResp.setRoles(loginUser.getRoleCodes()); + userInfoResp.setPasswordExpired(userService.isPasswordExpired(userDetailResp.getPwdResetTime())); return R.ok(userInfoResp); } diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserCenterController.java b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserCenterController.java index c95e18ca6..c685804c0 100644 --- a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserCenterController.java +++ b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/system/UserCenterController.java @@ -16,7 +16,6 @@ package top.continew.admin.webapi.system; -import cn.hutool.core.util.ReUtil; import com.xkcoding.justauth.AuthRequestFactory; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -32,7 +31,6 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import top.continew.admin.common.constant.CacheConstants; -import top.continew.admin.common.constant.RegexConstants; import top.continew.admin.common.enums.SocialSourceEnum; import top.continew.admin.common.util.SecureUtils; import top.continew.admin.common.util.helper.LoginHelper; @@ -95,8 +93,6 @@ public R updatePassword(@Validated @RequestBody UserPasswordUpdateReq upda String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq .getNewPassword())); ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); - ValidationUtils.throwIf(!ReUtil - .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 6 到 32 位,可以包含字母、数字、下划线,特殊字符,同时包含字母和数字"); userService.updatePassword(rawOldPassword, rawNewPassword, LoginHelper.getUserId()); return R.ok("修改成功,请牢记你的新密码"); } diff --git a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql index b6aac72ca..08004ebcb 100644 --- a/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql +++ b/continew-admin-webapi/src/main/resources/db/changelog/mysql/continew-admin_data.sql @@ -114,7 +114,14 @@ VALUES 'Copyright © 2022-present Charles7c  ContiNew Admin  津ICP备2022005864号-2', '用于显示登录页面的底部版权信息。', NULL, NULL), ('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), -('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL); +('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), +('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), +('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL), +('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL), +('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL), +('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), +('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), +('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); -- 初始化默认字典 INSERT INTO `sys_dict` diff --git a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql index cbe03bd4d..08f2c0c06 100644 --- a/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql +++ b/continew-admin-webapi/src/main/resources/db/changelog/postgresql/continew-admin_data.sql @@ -114,7 +114,14 @@ VALUES 'Copyright © 2022-present Charles7c  ContiNew Admin  津ICP备2022005864号-2', '用于显示登录页面的底部版权信息。', NULL, NULL), ('系统LOGO(16*16)', 'site_favicon', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。', NULL, NULL), -('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL); +('系统LOGO(33*33)', 'site_logo', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。', NULL, NULL), +('密码是否允许包含正反序帐户名', 'password_contain_name', NULL, '0', '', NULL, NULL), +('密码错误锁定帐户次数', 'password_error_count', NULL, '5', '0表示不限制。', NULL, NULL), +('密码有效期', 'password_expiration_days', NULL, '0', '取值范围为0-999,0表示永久有效。', NULL, NULL), +('密码错误锁定帐户的时间', 'password_lock_minutes', NULL, '5', '0表示不解锁。', NULL, NULL), +('密码最小长度', 'password_min_length', NULL, '8', '取值范围为8-32。', NULL, NULL), +('密码是否必须包含特殊字符', 'password_special_char', NULL, '0', '', NULL, NULL), +('修改密码最短间隔', 'password_update_interval', NULL, '5', '取值范围为0-9999,0表示不限制。', NULL, NULL); -- 初始化默认字典 INSERT INTO "sys_dict"