需求说明
某公司存在多个系统,走同一套登录逻辑,现有如下需求:指定系统 X,账号登录密码在一天内连续错误 N 次后,自动触发锁定账号,并于 Y 分钟后可以自动解锁。(N 和 Y 的值需要可配,且不同系统允许不一致,系统运维人员可以在页面配置)
需求分析
- 系统的自动锁定和解锁能力可配置(按 A 配 N 和 Y)
- 登录密码错误时,判断是否满足锁定的条件,如满足错误次数 > N,则将账号状态改为锁定,并添加一个异步的自动解锁任务,时间差为 Y 分钟。
功能实现
业务流程
数据模型
系统登录配置表
create table if not exists tab_system_config
(
`id` bigint unsigned auto_increment primary key not null comment '主键',
`system_code` varchar(20) unique not null comment '系统代码',
`lock_threshold` int default 3 not null comment '密码连续错误触发自动锁定账号次数',
`auto_unlock_time` int default 30 not null comment '账号自动解锁时间,单位:分钟',
`create_time` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
`update_time` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='系统登录配置表';
伪代码
- PowerJob Client bean注册
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.powerjob.client.PowerJobClient;
/**
* @author Jjn
* @since 2024-04-27 22:43
*/
@Configuration
public class PowerJobClientConfig {
@Value("${powerjob.worker.server-address}")
private String serverAddress;
@Value("${powerjob.client.password}")
private String password;
@Value("${powerjob.worker.app-name}")
private String appName;
@Bean
public PowerJobClient initPowerJobClient() {
String[] configuredServers = StringUtils.split(serverAddress, ",");
return new PowerJobClient(Lists.newArrayList(configuredServers), appName, password);
}
}
- 登录业务类
package com.github.jjnnzb.demo.login.service.impl;
import com.github.jjnnzb.demo.login.entity.bo.LoginParam;
import com.github.jjnnzb.demo.login.entity.po.SystemConfigPO;
import com.github.jjnnzb.demo.login.entity.po.UserPO;
import com.github.jjnnzb.demo.login.service.LoginService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.powerjob.client.PowerJobClient;
import tech.powerjob.common.serialize.JsonUtils;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
/**
* @author Jjn
* @since 2024-04-27 23:01
*/
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {
// 到页面上注册后,获取任务对应 id
@Value("${auto.lock.task.id}")
private Long autoUnlockTaskId;
private final StringRedisTemplate stringRedisTemplate;
private final PowerJobClient powerJobClient;
private static final String USER_LOCK_KEY = "user:lock";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public String login(LoginParam loginParam) {
String system = loginParam.getSystem();
UserPO currentUser = queryUser(loginParam.getUsername());
String password = loginParam.getPassword();
if (!Objects.equals(0, currentUser.getUserState())) {
LocalDateTime unlockTime = currentUser.getExpectedUnlockTime();
StringBuilder stringBuilder = new StringBuilder("账号被锁定");
if (unlockTime != null) {
String formattedTime = DATE_TIME_FORMATTER.format(unlockTime);
stringBuilder.append(",预计解锁时间:");
stringBuilder.append(formattedTime);
}
throw new RuntimeException(stringBuilder.toString());
}
if (!StringUtils.equals(password, currentUser.getPassword())) {
// 查询历史错误次数
Long incremented = stringRedisTemplate.opsForValue().increment(USER_LOCK_KEY + loginParam.getUsername());
SystemConfigPO config = getSystemConfig(system);
if (incremented != null && incremented.intValue() > config.getLockThreshold()) {
// 修改用户状态到锁定并添加异步自动解锁任务
// userDao.updateById(...);
powerJobClient.runJob(autoUnlockTaskId, JsonUtils.toJSONString(loginParam), config.getAutoUnlockTime() * 60_000);
}
throw new RuntimeException(MessageFormat.format("密码错误{0}次", incremented));
}
return grantToken(loginParam);
}
private String grantToken(LoginParam loginParam) {
return "";
}
private SystemConfigPO getSystemConfig(String system) {
// 查询系统配置
// select lock_threshold, auto_unlock_time from tab_system_config where system_code = #{system}
SystemConfigPO config = new SystemConfigPO();
config.setLockThreshold(5);
config.setAutoUnlockTime(30);
return config;
}
private UserPO queryUser(String username) {
UserPO user = new UserPO();
user.setUsername(username);
user.setPassword("");
user.setUserState(1);
user.setExpectedUnlockTime(LocalDateTime.now().plusMinutes(10));
return user;
}
}
- 自动解锁任务类
package com.github.jjnnzb.demo.login.task;
import com.github.jjnnzb.demo.login.entity.bo.LoginParam;
import org.springframework.stereotype.Component;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.worker.core.processor.ProcessResult;
import tech.powerjob.worker.core.processor.TaskContext;
import tech.powerjob.worker.core.processor.sdk.BasicProcessor;
import tech.powerjob.worker.log.OmsLogger;
/**
* @author Jjn
* @since 2024-04-27 23:39
*/
@Component
public class AutoUnlockTask implements BasicProcessor {
@Override
public ProcessResult process(TaskContext taskContext) throws Exception {
String instanceParams = taskContext.getInstanceParams();
OmsLogger omsLogger = taskContext.getOmsLogger();
LoginParam loginParam = JsonUtils.parseObject(instanceParams, LoginParam.class);
if (loginParam == null) {
omsLogger.warn("instance params is null");
return new ProcessResult(false, "instance params is null");
}
String username = loginParam.getUsername();
// update t_user set user_state = 0, expected_unlock_time = null where username = #{username}
return new ProcessResult(true, "unlock success");
}
}