【PowerJob语雀转载】 最佳实践-如何实现账号自动解锁功能?

49 阅读2分钟

需求说明

某公司存在多个系统,走同一套登录逻辑,现有如下需求:指定系统 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");
    }
}