用户登录问题修改

This commit is contained in:
jiang 2024-09-08 20:12:44 +08:00
parent b200f2a4b4
commit d192646d04
22 changed files with 1321 additions and 467 deletions

View File

@ -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;
}
}

View File

@ -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<String> weakPasswords;
// 密码历史记录限制
private int passwordHistoryLimit;
// 是否限制连续相同字符
private boolean restrictConsecutiveChars;
// 最大允许的连续字符数
private int maxConsecutiveChars;
// 密码中是否不能包含用户名
private boolean excludeUsernameInPassword;
// 是否在首次登录时强制修改密码
private boolean forcePasswordChangeOnFirstLogin;
}

View File

@ -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<LoginUser> 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<String> 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));
}
}
/**

View File

@ -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<LoginType, LoginStrategy> strategyMap;
@Autowired
public LoginStrategyFactory(List<LoginStrategy> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<LoginUser> 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;
}
}

View File

@ -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<LoginUser> 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;
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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<LoginUser> 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("手机验证码错误");
}
}
}

View File

@ -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<LoginUser> 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;
}
}

View File

@ -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<LoginUser> 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;
}
}

View File

@ -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<T> getPhoneCode(String username, String getMobileCodeType) {
long startTime = System.currentTimeMillis(); // 记录开始时间
// 记录开始时间
long startTime = System.currentTimeMillis();
int contactType = getContactType(username);
R<LoginUser> 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<LoginUser> 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<LoginUser> 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<LoginUser> 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<LoginUser> 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());

View File

@ -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<LoginUser> 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;
}
}

View File

@ -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<String> weakPasswords;
// 密码历史记录限制
private int passwordHistoryLimit;
// 是否限制连续相同字符
private boolean restrictConsecutiveChars;
// 最大允许的连续字符数
private int maxConsecutiveChars;
// 密码中是否不能包含用户名
private boolean excludeUsernameInPassword;
// 是否在首次登录时强制修改密码
private boolean forcePasswordChangeOnFirstLogin;
// 定期修改密码
private int regularlyChangePassword;
}

View File

@ -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<SysFile> 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<SysFile> 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("上传图片异常,请联系管理员");
}
}

View File

@ -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<SysUser> 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);
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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<UserPasswordHistory> 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<UserPasswordHistory> 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<UserPasswordHistory> 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;
}
}

View File

@ -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用于发送邮件

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bonus.system.mapper.PasswordValidatorMapper">
<!-- 插入密码历史记录 -->
<insert id="addPasswordExpiry">
INSERT INTO sys_user_password_history (user_id,
old_password,
new_password,
change_timestamp,
change_user)
VALUES (#{userId},
#{oldPassword},
#{newPassword},
NOW(),
#{changeUser});
</insert>
<select id="checkPasswordExpiry" resultType="com.bonus.system.domain.UserPasswordHistory">
SELECT id AS id,
old_password AS oldPassword,
new_password AS newPassword,
change_timestamp AS changeTimestamp,
change_user AS changeUser
FROM sys_user_password_history
WHERE user_id = #{userId}
ORDER BY change_timestamp DESC
LIMIT 5;
</select>
</mapper>