RuoYi
2023-02-21 3ef9c108c9e90a01aee36b708983dcb42175c429
支持登录IP黑名单限制
已修改7个文件
已重命名1个文件
已添加2个文件
239 ■■■■ 文件已修改
sql/ry_20230221.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/exception/user/BlackListException.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/ip/IpUtils.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/aspectj/LogAspect.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/security/service/SysLoginService.java 76 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/security/service/TokenService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/i18n/messages.properties 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/ry_20230221.sql
ÎļþÃû´Ó sql/ry_20230216.sql ÐÞ¸Ä
@@ -545,6 +545,7 @@
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黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
-- ----------------------------
src/main/java/com/ruoyi/common/exception/user/BlackListException.java
¶Ô±ÈÐÂÎļþ
@@ -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);
    }
}
src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java
¶Ô±ÈÐÂÎļþ
@@ -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);
    }
}
src/main/java/com/ruoyi/common/utils/ip/IpUtils.java
@@ -3,6 +3,7 @@
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 @@
 */
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 @@
                }
            }
        }
        return ip;
        return StringUtils.substring(ip, 0, 255);
    }
    /**
@@ -261,4 +279,104 @@
    {
        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;
    }
}
src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
@@ -90,7 +90,7 @@
            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)
src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
@@ -14,7 +14,6 @@
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
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;
import com.ruoyi.framework.aspectj.lang.annotation.RateLimiter;
@@ -79,7 +78,7 @@
        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();
src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
@@ -38,7 +38,7 @@
            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
src/main/java/com/ruoyi/framework/security/service/SysLoginService.java
@@ -9,13 +9,15 @@
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.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;
@@ -46,7 +48,7 @@
    @Autowired
    private ISysUserService userService;
    @Autowired
    private ISysConfigService configService;
@@ -61,12 +63,10 @@
     */
    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 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 @@
    {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
        sysUser.setLoginIp(IpUtils.getIpAddr());
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }
src/main/java/com/ruoyi/framework/security/service/TokenService.java
@@ -156,7 +156,7 @@
    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());
src/main/resources/i18n/messages.properties
@@ -9,6 +9,7 @@
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
login.blocked=很遗憾,访问IP已被列入系统黑名单
user.logout.success=退出成功
length.not.valid=长度必须在{min}到{max}个字符之间