redis 不支持 getAndExpire_开源

我们这里要实现的功能是登录时添加账号登录错误时最大错误次数和锁定时间,功能不复杂,这次提交里面我们主要来看下一个项目里面一个业务功能怎样写更加优雅

核心实现

我们先来看核心实现的思路

首先是 login 方法重写,进入 loadUserByUsername() 之前设置 AuthenticationContextHolder,方便后面方法使用登录信息

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);

loadUserByUsername(),这里的鉴权的地方,在返回用户之前添加密码的校验

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
    SysUser user = userService.selectUserByUserName(username);
    // ... 一些校验

    passwordService.validate(user);

    return createLoginUser(user);
}

SysPasswordService 这里定义了 validate 方法,实现也不复杂,基于 redis 做的

/**
 * 登录密码方法
 * 
 * @author ruoyi
 */
@Component
public class SysPasswordService
{
    @Autowired
    private RedisCache redisCache;

    @Value(value = "${user.password.maxRetryCount}")
    private int maxRetryCount;

    @Value(value = "${user.password.lockTime}")
    private int lockTime;

    /**
     * 登录账户密码错误次数缓存键名
     * 
     * @param username 用户名
     * @return 缓存键key
     */
    private String getCacheKey(String username)
    {
        return CacheConstants.PWD_ERR_CNT_KEY + username;
    }

    public void validate(SysUser user)
    {
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        String username = usernamePasswordAuthenticationToken.getName();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
// 1 根据在登录时设置的 contextHolder 中的信息,拿到 redis 这个账号的错误登录次数
        Integer retryCount = redisCache.getCacheObject(getCacheKey(username));

// 2.1 没找到,说明这个用户没有登录记录,初始化为0
        if (retryCount == null)
        {
            retryCount = 0;
        }
      
// 2.2 如果已经达到配置文件中配置的最大次数阈值,抛出异常
        if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
            throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
        }
      
// 2.4 判断如果账号密码不匹配,缓存记录 +1
        if (!matches(user, password))
        {
            retryCount = retryCount + 1;
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.count", retryCount)));
            redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
            throw new UserPasswordNotMatchException();
        }
// 2.5 正常登录,删除掉异常登录的缓存记录
        else
        {
            clearLoginRecordCache(username);
        }
    }

    public boolean matches(SysUser user, String rawPassword)
    {
        return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
    }

    public void clearLoginRecordCache(String loginName)
    {
        if (redisCache.hasKey(getCacheKey(loginName)))
        {
            redisCache.deleteObject(getCacheKey(loginName));
        }
    }
}

简单看下注解理解下

细节

ContextHolder 使用

下文需要拿到登录相关信息,在登录之前,把登录信息设置到 ContextHolder 方便后面调用

常量类的使用

cacheKey 的创建方法使用 常量类

/**
 * 登录账户密码错误次数缓存键名
 * 
 * @param username 用户名
 * @return 缓存键key
 */
private String getCacheKey(String username)
{
    return CacheConstants.PWD_ERR_CNT_KEY + username;
}

// CacheConstants
/**
 * 登录账户密码错误次数 redis key
 */
public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";

错误记录

继承 UserException 实现自定义异常类

/**
 * 用户错误最大次数异常类
 * 
 * @author ruoyi
 */
public class UserPasswordRetryLimitExceedException extends UserException
{
    private static final long serialVersionUID = 1L;

    public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime)
    {
        super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime });
    }
}

错误信息的异步记录

AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);

接着看AsyncManager,它提供了对于 ScheduledExecutorService 的单例调用

饿汉式

  1. 构造器私有化
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法
/**
 * 异步任务管理器
 * 
 * @author ruoyi
 */
public class AsyncManager
{
    /**
     * 操作延迟10毫秒
     */
    private final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

    /**
     * 单例模式
     */
    private AsyncManager(){}

    private static AsyncManager me = new AsyncManager();

    public static AsyncManager me()
    {
        return me;
    }

    /**
     * 执行任务
     * 
     * @param task 任务
     */
    public void execute(TimerTask task)
    {
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 停止任务线程池
     */
    public void shutdown()
    {
        Threads.shutdownAndAwaitTermination(executor);
    }
}

设计模式-单例模式(五种实现方法详解) - 腾讯云开发者社区-腾讯云 (tencent.com)

AsyncManager 是 ScheduledExecutorService 的一个封装实现

execute() 方法传入 TimerTask


redis 不支持 getAndExpire_缓存_02

AsyncFactory 中提供 TimerTask 工厂创建

看下配置

/**
 * 线程池配置
 *
 * @author ruoyi
 **/
@Configuration
public class ThreadPoolConfig
{
    // 核心线程池大小
    private int corePoolSize = 50;

    // 最大可创建的线程数
    private int maxPoolSize = 200;

    // 队列最大长度
    private int queueCapacity = 1000;

    // 线程池维护线程所允许的空闲时间
    private int keepAliveSeconds = 300;

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
  
  /**
     * 执行周期性或定时任务
     */
    @Bean(name = "scheduledExecutorService")
    protected ScheduledExecutorService scheduledExecutorService()
    {
        return new ScheduledThreadPoolExecutor(corePoolSize,
                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
                new ThreadPoolExecutor.CallerRunsPolicy())
        {
            @Override
            protected void afterExecute(Runnable r, Throwable t)
            {
                super.afterExecute(r, t);
                Threads.printException(r, t);
            }
        };
    }
}

这块实现是 基于 jdk 的定时调度,和 quartz 的定时任务实现是分开,这块主要用于日志的异步记录