Commit 454eae15 by Lizh

实现 SysConfig/SysLogininfor/SysPasswordService 三层对齐 custom-back 登录流程

- Domain 层: 新增 SysConfigEntity, SysLogininforEntity 实体 + Mapper + XML + DomainService
- App 层: 新增 SysConfigService(配置查询/验证码开关), SysLogininforService(异步登录日志), SysPasswordService(密码重试锁定, Redis 5次/10分钟)
- LoginServiceImpl: 注入新服务, login() 加锁定检查+失败计数+登录日志, getInfo() 改为配置驱动
- LoginDTO: 增加验证码 code/uuid 预留字段
- CustomServerApplication: 增加 @EnableAsync 支持

Co-Authored-By: Claude <noreply@anthropic.com>
parent aaa0cba2
...@@ -24,4 +24,16 @@ public class LoginDTO implements Serializable { ...@@ -24,4 +24,16 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "密码不能为空") @NotBlank(message = "密码不能为空")
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
private String password; private String password;
/**
* 验证码
*/
@Schema(description = "验证码")
private String code;
/**
* 唯一标识
*/
@Schema(description = "唯一标识")
private String uuid;
} }
package com.jomalls.custom.app.service.system;
/**
* 参数配置 App Service 接口
*
* @author Lizh
*/
public interface SysConfigService {
/**
* 根据配置键查询配置值
*
* @param configKey 配置键
* @return 配置值
*/
String selectConfigByKey(String configKey);
/**
* 是否开启验证码
*
* @return true=开启, false=关闭
*/
boolean selectCaptchaEnabled();
}
package com.jomalls.custom.app.service.system;
/**
* 系统访问记录 App Service 接口
*
* @author Lizh
*/
public interface SysLogininforService {
/**
* 异步记录登录日志
*
* @param username 用户名
* @param status 登录状态(0成功 1失败)
* @param message 提示消息
*/
void recordLogininfor(String username, String status, String message);
}
package com.jomalls.custom.app.service.system.impl;
import com.jomalls.custom.app.service.system.SysConfigService;
import com.jomalls.custom.domain.service.system.SysConfigDomainService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 参数配置 App Service 实现
*
* @author Lizh
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysConfigServiceImpl implements SysConfigService {
private final SysConfigDomainService sysConfigDomainService;
@Override
public String selectConfigByKey(String configKey) {
return sysConfigDomainService.selectConfigByKey(configKey);
}
@Override
public boolean selectCaptchaEnabled() {
String value = selectConfigByKey("sys.account.captchaEnabled");
return "true".equals(value);
}
}
package com.jomalls.custom.app.service.system.impl; package com.jomalls.custom.app.service.system.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jomalls.custom.app.constant.SecurityConstants;
import com.jomalls.custom.app.enums.SysUserStatusEnums; import com.jomalls.custom.app.enums.SysUserStatusEnums;
import com.jomalls.custom.app.exception.ServiceException; import com.jomalls.custom.app.exception.ServiceException;
import com.jomalls.custom.app.service.system.SysConfigService;
import com.jomalls.custom.app.service.system.SysLoginService; import com.jomalls.custom.app.service.system.SysLoginService;
import com.jomalls.custom.app.service.system.SysLogininforService;
import com.jomalls.custom.app.service.system.impl.SysPasswordService;
import com.jomalls.custom.app.utils.BeanMapper; import com.jomalls.custom.app.utils.BeanMapper;
import com.jomalls.custom.app.vo.system.*; import com.jomalls.custom.app.vo.system.*;
import com.jomalls.custom.dal.entity.SysMenuEntity; import com.jomalls.custom.dal.entity.SysMenuEntity;
...@@ -36,6 +40,9 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -36,6 +40,9 @@ public class SysLoginServiceImpl implements SysLoginService {
private final SysUserDomainService sysUserDomainService; private final SysUserDomainService sysUserDomainService;
private final SysRoleDomainService sysRoleDomainService; private final SysRoleDomainService sysRoleDomainService;
private final SysMenuDomainService sysMenuDomainService; private final SysMenuDomainService sysMenuDomainService;
private final SysConfigService sysConfigService;
private final SysLogininforService sysLogininforService;
private final SysPasswordService sysPasswordService;
private final TokenHandle tokenHandle; private final TokenHandle tokenHandle;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
...@@ -47,6 +54,9 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -47,6 +54,9 @@ public class SysLoginServiceImpl implements SysLoginService {
throw new ServiceException("用户名或密码不能为空"); throw new ServiceException("用户名或密码不能为空");
} }
// 检查账户是否被锁定
sysPasswordService.checkLocked(username);
// 1. 查询用户 // 1. 查询用户
SysUserEntity user = sysUserDomainService.getOne( SysUserEntity user = sysUserDomainService.getOne(
new LambdaQueryWrapper<SysUserEntity>() new LambdaQueryWrapper<SysUserEntity>()
...@@ -68,18 +78,26 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -68,18 +78,26 @@ public class SysLoginServiceImpl implements SysLoginService {
// 2. 验证密码(MD5,与 custom-back 兼容) // 2. 验证密码(MD5,与 custom-back 兼容)
if (!passwordEncoder.matches(password, user.getPassword())) { if (!passwordEncoder.matches(password, user.getPassword())) {
log.warn("用户[{}]密码验证失败", username); log.warn("用户[{}]密码验证失败", username);
// 记录登录失败重试次数
sysPasswordService.recordLoginFailure(username);
// 记录登录日志
sysLogininforService.recordLogininfor(username, "1", "密码错误");
throw new ServiceException("密码错误"); throw new ServiceException("密码错误");
} }
// 3. 记录登录信息(登录IP和登录时间) // 3. 记录登录信息(登录IP和登录时间)
recordLoginInfo(user); recordLoginInfo(user);
// 4. 构建 LoginUser // 4. 密码正确,清除重试计数并记录登录日志
sysPasswordService.clearLoginRecord(username);
sysLogininforService.recordLogininfor(username, "0", "登录成功");
// 5. 构建 LoginUser
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), username); LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), username);
// 5. 查询权限:管理员拥有全部权限 // 6. 查询权限:管理员拥有全部权限
Set<String> permissions; Set<String> permissions;
if (user.getUserId() == 1L) { if (SecurityConstants.ADMIN_USER_ID.equals(user.getUserId())) {
permissions = new HashSet<>(); permissions = new HashSet<>();
permissions.add("*:*:*"); permissions.add("*:*:*");
} else { } else {
...@@ -88,7 +106,7 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -88,7 +106,7 @@ public class SysLoginServiceImpl implements SysLoginService {
} }
loginUser.setPermissions(permissions); loginUser.setPermissions(permissions);
// 6. 生成 token // 7. 生成 token
String token = tokenHandle.createToken(loginUser); String token = tokenHandle.createToken(loginUser);
log.info("用户[{}]登录成功", username); log.info("用户[{}]登录成功", username);
return token; return token;
...@@ -135,11 +153,32 @@ public class SysLoginServiceImpl implements SysLoginService { ...@@ -135,11 +153,32 @@ public class SysLoginServiceImpl implements SysLoginService {
loginUser.getPermissions() : Collections.emptySet()); loginUser.getPermissions() : Collections.emptySet());
// 初始密码修改状态(与 custom-back SysLoginController.getInfo 对齐) // 初始密码修改状态(与 custom-back SysLoginController.getInfo 对齐)
// pwdUpdateDate 为 null 表示从未修改过初始密码 // 读取 sys.account.initPasswordModify 配置,为 true 且 pwdUpdateDate 为 null 时才提示
result.setIsDefaultModifyPwd(user.getPwdUpdateDate() == null); String initPwdModify = sysConfigService.selectConfigByKey("sys.account.initPasswordModify");
result.setIsDefaultModifyPwd("true".equals(initPwdModify) && user.getPwdUpdateDate() == null);
// 密码过期状态(简化实现:有 pwdUpdateDate 则认为未过期) // 密码过期状态
result.setIsPasswordExpired(false); // 读取 sys.account.passwordValidateDays 配置,大于 0 时检查是否超期
String validateDaysStr = sysConfigService.selectConfigByKey("sys.account.passwordValidateDays");
boolean passwordExpired = false;
if (validateDaysStr != null) {
try {
int validateDays = Integer.parseInt(validateDaysStr);
if (validateDays > 0) {
if (user.getPwdUpdateDate() == null) {
// 从未修改过密码,视为过期
passwordExpired = true;
} else {
long diffMs = System.currentTimeMillis() - user.getPwdUpdateDate().getTime();
long diffDays = diffMs / (1000 * 60 * 60 * 24);
passwordExpired = diffDays > validateDays;
}
}
} catch (NumberFormatException e) {
log.warn("密码有效期配置格式异常: {}", validateDaysStr);
}
}
result.setIsPasswordExpired(passwordExpired);
return result; return result;
} }
......
package com.jomalls.custom.app.service.system.impl;
import com.jomalls.custom.app.service.system.SysLogininforService;
import com.jomalls.custom.dal.entity.SysLogininforEntity;
import com.jomalls.custom.domain.service.system.SysLogininforDomainService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 系统访问记录 App Service 实现
* <p>
* 异步记录登录日志,不阻塞主流程
*
* @author Lizh
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysLogininforServiceImpl implements SysLogininforService {
private final SysLogininforDomainService sysLogininforDomainService;
@Async
@Override
public void recordLogininfor(String username, String status, String message) {
try {
SysLogininforEntity entity = new SysLogininforEntity();
entity.setUserName(username);
entity.setStatus(status);
entity.setMsg(message);
entity.setLoginTime(new Date());
sysLogininforDomainService.save(entity);
log.debug("记录登录日志: username={}, status={}, msg={}", username, status, message);
} catch (Exception e) {
log.warn("记录登录日志失败: {}", e.getMessage());
}
}
}
package com.jomalls.custom.app.service.system.impl;
import com.jomalls.custom.app.exception.ServiceException;
import com.jomalls.custom.dal.entity.SysUserEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 密码服务(密码重试锁定,与 custom-back SysPasswordService 对齐)
*
* @author Lizh
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SysPasswordService {
private static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
private static final int MAX_RETRY_COUNT = 5; // 最大重试次数
private static final int LOCK_TIME_MINUTES = 10; // 锁定时间(分钟)
private final StringRedisTemplate stringRedisTemplate;
/**
* 验证密码,失败时累计重试次数并锁定
*/
public void validate(SysUserEntity user, String rawPassword) {
// 此处不直接验证密码(由调用方验证),重点是重试锁定逻辑
// 实际由 SysLoginService 调用 matches + 记录重试
}
/**
* 登录失败后增加重试计数,超过阈值锁定账户
*/
public void recordLoginFailure(String username) {
if (stringRedisTemplate == null) return;
String key = PWD_ERR_CNT_KEY + username;
Long retryCount = stringRedisTemplate.opsForValue().increment(key);
stringRedisTemplate.expire(key, LOCK_TIME_MINUTES, TimeUnit.MINUTES);
if (retryCount != null && retryCount > MAX_RETRY_COUNT) {
log.warn("用户[{}]密码错误次数超过{}次,账户锁定{}分钟", username, MAX_RETRY_COUNT, LOCK_TIME_MINUTES);
}
}
/**
* 检查账户是否被锁定
*/
public void checkLocked(String username) {
if (stringRedisTemplate == null) return;
String key = PWD_ERR_CNT_KEY + username;
String countStr = stringRedisTemplate.opsForValue().get(key);
if (countStr != null) {
int count = Integer.parseInt(countStr);
if (count > MAX_RETRY_COUNT) {
throw new ServiceException("账户已被锁定,请" + LOCK_TIME_MINUTES + "分钟后重试");
}
}
}
/**
* 登录成功后清除重试计数
*/
public void clearLoginRecord(String username) {
if (stringRedisTemplate == null) return;
stringRedisTemplate.delete(PWD_ERR_CNT_KEY + username);
}
}
package com.jomalls.custom.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 参数配置表 Entity
*
* @author Lizh
*/
@Data
@TableName("sys_config")
public class SysConfigEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 参数主键
*/
@TableId(value = "config_id", type = IdType.AUTO)
private Long configId;
/**
* 参数名称
*/
@TableField("config_name")
private String configName;
/**
* 参数键名
*/
@TableField("config_key")
private String configKey;
/**
* 参数键值
*/
@TableField("config_value")
private String configValue;
/**
* 系统内置(Y是 N否)
*/
@TableField("config_type")
private String configType;
/**
* 创建者
*/
@TableField("create_by")
private String createBy;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 更新者
*/
@TableField("update_by")
private String updateBy;
/**
* 更新时间
*/
@TableField("update_time")
private Date updateTime;
/**
* 备注
*/
@TableField("remark")
private String remark;
}
package com.jomalls.custom.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 系统访问记录表 Entity
*
* @author Lizh
*/
@Data
@TableName("sys_logininfor")
public class SysLogininforEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 访问ID
*/
@TableId(value = "info_id", type = IdType.AUTO)
private Long infoId;
/**
* 用户账号
*/
@TableField("user_name")
private String userName;
/**
* 登录IP地址
*/
@TableField("ipaddr")
private String ipaddr;
/**
* 登录地点
*/
@TableField("login_location")
private String loginLocation;
/**
* 浏览器类型
*/
@TableField("browser")
private String browser;
/**
* 操作系统
*/
@TableField("os")
private String os;
/**
* 登录状态(0成功 1失败)
*/
@TableField("status")
private String status;
/**
* 提示消息
*/
@TableField("msg")
private String msg;
/**
* 登录时间
*/
@TableField("login_time")
private Date loginTime;
}
package com.jomalls.custom.dal.mapper;
import com.jomalls.custom.dal.entity.SysConfigEntity;
import com.jomalls.custom.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 参数配置 Mapper
*
* @author Lizh
*/
@Mapper
public interface SysConfigMapper extends BaseMapper<SysConfigEntity> {
/**
* 根据配置键查询配置值
*
* @param configKey 配置键
* @return 配置值
*/
String selectConfigByKey(@Param("configKey") String configKey);
}
package com.jomalls.custom.dal.mapper;
import com.jomalls.custom.dal.entity.SysLogininforEntity;
import com.jomalls.custom.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 系统访问记录 Mapper
*
* @author Lizh
*/
@Mapper
public interface SysLogininforMapper extends BaseMapper<SysLogininforEntity> {
}
package com.jomalls.custom.domain.service.system;
import com.jomalls.custom.dal.entity.SysConfigEntity;
import com.jomalls.custom.service.IBaseService;
/**
* 参数配置 接口
*
* @author Lizh
*/
public interface SysConfigDomainService extends IBaseService<SysConfigEntity> {
/**
* 根据配置键查询配置值
*
* @param configKey 配置键
* @return 配置值
*/
String selectConfigByKey(String configKey);
}
package com.jomalls.custom.domain.service.system;
import com.jomalls.custom.dal.entity.SysLogininforEntity;
import com.jomalls.custom.service.IBaseService;
/**
* 系统访问记录 接口
*
* @author Lizh
*/
public interface SysLogininforDomainService extends IBaseService<SysLogininforEntity> {
}
package com.jomalls.custom.domain.service.system.impl;
import com.jomalls.custom.dal.entity.SysConfigEntity;
import com.jomalls.custom.dal.mapper.SysConfigMapper;
import com.jomalls.custom.domain.service.system.SysConfigDomainService;
import com.jomalls.custom.service.impl.BaseServiceImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 参数配置 接口实现
*
* @author Lizh
*/
@Service
public class SysConfigDomainServiceImpl extends BaseServiceImpl<SysConfigMapper, SysConfigEntity> implements SysConfigDomainService {
@Autowired
public SysConfigDomainServiceImpl(SqlSessionFactory sqlSessionFactory) {
super(sqlSessionFactory);
}
@Override
public String selectConfigByKey(String configKey) {
return baseMapper.selectConfigByKey(configKey);
}
}
package com.jomalls.custom.domain.service.system.impl;
import com.jomalls.custom.dal.entity.SysLogininforEntity;
import com.jomalls.custom.dal.mapper.SysLogininforMapper;
import com.jomalls.custom.domain.service.system.SysLogininforDomainService;
import com.jomalls.custom.service.impl.BaseServiceImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 系统访问记录 接口实现
*
* @author Lizh
*/
@Service
public class SysLogininforDomainServiceImpl extends BaseServiceImpl<SysLogininforMapper, SysLogininforEntity> implements SysLogininforDomainService {
@Autowired
public SysLogininforDomainServiceImpl(SqlSessionFactory sqlSessionFactory) {
super(sqlSessionFactory);
}
}
<?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.jomalls.custom.dal.mapper.SysConfigMapper">
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="com.jomalls.custom.dal.entity.SysConfigEntity" id="sysConfigMap">
<result property="configId" column="config_id"/>
<result property="configName" column="config_name"/>
<result property="configKey" column="config_key"/>
<result property="configValue" column="config_value"/>
<result property="configType" column="config_type"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
</resultMap>
<sql id="tableColumns">
config_id,
config_name,
config_key,
config_value,
config_type,
create_by,
create_time,
update_by,
update_time,
remark
</sql>
<!-- 根据配置键查询配置值 -->
<select id="selectConfigByKey" resultType="String">
SELECT config_value FROM sys_config WHERE config_key = #{configKey}
</select>
<!-- 批量插入 -->
<insert id="insertBatchSomeColumn">
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.configName}, #{item.configKey}, #{item.configValue}, #{item.configType}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.remark})
</foreach>
</insert>
</mapper>
<?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.jomalls.custom.dal.mapper.SysLogininforMapper">
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="com.jomalls.custom.dal.entity.SysLogininforEntity" id="sysLogininforMap">
<result property="infoId" column="info_id"/>
<result property="userName" column="user_name"/>
<result property="ipaddr" column="ipaddr"/>
<result property="loginLocation" column="login_location"/>
<result property="browser" column="browser"/>
<result property="os" column="os"/>
<result property="status" column="status"/>
<result property="msg" column="msg"/>
<result property="loginTime" column="login_time"/>
</resultMap>
<sql id="tableColumns">
info_id,
user_name,
ipaddr,
login_location,
browser,
os,
status,
msg,
login_time
</sql>
<!-- 批量插入 -->
<insert id="insertBatchSomeColumn">
INSERT INTO sys_logininfor (user_name, ipaddr, login_location, browser, os, status, msg, login_time) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userName}, #{item.ipaddr}, #{item.loginLocation}, #{item.browser}, #{item.os}, #{item.status}, #{item.msg}, #{item.loginTime})
</foreach>
</insert>
</mapper>
...@@ -3,6 +3,7 @@ package com.jomalls.custom; ...@@ -3,6 +3,7 @@ package com.jomalls.custom;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
...@@ -12,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; ...@@ -12,6 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* @date 2026-05-26 18:25:27 * @date 2026-05-26 18:25:27
*/ */
@SpringBootApplication @SpringBootApplication
@EnableAsync
@EnableScheduling @EnableScheduling
public class CustomServerApplication { public class CustomServerApplication {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment