支持配置密码最大错误次数/锁定时间
This commit is contained in:
		
							parent
							
								
									250c5ba226
								
							
						
					
					
						commit
						aee5d417ed
					
				@ -41,6 +41,7 @@ public class CacheController
 | 
			
		||||
        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
 | 
			
		||||
        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
 | 
			
		||||
        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
 | 
			
		||||
        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,14 @@ logging:
 | 
			
		||||
    com.ruoyi: debug
 | 
			
		||||
    org.springframework: warn
 | 
			
		||||
 | 
			
		||||
# 用户配置
 | 
			
		||||
user:
 | 
			
		||||
  password:
 | 
			
		||||
    # 密码最大错误次数
 | 
			
		||||
    maxRetryCount: 5
 | 
			
		||||
    # 密码锁定时间(默认10分钟)
 | 
			
		||||
    lockTime: 10
 | 
			
		||||
 | 
			
		||||
# Spring配置
 | 
			
		||||
spring:
 | 
			
		||||
  # 资源信息
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效
 | 
			
		||||
user.not.exists=用户不存在/密码错误
 | 
			
		||||
user.password.not.match=用户不存在/密码错误
 | 
			
		||||
user.password.retry.limit.count=密码输入错误{0}次
 | 
			
		||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
 | 
			
		||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
 | 
			
		||||
user.password.delete=对不起,您的账号已被删除
 | 
			
		||||
user.blocked=用户已封禁,请联系管理员
 | 
			
		||||
role.blocked=角色已封禁,请联系管理员
 | 
			
		||||
 | 
			
		||||
@ -36,4 +36,9 @@ public class CacheConstants
 | 
			
		||||
     * 限流 redis key
 | 
			
		||||
     */
 | 
			
		||||
    public static final String RATE_LIMIT_KEY = "rate_limit:";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 登录账户密码错误次数 redis key
 | 
			
		||||
     */
 | 
			
		||||
    public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -74,6 +74,28 @@ public class RedisCache
 | 
			
		||||
        return redisTemplate.expire(key, timeout, unit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取有效时间
 | 
			
		||||
     *
 | 
			
		||||
     * @param key Redis键
 | 
			
		||||
     * @return 有效时间
 | 
			
		||||
     */
 | 
			
		||||
    public long getExpire(final String key)
 | 
			
		||||
    {
 | 
			
		||||
        return redisTemplate.getExpire(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断 key是否存在
 | 
			
		||||
     *
 | 
			
		||||
     * @param key 键
 | 
			
		||||
     * @return true 存在 false不存在
 | 
			
		||||
     */
 | 
			
		||||
    public Boolean hasKey(String key)
 | 
			
		||||
    {
 | 
			
		||||
        return redisTemplate.hasKey(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获得缓存的基本对象。
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
package com.ruoyi.common.exception.user;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 用户错误最大次数异常类
 | 
			
		||||
 * 
 | 
			
		||||
 * @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 });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
package com.ruoyi.framework.security.context;
 | 
			
		||||
 | 
			
		||||
import org.springframework.security.core.Authentication;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 身份验证信息
 | 
			
		||||
 * 
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
public class AuthenticationContextHolder
 | 
			
		||||
{
 | 
			
		||||
    private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
 | 
			
		||||
 | 
			
		||||
    public static Authentication getContext()
 | 
			
		||||
    {
 | 
			
		||||
        return contextHolder.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setContext(Authentication context)
 | 
			
		||||
    {
 | 
			
		||||
        contextHolder.set(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void clearContext()
 | 
			
		||||
    {
 | 
			
		||||
        contextHolder.remove();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -23,6 +23,7 @@ import com.ruoyi.common.utils.StringUtils;
 | 
			
		||||
import com.ruoyi.common.utils.ip.IpUtils;
 | 
			
		||||
import com.ruoyi.framework.manager.AsyncManager;
 | 
			
		||||
import com.ruoyi.framework.manager.factory.AsyncFactory;
 | 
			
		||||
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
 | 
			
		||||
import com.ruoyi.system.service.ISysConfigService;
 | 
			
		||||
import com.ruoyi.system.service.ISysUserService;
 | 
			
		||||
 | 
			
		||||
@ -70,9 +71,10 @@ public class SysLoginService
 | 
			
		||||
        Authentication authentication = null;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
 | 
			
		||||
            AuthenticationContextHolder.setContext(authenticationToken);
 | 
			
		||||
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
 | 
			
		||||
            authentication = authenticationManager
 | 
			
		||||
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
 | 
			
		||||
            authentication = authenticationManager.authenticate(authenticationToken);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception e)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
package com.ruoyi.framework.web.service;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.security.core.Authentication;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import com.ruoyi.common.constant.CacheConstants;
 | 
			
		||||
import com.ruoyi.common.constant.Constants;
 | 
			
		||||
import com.ruoyi.common.core.domain.entity.SysUser;
 | 
			
		||||
import com.ruoyi.common.core.redis.RedisCache;
 | 
			
		||||
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
 | 
			
		||||
import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
 | 
			
		||||
import com.ruoyi.common.utils.MessageUtils;
 | 
			
		||||
import com.ruoyi.common.utils.SecurityUtils;
 | 
			
		||||
import com.ruoyi.framework.manager.AsyncManager;
 | 
			
		||||
import com.ruoyi.framework.manager.factory.AsyncFactory;
 | 
			
		||||
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 登录密码方法
 | 
			
		||||
 * 
 | 
			
		||||
 * @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();
 | 
			
		||||
 | 
			
		||||
        Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
 | 
			
		||||
 | 
			
		||||
        if (retryCount == null)
 | 
			
		||||
        {
 | 
			
		||||
            retryCount = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
        }
 | 
			
		||||
        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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -27,6 +27,9 @@ public class UserDetailsServiceImpl implements UserDetailsService
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private ISysUserService userService;
 | 
			
		||||
    
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private SysPasswordService passwordService;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private SysPermissionService permissionService;
 | 
			
		||||
 | 
			
		||||
@ -50,6 +53,8 @@ public class UserDetailsServiceImpl implements UserDetailsService
 | 
			
		||||
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        passwordService.validate(user);
 | 
			
		||||
 | 
			
		||||
        return createLoginUser(user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user