支持登录IP黑名单限制
This commit is contained in:
		
							parent
							
								
									61caa7966b
								
							
						
					
					
						commit
						1268637e58
					
				@ -9,6 +9,7 @@ user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分
 | 
			
		||||
user.password.delete=对不起,您的账号已被删除
 | 
			
		||||
user.blocked=用户已封禁,请联系管理员
 | 
			
		||||
role.blocked=角色已封禁,请联系管理员
 | 
			
		||||
login.blocked=很遗憾,访问IP已被列入系统黑名单
 | 
			
		||||
user.logout.success=退出成功
 | 
			
		||||
 | 
			
		||||
length.not.valid=长度必须在{min}到{max}个字符之间
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
package com.ruoyi.common.exception.user;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 黑名单IP异常类
 | 
			
		||||
 * 
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
public class BlackListException extends UserException
 | 
			
		||||
{
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
    public BlackListException()
 | 
			
		||||
    {
 | 
			
		||||
        super("login.blocked", null);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
package com.ruoyi.common.exception.user;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 用户不存在异常类
 | 
			
		||||
 * 
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
public class UserNotExistsException extends UserException
 | 
			
		||||
{
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
    public UserNotExistsException()
 | 
			
		||||
    {
 | 
			
		||||
        super("user.not.exists", null);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ package com.ruoyi.common.utils.ip;
 | 
			
		||||
import java.net.InetAddress;
 | 
			
		||||
import java.net.UnknownHostException;
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import com.ruoyi.common.utils.ServletUtils;
 | 
			
		||||
import com.ruoyi.common.utils.StringUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -12,6 +13,23 @@ import com.ruoyi.common.utils.StringUtils;
 | 
			
		||||
 */
 | 
			
		||||
public class IpUtils
 | 
			
		||||
{
 | 
			
		||||
    public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
 | 
			
		||||
    // 匹配 ip
 | 
			
		||||
    public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
 | 
			
		||||
    public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";
 | 
			
		||||
    // 匹配网段
 | 
			
		||||
    public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取客户端IP
 | 
			
		||||
     * 
 | 
			
		||||
     * @return IP地址
 | 
			
		||||
     */
 | 
			
		||||
    public static String getIpAddr()
 | 
			
		||||
    {
 | 
			
		||||
        return getIpAddr(ServletUtils.getRequest());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取客户端IP
 | 
			
		||||
     * 
 | 
			
		||||
@ -248,7 +266,7 @@ public class IpUtils
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return ip;
 | 
			
		||||
        return StringUtils.substring(ip, 0, 255);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -261,4 +279,104 @@ public class IpUtils
 | 
			
		||||
    {
 | 
			
		||||
        return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否为IP
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isIP(String ip)
 | 
			
		||||
    {
 | 
			
		||||
        return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否为IP,或 *为间隔的通配符地址
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isIpWildCard(String ip)
 | 
			
		||||
    {
 | 
			
		||||
        return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 检测参数是否在ip通配符里
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip)
 | 
			
		||||
    {
 | 
			
		||||
        String[] s1 = ipWildCard.split("\\.");
 | 
			
		||||
        String[] s2 = ip.split("\\.");
 | 
			
		||||
        boolean isMatchedSeg = true;
 | 
			
		||||
        for (int i = 0; i < s1.length && !s1[i].equals("*"); i++)
 | 
			
		||||
        {
 | 
			
		||||
            if (!s1[i].equals(s2[i]))
 | 
			
		||||
            {
 | 
			
		||||
                isMatchedSeg = false;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return isMatchedSeg;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isIPSegment(String ipSeg)
 | 
			
		||||
    {
 | 
			
		||||
        return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断ip是否在指定网段中
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean ipIsInNetNoCheck(String iparea, String ip)
 | 
			
		||||
    {
 | 
			
		||||
        int idx = iparea.indexOf('-');
 | 
			
		||||
        String[] sips = iparea.substring(0, idx).split("\\.");
 | 
			
		||||
        String[] sipe = iparea.substring(idx + 1).split("\\.");
 | 
			
		||||
        String[] sipt = ip.split("\\.");
 | 
			
		||||
        long ips = 0L, ipe = 0L, ipt = 0L;
 | 
			
		||||
        for (int i = 0; i < 4; ++i)
 | 
			
		||||
        {
 | 
			
		||||
            ips = ips << 8 | Integer.parseInt(sips[i]);
 | 
			
		||||
            ipe = ipe << 8 | Integer.parseInt(sipe[i]);
 | 
			
		||||
            ipt = ipt << 8 | Integer.parseInt(sipt[i]);
 | 
			
		||||
        }
 | 
			
		||||
        if (ips > ipe)
 | 
			
		||||
        {
 | 
			
		||||
            long t = ips;
 | 
			
		||||
            ips = ipe;
 | 
			
		||||
            ipe = t;
 | 
			
		||||
        }
 | 
			
		||||
        return ips <= ipt && ipt <= ipe;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验ip是否符合过滤串规则
 | 
			
		||||
     * 
 | 
			
		||||
     * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
 | 
			
		||||
     * @param ip 校验IP地址
 | 
			
		||||
     * @return boolean 结果
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isMatchedIp(String filter, String ip)
 | 
			
		||||
    {
 | 
			
		||||
        if (StringUtils.isEmpty(filter) && StringUtils.isEmpty(ip))
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        String[] ips = filter.split(";");
 | 
			
		||||
        for (String iStr : ips)
 | 
			
		||||
        {
 | 
			
		||||
            if (isIP(iStr) && iStr.equals(ip))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -90,7 +90,7 @@ public class LogAspect
 | 
			
		||||
            SysOperLog operLog = new SysOperLog();
 | 
			
		||||
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
 | 
			
		||||
            // 请求的地址
 | 
			
		||||
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
 | 
			
		||||
            String ip = IpUtils.getIpAddr();
 | 
			
		||||
            operLog.setOperIp(ip);
 | 
			
		||||
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
 | 
			
		||||
            if (loginUser != null)
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import org.springframework.stereotype.Component;
 | 
			
		||||
import com.ruoyi.common.annotation.RateLimiter;
 | 
			
		||||
import com.ruoyi.common.enums.LimitType;
 | 
			
		||||
import com.ruoyi.common.exception.ServiceException;
 | 
			
		||||
import com.ruoyi.common.utils.ServletUtils;
 | 
			
		||||
import com.ruoyi.common.utils.StringUtils;
 | 
			
		||||
import com.ruoyi.common.utils.ip.IpUtils;
 | 
			
		||||
 | 
			
		||||
@ -79,7 +78,7 @@ public class RateLimiterAspect
 | 
			
		||||
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
 | 
			
		||||
        if (rateLimiter.limitType() == LimitType.IP)
 | 
			
		||||
        {
 | 
			
		||||
            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-");
 | 
			
		||||
            stringBuffer.append(IpUtils.getIpAddr()).append("-");
 | 
			
		||||
        }
 | 
			
		||||
        MethodSignature signature = (MethodSignature) point.getSignature();
 | 
			
		||||
        Method method = signature.getMethod();
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ public class AsyncFactory
 | 
			
		||||
            final Object... args)
 | 
			
		||||
    {
 | 
			
		||||
        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
 | 
			
		||||
        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
 | 
			
		||||
        final String ip = IpUtils.getIpAddr();
 | 
			
		||||
        return new TimerTask()
 | 
			
		||||
        {
 | 
			
		||||
            @Override
 | 
			
		||||
 | 
			
		||||
@ -9,16 +9,18 @@ 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.constant.UserConstants;
 | 
			
		||||
import com.ruoyi.common.core.domain.entity.SysUser;
 | 
			
		||||
import com.ruoyi.common.core.domain.model.LoginUser;
 | 
			
		||||
import com.ruoyi.common.core.redis.RedisCache;
 | 
			
		||||
import com.ruoyi.common.exception.ServiceException;
 | 
			
		||||
import com.ruoyi.common.exception.user.BlackListException;
 | 
			
		||||
import com.ruoyi.common.exception.user.CaptchaException;
 | 
			
		||||
import com.ruoyi.common.exception.user.CaptchaExpireException;
 | 
			
		||||
import com.ruoyi.common.exception.user.UserNotExistsException;
 | 
			
		||||
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
 | 
			
		||||
import com.ruoyi.common.utils.DateUtils;
 | 
			
		||||
import com.ruoyi.common.utils.MessageUtils;
 | 
			
		||||
import com.ruoyi.common.utils.ServletUtils;
 | 
			
		||||
import com.ruoyi.common.utils.StringUtils;
 | 
			
		||||
import com.ruoyi.common.utils.ip.IpUtils;
 | 
			
		||||
import com.ruoyi.framework.manager.AsyncManager;
 | 
			
		||||
@ -61,12 +63,10 @@ public class SysLoginService
 | 
			
		||||
     */
 | 
			
		||||
    public String login(String username, String password, String code, String uuid)
 | 
			
		||||
    {
 | 
			
		||||
        boolean captchaEnabled = configService.selectCaptchaEnabled();
 | 
			
		||||
        // 验证码开关
 | 
			
		||||
        if (captchaEnabled)
 | 
			
		||||
        {
 | 
			
		||||
            validateCaptcha(username, code, uuid);
 | 
			
		||||
        }
 | 
			
		||||
        // 验证码校验
 | 
			
		||||
        validateCaptcha(username, code, uuid);
 | 
			
		||||
        // 登录前置校验
 | 
			
		||||
        loginPreCheck(username, password);
 | 
			
		||||
        // 用户验证
 | 
			
		||||
        Authentication authentication = null;
 | 
			
		||||
        try
 | 
			
		||||
@ -110,18 +110,58 @@ public class SysLoginService
 | 
			
		||||
     */
 | 
			
		||||
    public void validateCaptcha(String username, String code, String uuid)
 | 
			
		||||
    {
 | 
			
		||||
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
 | 
			
		||||
        String captcha = redisCache.getCacheObject(verifyKey);
 | 
			
		||||
        redisCache.deleteObject(verifyKey);
 | 
			
		||||
        if (captcha == null)
 | 
			
		||||
        boolean captchaEnabled = configService.selectCaptchaEnabled();
 | 
			
		||||
        if (captchaEnabled)
 | 
			
		||||
        {
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
 | 
			
		||||
            throw new CaptchaExpireException();
 | 
			
		||||
            String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
 | 
			
		||||
            String captcha = redisCache.getCacheObject(verifyKey);
 | 
			
		||||
            redisCache.deleteObject(verifyKey);
 | 
			
		||||
            if (captcha == null)
 | 
			
		||||
            {
 | 
			
		||||
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
 | 
			
		||||
                throw new CaptchaExpireException();
 | 
			
		||||
            }
 | 
			
		||||
            if (!code.equalsIgnoreCase(captcha))
 | 
			
		||||
            {
 | 
			
		||||
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
 | 
			
		||||
                throw new CaptchaException();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!code.equalsIgnoreCase(captcha))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 登录前置校验
 | 
			
		||||
     * @param username 用户名
 | 
			
		||||
     * @param password 用户密码
 | 
			
		||||
     */
 | 
			
		||||
    public void loginPreCheck(String username, String password)
 | 
			
		||||
    {
 | 
			
		||||
        // 用户名或密码为空 错误
 | 
			
		||||
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
 | 
			
		||||
        {
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
 | 
			
		||||
            throw new CaptchaException();
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
 | 
			
		||||
            throw new UserNotExistsException();
 | 
			
		||||
        }
 | 
			
		||||
        // 密码如果不在指定范围内 错误
 | 
			
		||||
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
 | 
			
		||||
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
 | 
			
		||||
        {
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
 | 
			
		||||
            throw new UserPasswordNotMatchException();
 | 
			
		||||
        }
 | 
			
		||||
        // 用户名不在指定范围内 错误
 | 
			
		||||
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
 | 
			
		||||
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
 | 
			
		||||
        {
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
 | 
			
		||||
            throw new UserPasswordNotMatchException();
 | 
			
		||||
        }
 | 
			
		||||
        // IP黑名单校验
 | 
			
		||||
        String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
 | 
			
		||||
        if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
 | 
			
		||||
        {
 | 
			
		||||
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
 | 
			
		||||
            throw new BlackListException();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -134,7 +174,7 @@ public class SysLoginService
 | 
			
		||||
    {
 | 
			
		||||
        SysUser sysUser = new SysUser();
 | 
			
		||||
        sysUser.setUserId(userId);
 | 
			
		||||
        sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
 | 
			
		||||
        sysUser.setLoginIp(IpUtils.getIpAddr());
 | 
			
		||||
        sysUser.setLoginDate(DateUtils.getNowDate());
 | 
			
		||||
        userService.updateUserProfile(sysUser);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -156,7 +156,7 @@ public class TokenService
 | 
			
		||||
    public void setUserAgent(LoginUser loginUser)
 | 
			
		||||
    {
 | 
			
		||||
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
 | 
			
		||||
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
 | 
			
		||||
        String ip = IpUtils.getIpAddr();
 | 
			
		||||
        loginUser.setIpaddr(ip);
 | 
			
		||||
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
 | 
			
		||||
        loginUser.setBrowser(userAgent.getBrowser().getName());
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,7 @@
 | 
			
		||||
          <dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
      <el-table-column label="操作信息" align="center" prop="msg" />
 | 
			
		||||
      <el-table-column label="操作信息" align="center" prop="msg" :show-overflow-tooltip="true" />
 | 
			
		||||
      <el-table-column label="登录日期" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
 | 
			
		||||
        <template slot-scope="scope">
 | 
			
		||||
          <span>{{ parseTime(scope.row.loginTime) }}</span>
 | 
			
		||||
 | 
			
		||||
@ -545,6 +545,7 @@ insert into sys_config values(2, '用户管理-账号初始密码',         'sys
 | 
			
		||||
insert into sys_config values(3, '主框架页-侧边栏主题',           'sys.index.sideTheme',           'theme-dark',    'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' );
 | 
			
		||||
insert into sys_config values(4, '账号自助-验证码开关',           'sys.account.captchaEnabled',    'true',          'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)');
 | 
			
		||||
insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser',      'false',         'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)');
 | 
			
		||||
insert into sys_config values(6, '用户登录-黑名单列表',           'sys.login.blackIPList',         '',              'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
-- ----------------------------
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user