From d192646d041bb4b6dd391e72b740055a4dac60b0 Mon Sep 17 00:00:00 2001 From: jiang Date: Sun, 8 Sep 2024 20:12:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/bonus/auth/config/LoginType.java | 43 +++ .../auth/config/PasswordPolicyConfig.java | 54 ++++ .../auth/controller/TokenController.java | 99 ++++--- .../auth/factory/LoginStrategyFactory.java | 43 +++ .../java/com/bonus/auth/form/LoginBody.java | 85 +----- .../auth/service/EmailOtpLoginStrategy.java | 38 +++ .../service/EmailPasswordLoginStrategy.java | 47 ++++ .../com/bonus/auth/service/EmailService.java | 18 +- .../com/bonus/auth/service/LoginStrategy.java | 16 ++ .../service/PasswordValidatorService.java | 251 ++++++++++++++++++ .../auth/service/PhoneOtpLoginStrategy.java | 41 +++ .../service/PhonePasswordLoginStrategy.java | 48 ++++ .../bonus/auth/service/SysLoginService.java | 221 ++------------- .../UsernamePasswordLoginStrategy.java | 58 ++++ .../system/config/PasswordPolicyConfig.java | 57 ++++ .../controller/SysProfileController.java | 193 +++++++------- .../system/controller/SysUserController.java | 87 +++--- .../system/domain/UserPasswordHistory.java | 38 +++ .../service/PasswordValidatorService.java | 48 ++++ .../impl/PasswordValidatorServiceImpl.java | 225 ++++++++++++++++ .../service/impl/SysUserServiceImpl.java | 48 ++-- .../mapper/system/PasswordValidatorMapper.xml | 30 +++ 22 files changed, 1321 insertions(+), 467 deletions(-) create mode 100644 bonus-auth/src/main/java/com/bonus/auth/config/LoginType.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/config/PasswordPolicyConfig.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/factory/LoginStrategyFactory.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/EmailOtpLoginStrategy.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/EmailPasswordLoginStrategy.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/LoginStrategy.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/PhoneOtpLoginStrategy.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/PhonePasswordLoginStrategy.java create mode 100644 bonus-auth/src/main/java/com/bonus/auth/service/UsernamePasswordLoginStrategy.java create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/config/PasswordPolicyConfig.java create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/domain/UserPasswordHistory.java create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/service/PasswordValidatorService.java create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java create mode 100644 bonus-modules/bonus-system/src/main/resources/mapper/system/PasswordValidatorMapper.xml diff --git a/bonus-auth/src/main/java/com/bonus/auth/config/LoginType.java b/bonus-auth/src/main/java/com/bonus/auth/config/LoginType.java new file mode 100644 index 0000000..3fe870c --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/config/LoginType.java @@ -0,0 +1,43 @@ +package com.bonus.auth.config; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum LoginType { + /** + * 账号密码 + */ + USERNAME_PASSWORD, + /** + * 手机号密码 + */ + PHONE_PASSWORD, + /** + * 邮箱密码 + */ + EMAIL_PASSWORD, + /** + * 手机号验证码 + */ + PHONE_OTP, + /** + * 邮箱验证码 + */ + EMAIL_OTP; + + @JsonCreator + public static LoginType fromString(String key) { + if (key == null) { + return null; + } + + // 自定义转换逻辑,允许大小写不敏感的匹配 + for (LoginType type : LoginType.values()) { + if (type.name().equalsIgnoreCase(key)) { + return type; + } + } + // throw new ServiceException("不支持的登录方式"); + return null; + } +} + diff --git a/bonus-auth/src/main/java/com/bonus/auth/config/PasswordPolicyConfig.java b/bonus-auth/src/main/java/com/bonus/auth/config/PasswordPolicyConfig.java new file mode 100644 index 0000000..078ee9f --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/config/PasswordPolicyConfig.java @@ -0,0 +1,54 @@ +package com.bonus.auth.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 该类用于从 `application.yml` 中加载密码策略的配置项。 + * 使用 @ConfigurationProperties 注解,前缀为 password-policy。 + * @author bonus + */ +@Component +@ConfigurationProperties(prefix = "password-policy") +@Data +public class PasswordPolicyConfig { + + // 密码的最小长度 + private int minLength; + + // 密码的最大长度 + private int maxLength; + + // 是否需要包含大写字母 + private boolean requireUpperCase; + + // 是否需要包含小写字母 + private boolean requireLowerCase; + + // 是否需要包含数字 + private boolean requireDigit; + + // 是否需要包含特殊字符 + private boolean requireSpecialChar; + + // 常见的弱密码列表,禁止使用这些密码 + private List weakPasswords; + + // 密码历史记录限制 + private int passwordHistoryLimit; + + // 是否限制连续相同字符 + private boolean restrictConsecutiveChars; + + // 最大允许的连续字符数 + private int maxConsecutiveChars; + + // 密码中是否不能包含用户名 + private boolean excludeUsernameInPassword; + + // 是否在首次登录时强制修改密码 + private boolean forcePasswordChangeOnFirstLogin; +} diff --git a/bonus-auth/src/main/java/com/bonus/auth/controller/TokenController.java b/bonus-auth/src/main/java/com/bonus/auth/controller/TokenController.java index b0783ce..0ba3576 100644 --- a/bonus-auth/src/main/java/com/bonus/auth/controller/TokenController.java +++ b/bonus-auth/src/main/java/com/bonus/auth/controller/TokenController.java @@ -1,25 +1,33 @@ package com.bonus.auth.controller; -import javax.servlet.http.HttpServletRequest; - -import com.bonus.common.core.web.domain.AjaxResult; -import com.bonus.system.api.domain.SysUser; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import com.bonus.auth.config.LoginType; +import com.bonus.auth.factory.LoginStrategyFactory; import com.bonus.auth.form.LoginBody; import com.bonus.auth.form.RegisterBody; +import com.bonus.auth.service.LoginStrategy; +import com.bonus.auth.service.PasswordValidatorService; import com.bonus.auth.service.SysLoginService; +import com.bonus.auth.service.SysPasswordService; +import com.bonus.common.core.constant.SecurityConstants; import com.bonus.common.core.domain.R; import com.bonus.common.core.utils.JwtUtils; import com.bonus.common.core.utils.StringUtils; import com.bonus.common.security.auth.AuthUtil; import com.bonus.common.security.service.TokenService; import com.bonus.common.security.utils.SecurityUtils; +import com.bonus.system.api.RemoteUserService; import com.bonus.system.api.model.LoginUser; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Set; /** * Token 控制器 @@ -34,33 +42,62 @@ public class TokenController { @Autowired private SysLoginService sysLoginService; - @PostMapping("isLogin") - public R isLogin(@RequestBody LoginBody form) { - LoginUser userInfo; - if ("mobile".equals(form.getLoginType())) { - userInfo = sysLoginService.login(form.getMobile(), form.getPassword(), form.getLoginType()); - } else { - userInfo = sysLoginService.login(form.getUsername(), form.getPassword(), form.getLoginType()); + @Autowired + private LoginStrategyFactory loginStrategyFactory; + + @Resource + private RemoteUserService remoteUserService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private PasswordValidatorService passwordValidatorService; + + @PostMapping("isAdmin") + public R isAdmin(@RequestBody LoginBody form) { + //通过用户名获取人员信息 + R userResult = remoteUserService.getUserInfo(form.getUsername(), SecurityConstants.INNER); + if (userResult == null || userResult.getData() == null || R.FAIL == userResult.getCode()) { + return R.fail("登录用户不存在"); } - SysUser sysUser = userInfo.getSysUser(); - return R.ok(tokenService.isLogin(String.valueOf(sysUser.getUserId()))); + Set roles = userResult.getData().getRoles(); + if (roles.contains("admin")) { + if (ObjectUtils.isNotEmpty(userResult.getData().getSysUser().getPhonenumber())) { + passwordService.createPhoneCaptcha(userResult.getData().getSysUser().getPhonenumber()); + } else { + R.fail(roles.contains("admin")); + } + } + return R.ok(roles.contains("admin")); + } + + @PostMapping("isLogin") + public R isLogin(@RequestBody LoginBody form) { + LoginStrategy strategy = loginStrategyFactory.getStrategy(form.getLoginType()); + if (strategy == null) { + return R.fail("不支持的登录方式"); + } + LoginUser login = strategy.login(form.getUsername(), form.getPassword()); + return R.ok(tokenService.isLogin(String.valueOf(login.getSysUser().getUserId()))); } - /** - * 用户登录 - * - * @param form 登录表单 - * @return 登录结果 - */ @PostMapping("login") public R login(@RequestBody LoginBody form) { - LoginUser userInfo; - if ("mobile".equals(form.getLoginType())) { - userInfo = sysLoginService.login(form.getMobile(), form.getPassword(), form.getLoginType()); - } else { - userInfo = sysLoginService.login(form.getUsername(), form.getPassword(), form.getLoginType()); + + // 获取相应的登录策略 + LoginStrategy strategy = loginStrategyFactory.getStrategy(form.getLoginType()); + if (strategy == null) { + return R.fail("不支持的登录方式"); } - return R.ok(tokenService.createToken(userInfo)); + LoginUser login = strategy.login(form.getUsername(), form.getPassword()); + if (login.getRoles().contains("admin") && form.getLoginType().equals(LoginType.USERNAME_PASSWORD)) { + passwordValidatorService.checkPhoneCaptcha(form.getVerificationCode(), login.getSysUser().getPhonenumber()); + return R.ok(tokenService.createToken(login)); + } else { + return R.ok(tokenService.createToken(login)); + } + } /** diff --git a/bonus-auth/src/main/java/com/bonus/auth/factory/LoginStrategyFactory.java b/bonus-auth/src/main/java/com/bonus/auth/factory/LoginStrategyFactory.java new file mode 100644 index 0000000..8c99f39 --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/factory/LoginStrategyFactory.java @@ -0,0 +1,43 @@ +package com.bonus.auth.factory; + +import com.bonus.auth.config.LoginType; +import com.bonus.auth.service.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author bonus + */ +@Component +public class LoginStrategyFactory { + private final Map strategyMap; + + @Autowired + public LoginStrategyFactory(List strategies) { + strategyMap = new HashMap<>(); + // 通过类型查找对应的策略 + strategies.forEach(strategy -> { + if (strategy instanceof UsernamePasswordLoginStrategy) { + strategyMap.put(LoginType.USERNAME_PASSWORD, strategy); + } else if (strategy instanceof PhonePasswordLoginStrategy) { + strategyMap.put(LoginType.PHONE_PASSWORD, strategy); + } else if (strategy instanceof PhoneOtpLoginStrategy) { + strategyMap.put(LoginType.PHONE_OTP, strategy); + } else if (strategy instanceof EmailPasswordLoginStrategy) { + strategyMap.put(LoginType.EMAIL_PASSWORD, strategy); + } else if (strategy instanceof EmailOtpLoginStrategy) { + strategyMap.put(LoginType.EMAIL_OTP, strategy); + } + // 继续添加其他策略 + }); + } + + public LoginStrategy getStrategy(LoginType loginType) { + return strategyMap.get(loginType); + } +} + diff --git a/bonus-auth/src/main/java/com/bonus/auth/form/LoginBody.java b/bonus-auth/src/main/java/com/bonus/auth/form/LoginBody.java index 5448000..e5cff16 100644 --- a/bonus-auth/src/main/java/com/bonus/auth/form/LoginBody.java +++ b/bonus-auth/src/main/java/com/bonus/auth/form/LoginBody.java @@ -1,10 +1,14 @@ package com.bonus.auth.form; +import com.bonus.auth.config.LoginType; +import lombok.Data; + /** * 用户登录对象 * * @author bonus */ +@Data public class LoginBody { /** * 用户名 @@ -27,87 +31,8 @@ public class LoginBody { private String code; - private String loginType; + private LoginType loginType; private String mobileCodeType; - public String getMobileCodeType() { - return mobileCodeType; - } - - public void setMobileCodeType(String mobileCodeType) { - this.mobileCodeType = mobileCodeType; - } - - public String getMobile() { - return mobile; - } - - public void setMobile(String mobile) { - this.mobile = mobile; - } - - public String getLoginType() { - return loginType; - } - - public void setLoginType(String loginType) { - this.loginType = loginType; - } - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getNickName() { - return nickName; - } - - public void setNickName(String nickName) { - this.nickName = nickName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPhone() { - return phone; - } - - public void setPhone(String phone) { - this.phone = phone; - } - - public String getVerificationCode() { - return verificationCode; - } - - public void setVerificationCode(String verificationCode) { - this.verificationCode = verificationCode; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } } \ No newline at end of file diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/EmailOtpLoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/EmailOtpLoginStrategy.java new file mode 100644 index 0000000..15c8065 --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/EmailOtpLoginStrategy.java @@ -0,0 +1,38 @@ +package com.bonus.auth.service; + +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.domain.R; +import com.bonus.system.api.RemoteUserService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author bonus + */ +@Service +public class EmailOtpLoginStrategy implements LoginStrategy { + @Resource + private RemoteUserService remoteUserService; + + @Resource + private PasswordValidatorService passwordValidatorService; + + @Override + public LoginUser login(String email, String otp) { + R userResult = remoteUserService.getUserInfo(email, SecurityConstants.INNER); + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + //验证用户是否存在 + passwordValidatorService.validateUserResult(email, userResult); + // 验证用户查询结果 + passwordValidatorService.validateUserResult(email, userResult); + passwordValidatorService.validateApprovalStatus(email, user); + // 验证用户状态 + passwordValidatorService.validateUserStatus(email, user); + //返回信息 + return userInfo; + } +} diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/EmailPasswordLoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/EmailPasswordLoginStrategy.java new file mode 100644 index 0000000..1b38974 --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/EmailPasswordLoginStrategy.java @@ -0,0 +1,47 @@ +package com.bonus.auth.service; + +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.domain.R; +import com.bonus.system.api.RemoteUserService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author bonus + */ +@Service +public class EmailPasswordLoginStrategy implements LoginStrategy { + @Resource + private RemoteUserService remoteUserService; + + @Resource + private PasswordValidatorService passwordValidatorService; + @Resource + private SysPasswordService passwordService; + + @Override + public LoginUser login(String email, String password) { + //通过手机号获取用户信息 + R userResult = remoteUserService.getUserInfo(email, SecurityConstants.INNER); + //验证用户是否存在 + passwordValidatorService.validateUserResult(email, userResult); + //获取用户信息 + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + //校验用户审批状态 + passwordValidatorService.validateApprovalStatus(user.getUserName(), user); + // 处理IP校验 + passwordValidatorService.validateIpBlacklist(user.getUserName()); + // 验证密码 + passwordService.validate(user, password, System.currentTimeMillis()); + //校验用户启用状态 + passwordValidatorService.validateUserStatus(user.getUserName(), user); + //返回信息 + return userInfo; + } +} + + diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/EmailService.java b/bonus-auth/src/main/java/com/bonus/auth/service/EmailService.java index da1761b..91e8e07 100644 --- a/bonus-auth/src/main/java/com/bonus/auth/service/EmailService.java +++ b/bonus-auth/src/main/java/com/bonus/auth/service/EmailService.java @@ -3,10 +3,7 @@ package com.bonus.auth.service; import com.bonus.common.core.constant.CacheConstants; import com.bonus.common.core.constant.Constants; import com.bonus.common.core.domain.R; -import com.bonus.common.core.exception.CaptchaException; -import com.bonus.common.core.utils.StringUtils; import com.bonus.common.core.utils.VerificationCodeUtils; -import com.bonus.common.core.utils.sms.SmsUtils; import com.bonus.common.redis.service.RedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; @@ -32,11 +29,16 @@ public class EmailService { String code = VerificationCodeUtils.generateVerificationCode(VerificationCodeUtils.CodeType.NUMERIC); String str = "您的验证码为" + code + ",尊敬的客户,以上验证码3分钟有效,微服务平台提醒您:转发可能导致账号被盗,请勿将验证码泄露于他人"; SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom("2642480752@qq.com"); // 发件人邮箱地址 - message.setTo(to); // 收件人邮箱地址 - message.setSubject("【博诺思】"); // 邮件主题 - message.setText(str); // 邮件内容 - mailSender.send(message); // 发送邮件 + // 发件人邮箱地址 + message.setFrom("2642480752@qq.com"); + // 收件人邮箱地址 + message.setTo(to); + // 邮件主题 + message.setSubject("【博诺思】"); + // 邮件内容 + message.setText(str); + // 发送邮件 + mailSender.send(message); String verifyKey = CacheConstants.CAPTCHA_PHONE_CODE_KEY + to; redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); return R.ok(); diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/LoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/LoginStrategy.java new file mode 100644 index 0000000..d5f77c8 --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/LoginStrategy.java @@ -0,0 +1,16 @@ +package com.bonus.auth.service; + +import com.bonus.system.api.model.LoginUser; + +/** + * @author bonus + */ +public interface LoginStrategy { + /** + * 登录方法 + * @param identifier 用户的标识符(用户名、手机号、邮箱) + * @param credential 用户凭据(密码或验证码) + * @return 登录结果 + */ + LoginUser login(String identifier, String credential); +} diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java b/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java new file mode 100644 index 0000000..fe3152f --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/PasswordValidatorService.java @@ -0,0 +1,251 @@ +package com.bonus.auth.service; + +import com.bonus.auth.config.PasswordPolicyConfig; +import com.bonus.common.core.constant.CacheConstants; +import com.bonus.common.core.constant.UserConstants; +import com.bonus.common.core.domain.R; +import com.bonus.common.core.enums.UserStatus; +import com.bonus.common.core.exception.CaptchaException; +import com.bonus.common.core.exception.ServiceException; +import com.bonus.common.core.text.Convert; +import com.bonus.common.core.utils.StringUtils; +import com.bonus.common.core.utils.ip.IpUtils; +import com.bonus.common.core.web.domain.AjaxResult; +import com.bonus.common.redis.service.RedisService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Component +public class PasswordValidatorService { + + @Resource + private PasswordPolicyConfig config; + + @Resource + private RedisService redisService; + + @Resource + private SysRecordLogService recordLogService; + + /** + * 对新密码进行校验,返回布尔值表示密码是否符合要求。 + * + * @param username 用户名,不能包含在密码中 + * @param newPassword 新密码 + * @return 如果密码符合策略要求,则返回 AjaxResult.success,否则返回错误提示 + */ + public AjaxResult validatePassword(String username, String newPassword) { + if (!isPasswordLengthValid(newPassword)) { + return AjaxResult.error("密码长度应为" + config.getMinLength() + "至" + config.getMaxLength() + "位!"); + } + + if (!containsRequiredCharacters(newPassword)) { + return AjaxResult.error(getCharacterRequirementErrorMessage()); + } + + if (containsWeakPassword(newPassword)) { + return AjaxResult.error("密码包含常见的弱密码片段!"); + } + + if (containsConsecutiveCharacters(newPassword, config.getMaxConsecutiveChars())) { + return AjaxResult.error("密码不能包含超过" + config.getMaxConsecutiveChars() + "位连续字符!"); + } + + if (newPassword.contains(username)) { + return AjaxResult.error("密码不能包含账号!"); + } + + return AjaxResult.success(); + } + + /** + * 检查密码长度是否符合配置要求 + */ + private boolean isPasswordLengthValid(String password) { + return password.length() >= config.getMinLength() && password.length() <= config.getMaxLength(); + } + + /** + * 检查密码是否包含必需的字符类型 + */ + private boolean containsRequiredCharacters(String password) { + boolean hasUpperCase = false, hasLowerCase = false, hasDigit = false, hasSpecialChar = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) { + hasUpperCase = true; + } + if (Character.isLowerCase(c)) { + hasLowerCase = true; + } + if (Character.isDigit(c)) { + hasDigit = true; + } + if ("!@#$%^&*()-_=+[{]};:'\",<.>/?".indexOf(c) >= 0) { + hasSpecialChar = true; + } + } + + return (!config.isRequireUpperCase() || hasUpperCase) && + (!config.isRequireLowerCase() || hasLowerCase) && + (!config.isRequireDigit() || hasDigit) && + (!config.isRequireSpecialChar() || hasSpecialChar); + } + + /** + * 根据配置返回密码不符合要求时的错误提示信息 + */ + private String getCharacterRequirementErrorMessage() { + if (config.isRequireUpperCase()) { + return "密码必须包含大写字母!"; + } + if (config.isRequireLowerCase()) { + return "密码必须包含小写字母!"; + } + if (config.isRequireDigit()) { + return "密码必须包含数字!"; + } + if (config.isRequireSpecialChar()) { + return "密码必须包含特殊字符!"; + } + return "密码不符合字符要求!"; + } + + /** + * 检查密码是否包含常见的弱密码 + */ + private boolean containsWeakPassword(String password) { + for (String weakPwd : config.getWeakPasswords()) { + if (password.contains(weakPwd)) { + return true; + } + } + return false; + } + + /** + * 检查密码中是否包含超过 n 个连续相同字符 + */ + private boolean containsConsecutiveCharacters(String password, int n) { + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutive = true; + for (int j = 1; j < n; j++) { + if (password.charAt(i + j) != password.charAt(i)) { + consecutive = false; + break; + } + } + if (consecutive) { + return true; + } + } + return false; + } + + /** + * 验证登录参数 + */ + public void validateLoginParameters(String username, String password) { + if (StringUtils.isAnyBlank(username, password)) { + logAndThrowError(username, "用户名/密码必须填写", "用户名/密码为空"); + } + if (!isPasswordLengthValid(password)) { + logAndThrowError(username, "密码格式不正确", "密码格式不正确"); + } + if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { + logAndThrowError(username, "用户名格式不正确", "用户名格式不正确"); + } + } + + /** + * 验证IP黑名单 + */ + public void validateIpBlacklist(String username) { + long startTime = System.currentTimeMillis(); + try { + String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST)); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) { + logAndThrowError(username, "访问IP已被列入系统黑名单", "访问IP已被列入系统黑名单"); + } + } catch (Exception e) { + logAndThrowError(username, "IP黑名单校验失败,请稍后重试", e.getMessage()); + } + } + + /** + * 验证用户查询结果 + */ + public void validateUserResult(String username, R userResult) { + if (userResult == null || userResult.getData() == null || R.FAIL == userResult.getCode()) { + logAndThrowError(username, "用户名/密码错误", "登录用户不存在"); + } + } + + /** + * 验证用户状态 + */ + public void validateUserStatus(String username, SysUser user) { + if (UserStatus.DELETED.getCode().equals(user.getDelFlag()) || UserStatus.DISABLE.getCode().equals(user.getStatus())) { + logAndThrowError(username, "账号已被删除或停用", "账号已被删除或停用"); + } + } + + /** + * 处理IP校验 + */ + public void handleIpValidation(String username, SysUser user) { + try { + String nowIp = IpUtils.getIpAddr(); + String hisIp = redisService.getCacheObject("IP:" + user.getUserId()); + + if (!nowIp.equals(hisIp)) { + recordLogService.saveErrorLogs(username, System.currentTimeMillis(), user.getUserId().toString()); + } + redisService.setCacheObject("IP:" + user.getUserId(), nowIp, 5L, TimeUnit.MINUTES); + } catch (Exception e) { + logAndThrowError(username, "IP校验失败,请稍后重试", e.getMessage()); + } + } + + public void validateApprovalStatus(String username, SysUser user) { + if ("0".equals(user.getApprovalStatus())) { + logAndThrowError(username, "账号未审批", "用户不存在"); + } + } + + /** + * 记录错误日志并抛出异常 + */ + private void logAndThrowError(String username, String message, String logMessage) { + long startTime = System.currentTimeMillis(); + recordLogService.saveLogs(username, startTime, logMessage, message, null, "失败"); + throw new ServiceException(message); + } + + /** + * 校验手机验证码 + */ + public void checkPhoneCaptcha(String code, String phone) throws CaptchaException { + if (StringUtils.isEmpty(code)) { + throw new CaptchaException("手机验证码不能为空"); + } + if (StringUtils.isEmpty(phone)) { + throw new CaptchaException("手机号不能为空"); + } + String verifyKey = CacheConstants.CAPTCHA_PHONE_CODE_KEY + StringUtils.nvl(phone, ""); + String captcha = redisService.getCacheObject(verifyKey); + if (captcha == null) { + throw new CaptchaException("手机验证码已失效"); + } + redisService.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) { + throw new CaptchaException("手机验证码错误"); + } + } + + +} diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/PhoneOtpLoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/PhoneOtpLoginStrategy.java new file mode 100644 index 0000000..790f098 --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/PhoneOtpLoginStrategy.java @@ -0,0 +1,41 @@ +package com.bonus.auth.service; + +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.domain.R; +import com.bonus.system.api.RemoteUserService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author bonus + */ +@Service +public class PhoneOtpLoginStrategy implements LoginStrategy { + @Resource + private RemoteUserService remoteUserService; + + @Resource + private PasswordValidatorService passwordValidatorService; + + @Resource + private SysPasswordService passwordService; + + @Override + public LoginUser login(String phone, String otp) { + R userResult = remoteUserService.getUserInfo(phone, SecurityConstants.INNER); + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + //验证用户是否存在 + passwordValidatorService.validateUserResult(phone, userResult); + // 验证用户查询结果 + passwordValidatorService.validateUserResult(phone, userResult); + passwordValidatorService.validateApprovalStatus(phone, user); + // 验证用户状态 + passwordValidatorService.validateUserStatus(phone, user); + //返回信息 + return userInfo; + } +} diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/PhonePasswordLoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/PhonePasswordLoginStrategy.java new file mode 100644 index 0000000..f301faa --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/PhonePasswordLoginStrategy.java @@ -0,0 +1,48 @@ +package com.bonus.auth.service; + +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.domain.R; +import com.bonus.system.api.RemoteUserService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author bonus + */ +@Service +public class PhonePasswordLoginStrategy implements LoginStrategy { + @Resource + private RemoteUserService remoteUserService; + + @Resource + private PasswordValidatorService passwordValidatorService; + + @Resource + private SysPasswordService passwordService; + + @Override + public LoginUser login(String phone, String password) { + //通过手机号获取用户信息 + R userResult = remoteUserService.getUserInfo(phone, SecurityConstants.INNER); + //验证用户是否存在 + passwordValidatorService.validateUserResult(phone, userResult); + //获取用户信息 + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + //校验用户审批状态 + passwordValidatorService.validateApprovalStatus(user.getUserName(), user); + // 处理IP校验 + passwordValidatorService.validateIpBlacklist(user.getUserName()); + // 验证密码 + passwordService.validate(user, password, System.currentTimeMillis()); + //校验用户启用状态 + passwordValidatorService.validateUserStatus(user.getUserName(), user); + //返回信息 + return userInfo; + } +} + + diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/SysLoginService.java b/bonus-auth/src/main/java/com/bonus/auth/service/SysLoginService.java index 4b161c6..01ea752 100644 --- a/bonus-auth/src/main/java/com/bonus/auth/service/SysLoginService.java +++ b/bonus-auth/src/main/java/com/bonus/auth/service/SysLoginService.java @@ -1,27 +1,25 @@ package com.bonus.auth.service; import com.bonus.auth.form.RegisterBody; -import com.bonus.common.core.constant.*; -import com.hankcs.hanlp.HanLP; -import org.apache.poi.ss.formula.functions.T; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import com.bonus.common.core.constant.Constants; +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.constant.UserConstants; import com.bonus.common.core.domain.R; -import com.bonus.common.core.enums.UserStatus; import com.bonus.common.core.exception.ServiceException; -import com.bonus.common.core.text.Convert; import com.bonus.common.core.utils.StringUtils; -import com.bonus.common.core.utils.ip.IpUtils; +import com.bonus.common.core.web.domain.AjaxResult; import com.bonus.common.redis.service.RedisService; import com.bonus.common.security.utils.SecurityUtils; import com.bonus.system.api.RemoteUserService; import com.bonus.system.api.domain.SysUser; import com.bonus.system.api.model.LoginUser; +import com.hankcs.hanlp.HanLP; +import org.apache.poi.ss.formula.functions.T; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; +import javax.annotation.Resource; /** * 登录校验方法 @@ -50,6 +48,9 @@ public class SysLoginService { @Value("${system.supports.emailLogin}") private boolean supportsEmailLogin; + @Resource + private PasswordValidatorService passwordValidatorService; + /** * 获取验证码 * @@ -58,14 +59,19 @@ public class SysLoginService { * @return 响应结果 */ public R getPhoneCode(String username, String getMobileCodeType) { - long startTime = System.currentTimeMillis(); // 记录开始时间 + // 记录开始时间 + long startTime = System.currentTimeMillis(); int contactType = getContactType(username); + R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); + boolean userExists = userResult != null && userResult.getData() != null; + if (userResult.getData().getRoles().contains("admin")) { + passwordService.createPhoneCaptcha(userResult.getData().getSysUser().getPhonenumber()); + return R.ok(); + } if (contactType == 2) { recordLogService.saveLogs(username, startTime, "获取验证码失败", "联系方式无效", null, "失败"); throw new ServiceException("请输入正确的联系方式"); } - R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); - boolean userExists = userResult != null && userResult.getData() != null; if ("register".equals(getMobileCodeType)) { handleRegister(username, startTime, contactType, userExists); } else { @@ -117,7 +123,7 @@ public class SysLoginService { private void handleLogin(String username, long startTime, int contactType, boolean userExists) { if (!userExists) { recordLogService.saveLogs(username, startTime, "登录用户不存在", "手机号未注册", null, "失败"); - throw new ServiceException("手机号登录失败"); + throw new ServiceException("登录用户不存在"); } if (contactType == 0) { if (!supportsEmailLogin) { @@ -135,180 +141,6 @@ public class SysLoginService { recordLogService.saveLogs(username, startTime, "获取验证码", "用户存在", null, "成功"); } - - /** - * 用户登录方法 - * - * @param username 用户名或手机号 - * @param password 密码或验证码 - * @param loginType 登录类型(mobile:手机验证码登录,其他:用户名密码登录) - * @return 登录用户信息 - */ - public LoginUser login(String username, String password, String loginType) { - long startTime = System.currentTimeMillis(); - int contactType = getContactType(username); - if (contactType == 0) { - if (!supportsEmailLogin) { - recordLogService.saveLogs(username, startTime, "邮箱登录不支持", "邮箱登录未开启", null, "失败"); - throw new ServiceException("用户名/密码错误"); - } - } else if (contactType == 1) { - if (!supportsPhoneLogin) { - recordLogService.saveLogs(username, startTime, "手机登录不支持", "手机登录未开启", null, "失败"); - throw new ServiceException("用户名/密码错误"); - } - }// 记录开始时间 - if ("mobile".equals(loginType)) { - return handleMobileLogin(username, startTime); - } else { - return handleUsernamePasswordLogin(username, password, startTime); - } - - } - - /** - * 处理手机号验证码登录 - * - * @param mobile 手机号 - * @param startTime 操作开始时间 - * @return 登录用户信息 - */ - private LoginUser handleMobileLogin(String mobile, long startTime) { - R userResult = remoteUserService.getUserInfo(mobile, SecurityConstants.INNER); - validateUserResult(mobile, userResult, startTime); - LoginUser userInfo = userResult.getData(); - SysUser user = userInfo.getSysUser(); - validateApprovalStatus(user.getUserName(), user, startTime); - validateIpBlacklist(user.getUserName(), startTime); - validateUserStatus(user.getUserName(), user, startTime); - recordLogService.saveLogs(user.getUserName(), startTime, "登陆成功", "手机号验证码登录成功", user.getUserId().toString(), "成功"); - return userInfo; - } - - /** - * 处理用户名密码登录 - * - * @param username 用户名 - * @param password 密码 - * @param startTime 操作开始时间 - * @return 登录用户信息 - */ - private LoginUser handleUsernamePasswordLogin(String username, String password, long startTime) { - //validateLoginParameters(username, password, startTime); // 验证登录参数 - validateIpBlacklist(username, startTime); // IP黑名单校验 - R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); - validateUserResult(username, userResult, startTime); // 验证用户查询结果 - LoginUser userInfo = userResult.getData(); - SysUser user = userInfo.getSysUser(); - validateApprovalStatus(username, user, startTime); - validateUserStatus(username, user, startTime); // 验证用户状态 - passwordService.validate(user, password, startTime); // 验证密码 - handleIpValidation(username, user, startTime); // 处理IP校验 - recordLogService.saveLogs(username, startTime, "登陆成功", "用户名密码登录成功", user.getUserId().toString(), "成功"); - return userInfo; - } - - /** - * 验证登录参数 - * - * @param username 用户名 - * @param password 密码 - * @param startTime 操作开始时间 - */ - private void validateLoginParameters(String username, String password, long startTime) { - if (StringUtils.isAnyBlank(username, password)) { - recordLogService.saveLogs(username, startTime, "用户名/密码为空", "用户名/密码必须填写", null, "失败"); - throw new ServiceException("用户名/密码必须填写"); - } - - if (password.length() < ValidateUtils.MIN_LENGTH || password.length() > ValidateUtils.MAX_LENGTH) { - recordLogService.saveLogs(username, startTime, "密码格式不正确", "密码格式不正确", null, "失败"); - throw new ServiceException("用户名/密码错误"); - } - - if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { - recordLogService.saveLogs(username, startTime, "用户名格式不正确", "用户名格式不正确", null, "失败"); - throw new ServiceException("用户名/密码错误"); - } - } - - /** - * 验证IP黑名单 - * - * @param username 用户名 - * @param startTime 操作开始时间 - */ - private void validateIpBlacklist(String username, long startTime) { - try { - String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST)); - if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) { - recordLogService.saveLogs(username, startTime, "访问IP已被列入系统黑名单", "很遗憾,访问IP已被列入系统黑名单", null, "失败"); - throw new ServiceException("很遗憾,访问IP已被列入系统黑名单"); - } - } catch (Exception e) { - recordLogService.saveLogs(username, startTime, "IP黑名单校验异常", e.getMessage(), null, "失败"); - throw new ServiceException("IP黑名单校验失败,请稍后重试"); - } - } - - /** - * 验证用户查询结果 - * - * @param username 用户名 - * @param userResult 用户查询结果 - * @param startTime 操作开始时间 - */ - private void validateUserResult(String username, R userResult, long startTime) { - if (userResult == null || userResult.getData() == null || R.FAIL == userResult.getCode()) { - recordLogService.saveLogs(username, startTime, "登录用户不存在", "用户名/密码错误", null, "失败"); - throw new ServiceException(userResult == null ? "用户名/密码错误" : userResult.getMsg()); - } - } - - - private void validateApprovalStatus(String username, SysUser user, long startTime) { - if ("0".equals(user.getApprovalStatus())) { - recordLogService.saveLogs(username, startTime, "账号未审批", "用户不存在", null, "失败"); - throw new ServiceException("账号未审批,请联系管理员"); - } - } - - /** - * 验证用户状态 - * - * @param username 用户名 - * @param user 用户信息 - * @param startTime 操作开始时间 - */ - private void validateUserStatus(String username, SysUser user, long startTime) { - if (UserStatus.DELETED.getCode().equals(user.getDelFlag()) || UserStatus.DISABLE.getCode().equals(user.getStatus())) { - recordLogService.saveLogs(username, startTime, "账号已被删除或停用", "用户不存在", null, "失败"); - throw new ServiceException("账号已被删除或停用,请联系管理员"); - } - } - - /** - * 处理IP校验 - * - * @param username 用户名 - * @param user 用户信息 - * @param startTime 操作开始时间 - */ - private void handleIpValidation(String username, SysUser user, long startTime) { - try { - String nowIp = IpUtils.getIpAddr(); - String hisIp = redisService.getCacheObject("IP:" + user.getUserId().toString()); - - if (!nowIp.equals(hisIp)) { - recordLogService.saveErrorLogs(username, startTime, user.getUserId().toString()); - } - redisService.setCacheObject("IP:" + user.getUserId().toString(), nowIp, 5L, TimeUnit.MINUTES); - } catch (Exception e) { - recordLogService.saveLogs(username, startTime, "IP校验异常", e.getMessage(), null, "失败"); - throw new ServiceException("IP校验失败,请稍后重试"); - } - } - /** * 用户登出 * @@ -336,13 +168,14 @@ public class SysLoginService { if (StringUtils.isAnyBlank(registerBody.getUsername(), registerBody.getPassword()) || registerBody.getUsername().length() < UserConstants.USERNAME_MIN_LENGTH || - registerBody.getUsername().length() > UserConstants.USERNAME_MAX_LENGTH || - registerBody.getPassword().length() < UserConstants.PASSWORD_MIN_LENGTH || - registerBody.getPassword().length() > UserConstants.PASSWORD_MAX_LENGTH) { + registerBody.getUsername().length() > UserConstants.USERNAME_MAX_LENGTH) { recordLogService.saveLogs(registerBody.getUsername(), startTime, "注册参数无效", "账户或密码长度不符合要求", null, "失败"); throw new ServiceException("账户或密码长度不符合要求"); } - + AjaxResult ajaxResult = passwordValidatorService.validatePassword(registerBody.getUsername(), registerBody.getPassword()); + if (ajaxResult.isError()) { + throw new ServiceException((String) ajaxResult.get("msg")); + } SysUser sysUser = new SysUser(); sysUser.setUserName(registerBody.getUsername()); sysUser.setNickName(registerBody.getNickName()); diff --git a/bonus-auth/src/main/java/com/bonus/auth/service/UsernamePasswordLoginStrategy.java b/bonus-auth/src/main/java/com/bonus/auth/service/UsernamePasswordLoginStrategy.java new file mode 100644 index 0000000..852468f --- /dev/null +++ b/bonus-auth/src/main/java/com/bonus/auth/service/UsernamePasswordLoginStrategy.java @@ -0,0 +1,58 @@ +package com.bonus.auth.service; + +import com.bonus.common.core.constant.SecurityConstants; +import com.bonus.common.core.domain.R; +import com.bonus.system.api.RemoteUserService; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author bonus + */ +@Service +public class UsernamePasswordLoginStrategy implements LoginStrategy { + @Resource + private RemoteUserService remoteUserService; + + @Resource + private PasswordValidatorService passwordValidatorService; + + @Resource + private SysPasswordService passwordService; + + + /** + * 登录方法 + * + * @param username 用户的标识符(用户名、手机号、邮箱) + * @param password 用户凭据(密码或验证码) + * @return 登录结果 + */ + @Override + public LoginUser login(String username, String password) { + //参数校验 + passwordValidatorService.validateLoginParameters(username, password); + // IP黑名单校验; + passwordValidatorService.validateIpBlacklist(username); + //通过用户名获取人员信息 + R userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); + //获取用户信息 + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + // 验证用户查询结果 + passwordValidatorService.validateUserResult(username, userResult); + passwordValidatorService.validateApprovalStatus(username, user); + // 验证用户状态 + passwordValidatorService.validateUserStatus(username, user); + // 验证密码 + passwordService.validate(user, password, System.currentTimeMillis()); + // 处理IP校验 + passwordValidatorService.handleIpValidation(username, user); + //返回信息 + return userInfo; + } + +} diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/PasswordPolicyConfig.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/PasswordPolicyConfig.java new file mode 100644 index 0000000..ea8f4bb --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/PasswordPolicyConfig.java @@ -0,0 +1,57 @@ +package com.bonus.system.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 该类用于从 `application.yml` 中加载密码策略的配置项。 + * 使用 @ConfigurationProperties 注解,前缀为 password-policy。 + * @author bonus + */ +@Component +@ConfigurationProperties(prefix = "password-policy") +@Data +public class PasswordPolicyConfig { + + // 密码的最小长度 + private int minLength; + + // 密码的最大长度 + private int maxLength; + + // 是否需要包含大写字母 + private boolean requireUpperCase; + + // 是否需要包含小写字母 + private boolean requireLowerCase; + + // 是否需要包含数字 + private boolean requireDigit; + + // 是否需要包含特殊字符 + private boolean requireSpecialChar; + + // 常见的弱密码列表,禁止使用这些密码 + private List weakPasswords; + + // 密码历史记录限制 + private int passwordHistoryLimit; + + // 是否限制连续相同字符 + private boolean restrictConsecutiveChars; + + // 最大允许的连续字符数 + private int maxConsecutiveChars; + + // 密码中是否不能包含用户名 + private boolean excludeUsernameInPassword; + + // 是否在首次登录时强制修改密码 + private boolean forcePasswordChangeOnFirstLogin; + + // 定期修改密码 + private int regularlyChangePassword; +} diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysProfileController.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysProfileController.java index 16a7e33..e91f93c 100644 --- a/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysProfileController.java +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysProfileController.java @@ -1,101 +1,96 @@ package com.bonus.system.controller; -import java.util.Arrays; - -import com.bonus.common.core.constant.ValidateUtils; -import com.bonus.common.log.annotation.SysLog; -import com.bonus.common.log.enums.OperaType; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import com.bonus.common.core.domain.R; import com.bonus.common.core.utils.StringUtils; import com.bonus.common.core.utils.file.FileTypeUtils; import com.bonus.common.core.utils.file.MimeTypeUtils; import com.bonus.common.core.web.controller.BaseController; import com.bonus.common.core.web.domain.AjaxResult; +import com.bonus.common.log.annotation.SysLog; +import com.bonus.common.log.enums.OperaType; import com.bonus.common.security.service.TokenService; import com.bonus.common.security.utils.SecurityUtils; import com.bonus.system.api.RemoteFileService; import com.bonus.system.api.domain.SysFile; import com.bonus.system.api.domain.SysUser; import com.bonus.system.api.model.LoginUser; +import com.bonus.system.domain.UserPasswordHistory; import com.bonus.system.service.ISysUserService; +import com.bonus.system.service.PasswordValidatorService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.Arrays; /** * 个人信息 业务处理 - * + * * @author bonus */ @Slf4j @RestController @RequestMapping("/user/profile") -public class SysProfileController extends BaseController -{ +public class SysProfileController extends BaseController { @Autowired private ISysUserService userService; - + @Autowired private TokenService tokenService; - + @Autowired private RemoteFileService remoteFileService; + @Resource + private PasswordValidatorService passwordValidatorService; + /** * 个人信息 */ @GetMapping public AjaxResult profile() { - try{ - String username = SecurityUtils.getUsername(); - SysUser user = userService.selectUserByUserName(username); - AjaxResult ajax = AjaxResult.success(user); - ajax.put("roleGroup", userService.selectUserRoleGroup(username)); - ajax.put("postGroup", userService.selectUserPostGroup(username)); - return ajax; - }catch (Exception e){ - log.error(e.toString(),e); - } - return error("数据查询异常"); + try { + String username = SecurityUtils.getUsername(); + SysUser user = userService.selectUserByUserName(username); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(username)); + ajax.put("postGroup", userService.selectUserPostGroup(username)); + return ajax; + } catch (Exception e) { + log.error(e.toString(), e); + } + return error("数据查询异常"); } /** * 修改用户 */ @PutMapping - @SysLog(title = "个人中心", businessType = OperaType.UPDATE,logType = 0,module = "首页->个人中心") + @SysLog(title = "个人中心", businessType = OperaType.UPDATE, logType = 0, module = "首页->个人中心") public AjaxResult updateProfile(@RequestBody SysUser user) { - try{ + try { LoginUser loginUser = SecurityUtils.getLoginUser(); SysUser currentUser = loginUser.getSysUser(); currentUser.setNickName(user.getNickName()); currentUser.setEmail(user.getEmail()); currentUser.setPhonenumber(user.getPhonenumber()); currentUser.setSex(user.getSex()); - if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) - { + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) { return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在"); } - if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) - { + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) { return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在"); } - if (userService.updateUserProfile(currentUser) > 0) - { + if (userService.updateUserProfile(currentUser) > 0) { // 更新缓存用户信息 tokenService.setLoginUser(loginUser); return success(); } return error("修改个人信息异常,请联系管理员"); - }catch (Exception e){ - log.error(e.toString(),e); + } catch (Exception e) { + log.error(e.toString(), e); } return error("修改个人信息异常,请联系管理员"); } @@ -104,71 +99,71 @@ public class SysProfileController extends BaseController * 重置密码 */ @PutMapping("/updatePwd") - @SysLog(title = "个人中心", businessType = OperaType.UPDATE,logType = 0,module = "首页->个人中心",details = "修改密码") + @SysLog(title = "个人中心", businessType = OperaType.UPDATE, logType = 0, module = "首页->个人中心", details = "修改密码") public AjaxResult updatePwd(String oldPassword, String newPassword) { - try{ - String username = SecurityUtils.getUsername(); - SysUser user = userService.selectUserByUserName(username); - String password = user.getPassword(); - String msg= ValidateUtils.isPwd(oldPassword); - if (StringUtils.isNotEmpty(msg)) { - return error(msg); - } - if (!SecurityUtils.matchesPassword(oldPassword, password)) { - return error("修改密码失败,旧密码错误"); - } - if (SecurityUtils.matchesPassword(newPassword, password)) { - return error("新密码不能与旧密码相同"); - } - newPassword = SecurityUtils.encryptPassword(newPassword); - if (userService.resetUserPwd(username, newPassword) > 0) - { - // 更新缓存用户密码 - LoginUser loginUser = SecurityUtils.getLoginUser(); - loginUser.getSysUser().setPassword(newPassword); - tokenService.setLoginUser(loginUser); - return success(); - } - return error("修改密码异常,请联系管理员"); - }catch (Exception e){ - return error("修改密码异常,请联系管理员"); - } + try { + String username = SecurityUtils.getUsername(); + SysUser user = userService.selectUserByUserName(username); + + String password = user.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) { + return error("修改密码失败,旧密码错误"); + } + AjaxResult ajaxResult = passwordValidatorService.validatePassword(user.getUserId(), user.getUserName(), password, newPassword); + if (ajaxResult.isError()) { + return ajaxResult; + } + newPassword = SecurityUtils.encryptPassword(newPassword); + if (userService.resetUserPwd(username, newPassword) > 0) { + // 更新缓存用户密码 + LoginUser loginUser = SecurityUtils.getLoginUser(); + loginUser.getSysUser().setPassword(newPassword); + tokenService.setLoginUser(loginUser); + UserPasswordHistory userPasswordHistory = new UserPasswordHistory(); + userPasswordHistory.setUserId(user.getUserId()); + userPasswordHistory.setChangeUser(SecurityUtils.getUserId()); + userPasswordHistory.setNewPassword(newPassword); + userPasswordHistory.setOldPassword(password); + passwordValidatorService.addPasswordExpiry(userPasswordHistory); + return success(); + } + return error("修改密码异常,请联系管理员"); + } catch (Exception e) { + return error("修改密码异常,请联系管理员"); } - + } + /** * 头像上传 */ @PostMapping("/avatar") - @SysLog(title = "个人中心", businessType = OperaType.UPDATE,logType = 0,module = "首页->个人中心",details = "头像上传") + @SysLog(title = "个人中心", businessType = OperaType.UPDATE, logType = 0, module = "首页->个人中心", details = "头像上传") public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) { - try{ - if (!file.isEmpty()) { - LoginUser loginUser = SecurityUtils.getLoginUser(); - String extension = FileTypeUtils.getExtension(file); - if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) - { - return error("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式"); - } - R fileResult = remoteFileService.upload(file); - if (StringUtils.isNull(fileResult) || StringUtils.isNull(fileResult.getData())) - { - return error("文件服务异常,请联系管理员"); - } - String url = fileResult.getData().getUrl(); - if (userService.updateUserAvatar(loginUser.getUsername(), url)) - { - AjaxResult ajax = AjaxResult.success(); - ajax.put("imgUrl", url); - // 更新缓存用户头像 - loginUser.getSysUser().setAvatar(url); - tokenService.setLoginUser(loginUser); - return ajax; - } - } - return error("上传图片异常,请联系管理员"); - }catch (Exception e){ - log.error(e.toString(),e); - } + try { + if (!file.isEmpty()) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + String extension = FileTypeUtils.getExtension(file); + if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) { + return error("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式"); + } + R fileResult = remoteFileService.upload(file); + if (StringUtils.isNull(fileResult) || StringUtils.isNull(fileResult.getData())) { + return error("文件服务异常,请联系管理员"); + } + String url = fileResult.getData().getUrl(); + if (userService.updateUserAvatar(loginUser.getUsername(), url)) { + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", url); + // 更新缓存用户头像 + loginUser.getSysUser().setAvatar(url); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } catch (Exception e) { + log.error(e.toString(), e); + } return error("上传图片异常,请联系管理员"); } } diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysUserController.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysUserController.java index 057d66d..9e727e3 100644 --- a/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysUserController.java +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/controller/SysUserController.java @@ -1,34 +1,13 @@ package com.bonus.system.controller; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import javax.servlet.http.HttpServletResponse; - -import com.bonus.common.core.constant.ValidateUtils; -import com.bonus.common.log.annotation.SysLog; -import com.bonus.common.log.enums.OperaType; -import org.apache.commons.lang3.ArrayUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; import com.bonus.common.core.domain.R; import com.bonus.common.core.utils.StringUtils; import com.bonus.common.core.utils.poi.ExcelUtil; import com.bonus.common.core.web.controller.BaseController; import com.bonus.common.core.web.domain.AjaxResult; import com.bonus.common.core.web.page.TableDataInfo; +import com.bonus.common.log.annotation.SysLog; +import com.bonus.common.log.enums.OperaType; import com.bonus.common.security.annotation.InnerAuth; import com.bonus.common.security.annotation.RequiresPermissions; import com.bonus.common.security.utils.SecurityUtils; @@ -36,12 +15,21 @@ import com.bonus.system.api.domain.SysDept; import com.bonus.system.api.domain.SysRole; import com.bonus.system.api.domain.SysUser; import com.bonus.system.api.model.LoginUser; -import com.bonus.system.service.ISysConfigService; -import com.bonus.system.service.ISysDeptService; -import com.bonus.system.service.ISysPermissionService; -import com.bonus.system.service.ISysPostService; -import com.bonus.system.service.ISysRoleService; -import com.bonus.system.service.ISysUserService; +import com.bonus.system.domain.UserPasswordHistory; +import com.bonus.system.service.*; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * 用户信息 @@ -69,6 +57,9 @@ public class SysUserController extends BaseController { @Autowired private ISysConfigService configService; + @Resource + private PasswordValidatorService passwordValidatorService; + /** * 获取用户列表 */ @@ -107,6 +98,7 @@ public class SysUserController extends BaseController { List userList = util.importExcel(file.getInputStream()); String operName = SecurityUtils.getUsername(); String message = userService.importUser(userList, updateSupport, operName); + return success(message); } catch (Exception e) { logger.error(e.toString(), e); @@ -162,6 +154,7 @@ public class SysUserController extends BaseController { if (!userService.checkUserNameUnique(sysUser)) { return R.fail("保存用户'" + username + "'失败,注册账号已存在"); } + return R.ok(userService.registerUser(sysUser)); } @@ -231,12 +224,17 @@ public class SysUserController extends BaseController { } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); } - String pwd = ValidateUtils.isPwd(user.getPassword()); + /*String pwd = ValidateUtils.isPwd(user.getPassword()); if (StringUtils.isNotEmpty(pwd)) { return error("新增用户'" + user.getUserName() + "'失败," + pwd); + }*/ + AjaxResult ajaxResult = passwordValidatorService.validatePassword(-100L, user.getUserName(), "", user.getPassword()); + if (ajaxResult.isError()) { + return ajaxResult; } user.setCreateBy(SecurityUtils.getUsername()); user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); } catch (Exception e) { logger.error(e.toString(), e); @@ -297,10 +295,21 @@ public class SysUserController extends BaseController { @SysLog(title = "用户管理", businessType = OperaType.UPDATE, logType = 0, module = "系统管理->用户管理", details = "重置用户密码") public AjaxResult resetPwd(@RequestBody SysUser user) { try { + SysUser sysUser = userService.selectUserById(user.getUserId()); + AjaxResult ajaxResult = passwordValidatorService.validatePassword(sysUser.getUserId(), sysUser.getUserName(), sysUser.getPassword(), user.getPassword()); + if (ajaxResult.isError()) { + return ajaxResult; + } userService.checkUserAllowed(user); userService.checkUserDataScope(user.getUserId()); user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); user.setUpdateBy(SecurityUtils.getUsername()); + UserPasswordHistory userPasswordHistory = new UserPasswordHistory(); + userPasswordHistory.setUserId(user.getUserId()); + userPasswordHistory.setChangeUser(SecurityUtils.getUserId()); + userPasswordHistory.setNewPassword(user.getPassword()); + userPasswordHistory.setOldPassword(sysUser.getPassword()); + passwordValidatorService.addPasswordExpiry(userPasswordHistory); return toAjax(userService.resetPwd(user)); } catch (Exception e) { logger.error(e.toString(), e); @@ -390,4 +399,22 @@ public class SysUserController extends BaseController { } return error("系统异常,请联系管理员"); } + + @GetMapping("/checkPasswordStatus") + public AjaxResult checkPasswordStatus() { + // 1. 先检查是否是首次登录 + boolean firstLoginResult = passwordValidatorService.checkFirstLogin(); + if (firstLoginResult) { + return AjaxResult.success("首次登录需要修改密码", true); + } + // 2. 再检查密码是否已过期 + boolean passwordExpiryResult = passwordValidatorService.checkPasswordExpiry(); + // 如果密码已过期,返回密码过期的提示 + if (passwordExpiryResult) { + return AjaxResult.success("密码已过期,需要修改密码", true); + } + // 3. 如果都不需要操作,返回成功 + return AjaxResult.success(false); + } + } diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/domain/UserPasswordHistory.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/domain/UserPasswordHistory.java new file mode 100644 index 0000000..8f23fcc --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/domain/UserPasswordHistory.java @@ -0,0 +1,38 @@ +package com.bonus.system.domain; + +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author bonus + */ +@Data +public class UserPasswordHistory implements Serializable { + /** + * 记录的唯一标识符 + */ + private Long id; + /** + * 用户的唯一标识符 + */ + private Long userId; + /** + * 旧密码的哈希值 + */ + private String oldPassword; + /** + * 新密码的哈希值 + */ + private String newPassword; + /** + * 密码变更时间 + */ + private LocalDateTime changeTimestamp; + /** + * 变更人 + */ + private Long changeUser; + +} diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/PasswordValidatorService.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/PasswordValidatorService.java new file mode 100644 index 0000000..d3d63cc --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/PasswordValidatorService.java @@ -0,0 +1,48 @@ +package com.bonus.system.service; + +import com.bonus.common.core.web.domain.AjaxResult; +import com.bonus.system.domain.UserPasswordHistory; + +/** + * 密码校验器接口类,用于根据配置对用户输入的密码进行验证。 + * 提供对新密码的校验、首次登录检测、定期密码修改功能。 + * + * @author bonus + */ +public interface PasswordValidatorService { + + /** + * 对新密码进行校验,返回 AjaxResult 表示密码是否符合要求。 + * + * @param username 用户名,不能包含在密码中 + * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @return 如果密码符合策略要求,则返回 AjaxResult 对象 + */ + AjaxResult validatePassword(Long userId, String username, String oldPassword, String newPassword); + + /** + * 检查用户是否是首次登录。 + * + * @return 如果是首次登录,返回 AjaxResult 提示用户修改密码,否则返回错误信息 + */ + boolean checkFirstLogin(); + + /** + * 检查用户密码是否已过期,提示用户定期修改密码。 + * + * @return 如果需要修改密码,返回 AjaxResult 提示用户修改,否则返回成功信息 + */ + boolean checkPasswordExpiry(); + + /** + * 检查新密码是否在最近五次修改的密码中 + * + * @param userId 用户ID + * @param newPassword 新密码 + * @return 如果新密码在最近的五次修改记录中,返回 true,否则返回 false + */ + AjaxResult isPasswordInRecentHistory(Long userId, String newPassword); + + int addPasswordExpiry(UserPasswordHistory userPasswordHistory); +} diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java new file mode 100644 index 0000000..0e29db8 --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/PasswordValidatorServiceImpl.java @@ -0,0 +1,225 @@ +package com.bonus.system.service.impl; + +import com.bonus.common.core.utils.DateUtils; +import com.bonus.common.core.web.domain.AjaxResult; +import com.bonus.common.security.utils.SecurityUtils; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.api.model.LoginUser; +import com.bonus.system.config.PasswordPolicyConfig; +import com.bonus.system.domain.UserPasswordHistory; +import com.bonus.system.mapper.PasswordValidatorMapper; +import com.bonus.system.service.ISysConfigService; +import com.bonus.system.service.PasswordValidatorService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * 密码校验器类,用于根据配置对用户输入的密码进行验证。 + */ +@Service +public class PasswordValidatorServiceImpl implements PasswordValidatorService { + + @Resource + private PasswordPolicyConfig config; + + @Resource + private ISysConfigService configService; + + @Resource + private PasswordValidatorMapper passwordValidatorMapper; + + + @Override + public AjaxResult validatePassword(Long userId, String username, String oldPassword, String newPassword) { + // 1. 检查密码长度是否符合配置 + if (newPassword.length() < config.getMinLength() || newPassword.length() > config.getMaxLength()) { + return AjaxResult.error("密码长度应为" + config.getMinLength() + "至" + config.getMaxLength() + "位!"); + } + // 2. 检查密码是否包含大写字母、小写字母、数字和特殊字符 + boolean hasUpperCase = false, hasLowerCase = false, hasDigit = false, hasSpecialChar = false; + for (char c : newPassword.toCharArray()) { + if (Character.isUpperCase(c)) { + hasUpperCase = true; + } + if (Character.isLowerCase(c)) { + hasLowerCase = true; + } + if (Character.isDigit(c)) { + hasDigit = true; + } + if ("!@#$%^&*()-_=+[{]};:'\",<.>/?".indexOf(c) >= 0) { + hasSpecialChar = true; + } + } + + // 根据配置检查大写字母、小写字母、数字和特殊字符的要求 + if (config.isRequireUpperCase() && !hasUpperCase) { + return AjaxResult.error("密码必须包含大写字母!"); + + } + + if (config.isRequireLowerCase() && !hasLowerCase) { + return AjaxResult.error("密码必须包含小写字母!"); + } + + if (config.isRequireDigit() && !hasDigit) { + return AjaxResult.error("密码必须包含数字!"); + } + + if (config.isRequireSpecialChar() && !hasSpecialChar) { + return AjaxResult.error("密码必须包含特殊字符!"); + } + + // 3. 检查是否包含常见弱密码 + for (String weakPwd : config.getWeakPasswords()) { + if (newPassword.contains(weakPwd)) { + return AjaxResult.error("密码包含常见的弱密码片段: " + weakPwd); + } + } + + // 4. 检查是否包含超过规定数量的连续字符 + if (config.isRestrictConsecutiveChars() && containsConsecutiveCharacters(newPassword, config.getMaxConsecutiveChars())) { + return AjaxResult.error("密码不能包含超过" + config.getMaxConsecutiveChars() + "位连续字符!"); + } + + // 5. 检查密码中是否包含用户名 + if (config.isExcludeUsernameInPassword() && newPassword.contains(username)) { + return AjaxResult.error("密码不能包含账号!"); + } + + // 6. 检查新密码是否与旧密码相同 + if (SecurityUtils.matchesPassword(newPassword, oldPassword)) { + return AjaxResult.error("新密码不能与原密码相同!"); + } + + // 7. 检查新密码是否与历史密码相同 + List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); + // 如果没有找到密码历史记录,返回提示 + if (userPasswordHistories.isEmpty()) { + return AjaxResult.success("没有找到密码历史记录。"); + } + // 只取最近的五次修改记录 + // 使用 Math.min 以确保不会超过实际记录数 + int limit = Math.min(userPasswordHistories.size(), config.getPasswordHistoryLimit()); + for (int i = 0; i < limit; i++) { + UserPasswordHistory history = userPasswordHistories.get(i); + // 比较新密码与历史密码 + if (SecurityUtils.matchesPassword(newPassword, history.getNewPassword())) { + return AjaxResult.error("新密码不能与最近的" + config.getPasswordHistoryLimit() + "个旧密码相同!"); + } + } + + return AjaxResult.success(); + } + + /** + * 检查用户是否是首次登录 + * + * @return 如果密码符合策略要求,则返回 true,否则返回 false + */ + @Override + public boolean checkFirstLogin() { + // 如果未开启首次登录强制修改密码功能,直接返回错误 + if (!config.isForcePasswordChangeOnFirstLogin()) { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser sysUser = loginUser.getSysUser(); + // 获取初始密码配置 + String initialPassword = configService.selectConfigByKey("sys.user.initPassword"); + // 检查当前用户密码是否是初始密码 + // 根据检查结果返回相应的响应 + return SecurityUtils.matchesPassword(initialPassword, sysUser.getPassword()); + } + + /** + * 检查用户密码是否已过期,提示用户定期修改密码。 + * + * @return 如果需要修改密码,返回 AjaxResult 提示用户修改,否则返回成功信息 + */ + @Override + public boolean checkPasswordExpiry() { + Long userId = SecurityUtils.getUserId(); + List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); + // 如果没有找到密码历史记录,返回错误 + if (userPasswordHistories.isEmpty()) { + return false; + } + // 获取最近的密码修改记录 + UserPasswordHistory userPasswordHistory = userPasswordHistories.get(0); + // 获取当前日期 + Date currentDate = DateUtils.getNowDate(); + // 获取最近密码修改日期 + Date changeDate = DateUtils.toDate(userPasswordHistory.getChangeTimestamp()); + + // 计算最近密码修改到当前日期的天数 + long daysSinceChange = DateUtils.daysBetween(changeDate, currentDate); + // 检查是否达到定期修改密码的天数要求 + return daysSinceChange > config.getRegularlyChangePassword(); + } + + /** + * 检查新密码是否在最近五次修改的密码中 + * + * @param userId 用户ID + * @param newPassword 新密码 + * @return 如果新密码在最近的五次修改记录中,返回错误提示,否则返回成功提示 + */ + @Override + public AjaxResult isPasswordInRecentHistory(Long userId, String newPassword) { + List userPasswordHistories = passwordValidatorMapper.checkPasswordExpiry(userId); + + // 如果没有找到密码历史记录,返回提示 + if (userPasswordHistories.isEmpty()) { + return AjaxResult.success("没有找到密码历史记录。"); + } + + // 只取最近的五次修改记录 + // 使用 Math.min 以确保不会超过实际记录数 + int limit = Math.min(userPasswordHistories.size(), config.getPasswordHistoryLimit()); + for (int i = 0; i < limit; i++) { + UserPasswordHistory history = userPasswordHistories.get(i); + // 比较新密码与历史密码 + if (SecurityUtils.matchesPassword(newPassword, history.getNewPassword())) { + return AjaxResult.error("新密码不能与最近的密码相同。"); // 新密码在最近五次记录中 + } + } + + return AjaxResult.success("新密码有效。"); // 新密码不在记录中 + } + + /** + * @param userPasswordHistory + * @return + */ + @Override + public int addPasswordExpiry(UserPasswordHistory userPasswordHistory) { + return passwordValidatorMapper.addPasswordExpiry(userPasswordHistory); + } + + /** + * 帮助方法,用于检测密码是否包含超过 n 个连续相同字符。 + * + * @param password 需要检查的密码 + * @param n 允许的最大连续相同字符数量 + * @return 如果包含连续字符,则返回 true,否则返回 false + */ + private boolean containsConsecutiveCharacters(String password, int n) { + for (int i = 0; i <= password.length() - n; i++) { + boolean consecutive = true; + for (int j = 1; j < n; j++) { + if (password.charAt(i + j) != password.charAt(i)) { + consecutive = false; + break; + } + } + if (consecutive) { + return true; + } + } + return false; + } +} diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/SysUserServiceImpl.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/SysUserServiceImpl.java index 7db1256..001e760 100644 --- a/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/SysUserServiceImpl.java +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/service/impl/SysUserServiceImpl.java @@ -1,13 +1,24 @@ package com.bonus.system.service.impl; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import javax.validation.Validator; - +import com.bonus.common.core.constant.UserConstants; import com.bonus.common.core.domain.R; +import com.bonus.common.core.exception.ServiceException; +import com.bonus.common.core.utils.SpringUtils; +import com.bonus.common.core.utils.StringUtils; +import com.bonus.common.core.utils.bean.BeanValidators; import com.bonus.common.core.utils.sms.SmsUtils; import com.bonus.common.core.web.domain.BaseEntity; +import com.bonus.common.datascope.annotation.DataScope; +import com.bonus.common.security.utils.SecurityUtils; +import com.bonus.system.api.domain.SysRole; +import com.bonus.system.api.domain.SysUser; +import com.bonus.system.domain.SysPost; +import com.bonus.system.domain.SysUserPost; +import com.bonus.system.domain.SysUserRole; +import com.bonus.system.mapper.*; +import com.bonus.system.service.ISysConfigService; +import com.bonus.system.service.ISysDeptService; +import com.bonus.system.service.ISysUserService; import com.bonus.system.utils.CommonDataPermissionInfo; import org.apache.poi.ss.formula.functions.T; import org.slf4j.Logger; @@ -20,26 +31,11 @@ import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import com.bonus.common.core.constant.UserConstants; -import com.bonus.common.core.exception.ServiceException; -import com.bonus.common.core.utils.SpringUtils; -import com.bonus.common.core.utils.StringUtils; -import com.bonus.common.core.utils.bean.BeanValidators; -import com.bonus.common.datascope.annotation.DataScope; -import com.bonus.common.security.utils.SecurityUtils; -import com.bonus.system.api.domain.SysRole; -import com.bonus.system.api.domain.SysUser; -import com.bonus.system.domain.SysPost; -import com.bonus.system.domain.SysUserPost; -import com.bonus.system.domain.SysUserRole; -import com.bonus.system.mapper.SysPostMapper; -import com.bonus.system.mapper.SysRoleMapper; -import com.bonus.system.mapper.SysUserMapper; -import com.bonus.system.mapper.SysUserPostMapper; -import com.bonus.system.mapper.SysUserRoleMapper; -import com.bonus.system.service.ISysConfigService; -import com.bonus.system.service.ISysDeptService; -import com.bonus.system.service.ISysUserService; + +import javax.validation.Validator; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; /** * 用户 业务层处理 @@ -80,6 +76,8 @@ public class SysUserServiceImpl implements ISysUserService { protected Validator validator; + + @Autowired private JavaMailSender mailSender; // 自动注入JavaMailSender,用于发送邮件 diff --git a/bonus-modules/bonus-system/src/main/resources/mapper/system/PasswordValidatorMapper.xml b/bonus-modules/bonus-system/src/main/resources/mapper/system/PasswordValidatorMapper.xml new file mode 100644 index 0000000..a4d3243 --- /dev/null +++ b/bonus-modules/bonus-system/src/main/resources/mapper/system/PasswordValidatorMapper.xml @@ -0,0 +1,30 @@ + + + + + + INSERT INTO sys_user_password_history (user_id, + old_password, + new_password, + change_timestamp, + change_user) + VALUES (#{userId}, + #{oldPassword}, + #{newPassword}, + NOW(), + #{changeUser}); + + + \ No newline at end of file