作者 RuoYi

支持登录IP黑名单限制

@@ -9,6 +9,7 @@ user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分 @@ -9,6 +9,7 @@ user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分
9 user.password.delete=对不起,您的账号已被删除 9 user.password.delete=对不起,您的账号已被删除
10 user.blocked=用户已封禁,请联系管理员 10 user.blocked=用户已封禁,请联系管理员
11 role.blocked=角色已封禁,请联系管理员 11 role.blocked=角色已封禁,请联系管理员
  12 +login.blocked=很遗憾,访问IP已被列入系统黑名单
12 user.logout.success=退出成功 13 user.logout.success=退出成功
13 14
14 length.not.valid=长度必须在{min}到{max}个字符之间 15 length.not.valid=长度必须在{min}到{max}个字符之间
  1 +package com.ruoyi.common.exception.user;
  2 +
  3 +/**
  4 + * 黑名单IP异常类
  5 + *
  6 + * @author ruoyi
  7 + */
  8 +public class BlackListException extends UserException
  9 +{
  10 + private static final long serialVersionUID = 1L;
  11 +
  12 + public BlackListException()
  13 + {
  14 + super("login.blocked", null);
  15 + }
  16 +}
  1 +package com.ruoyi.common.exception.user;
  2 +
  3 +/**
  4 + * 用户不存在异常类
  5 + *
  6 + * @author ruoyi
  7 + */
  8 +public class UserNotExistsException extends UserException
  9 +{
  10 + private static final long serialVersionUID = 1L;
  11 +
  12 + public UserNotExistsException()
  13 + {
  14 + super("user.not.exists", null);
  15 + }
  16 +}
@@ -3,6 +3,7 @@ package com.ruoyi.common.utils.ip; @@ -3,6 +3,7 @@ package com.ruoyi.common.utils.ip;
3 import java.net.InetAddress; 3 import java.net.InetAddress;
4 import java.net.UnknownHostException; 4 import java.net.UnknownHostException;
5 import javax.servlet.http.HttpServletRequest; 5 import javax.servlet.http.HttpServletRequest;
  6 +import com.ruoyi.common.utils.ServletUtils;
6 import com.ruoyi.common.utils.StringUtils; 7 import com.ruoyi.common.utils.StringUtils;
7 8
8 /** 9 /**
@@ -12,6 +13,23 @@ import com.ruoyi.common.utils.StringUtils; @@ -12,6 +13,23 @@ import com.ruoyi.common.utils.StringUtils;
12 */ 13 */
13 public class IpUtils 14 public class IpUtils
14 { 15 {
  16 + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
  17 + // 匹配 ip
  18 + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
  19 + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";
  20 + // 匹配网段
  21 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
  22 +
  23 + /**
  24 + * 获取客户端IP
  25 + *
  26 + * @return IP地址
  27 + */
  28 + public static String getIpAddr()
  29 + {
  30 + return getIpAddr(ServletUtils.getRequest());
  31 + }
  32 +
15 /** 33 /**
16 * 获取客户端IP 34 * 获取客户端IP
17 * 35 *
@@ -248,7 +266,7 @@ public class IpUtils @@ -248,7 +266,7 @@ public class IpUtils
248 } 266 }
249 } 267 }
250 } 268 }
251 - return ip; 269 + return StringUtils.substring(ip, 0, 255);
252 } 270 }
253 271
254 /** 272 /**
@@ -261,4 +279,104 @@ public class IpUtils @@ -261,4 +279,104 @@ public class IpUtils
261 { 279 {
262 return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); 280 return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
263 } 281 }
  282 +
  283 + /**
  284 + * 是否为IP
  285 + */
  286 + public static boolean isIP(String ip)
  287 + {
  288 + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
  289 + }
  290 +
  291 + /**
  292 + * 是否为IP,或 *为间隔的通配符地址
  293 + */
  294 + public static boolean isIpWildCard(String ip)
  295 + {
  296 + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
  297 + }
  298 +
  299 + /**
  300 + * 检测参数是否在ip通配符里
  301 + */
  302 + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip)
  303 + {
  304 + String[] s1 = ipWildCard.split("\\.");
  305 + String[] s2 = ip.split("\\.");
  306 + boolean isMatchedSeg = true;
  307 + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++)
  308 + {
  309 + if (!s1[i].equals(s2[i]))
  310 + {
  311 + isMatchedSeg = false;
  312 + break;
  313 + }
  314 + }
  315 + return isMatchedSeg;
  316 + }
  317 +
  318 + /**
  319 + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
  320 + */
  321 + public static boolean isIPSegment(String ipSeg)
  322 + {
  323 + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
  324 + }
  325 +
  326 + /**
  327 + * 判断ip是否在指定网段中
  328 + */
  329 + public static boolean ipIsInNetNoCheck(String iparea, String ip)
  330 + {
  331 + int idx = iparea.indexOf('-');
  332 + String[] sips = iparea.substring(0, idx).split("\\.");
  333 + String[] sipe = iparea.substring(idx + 1).split("\\.");
  334 + String[] sipt = ip.split("\\.");
  335 + long ips = 0L, ipe = 0L, ipt = 0L;
  336 + for (int i = 0; i < 4; ++i)
  337 + {
  338 + ips = ips << 8 | Integer.parseInt(sips[i]);
  339 + ipe = ipe << 8 | Integer.parseInt(sipe[i]);
  340 + ipt = ipt << 8 | Integer.parseInt(sipt[i]);
  341 + }
  342 + if (ips > ipe)
  343 + {
  344 + long t = ips;
  345 + ips = ipe;
  346 + ipe = t;
  347 + }
  348 + return ips <= ipt && ipt <= ipe;
  349 + }
  350 +
  351 + /**
  352 + * 校验ip是否符合过滤串规则
  353 + *
  354 + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
  355 + * @param ip 校验IP地址
  356 + * @return boolean 结果
  357 + */
  358 + public static boolean isMatchedIp(String filter, String ip)
  359 + {
  360 + if (StringUtils.isEmpty(filter) && StringUtils.isEmpty(ip))
  361 + {
  362 + return false;
  363 + }
  364 + String[] ips = filter.split(";");
  365 + for (String iStr : ips)
  366 + {
  367 + if (isIP(iStr) && iStr.equals(ip))
  368 + {
  369 + return true;
  370 + }
  371 + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip))
  372 + {
  373 + return true;
  374 + }
  375 + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip))
  376 + {
  377 + return true;
  378 + }
  379 + }
  380 + return false;
  381 + }
264 } 382 }
@@ -90,7 +90,7 @@ public class LogAspect @@ -90,7 +90,7 @@ public class LogAspect
90 SysOperLog operLog = new SysOperLog(); 90 SysOperLog operLog = new SysOperLog();
91 operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); 91 operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
92 // 请求的地址 92 // 请求的地址
93 - String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); 93 + String ip = IpUtils.getIpAddr();
94 operLog.setOperIp(ip); 94 operLog.setOperIp(ip);
95 operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); 95 operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
96 if (loginUser != null) 96 if (loginUser != null)
@@ -16,7 +16,6 @@ import org.springframework.stereotype.Component; @@ -16,7 +16,6 @@ import org.springframework.stereotype.Component;
16 import com.ruoyi.common.annotation.RateLimiter; 16 import com.ruoyi.common.annotation.RateLimiter;
17 import com.ruoyi.common.enums.LimitType; 17 import com.ruoyi.common.enums.LimitType;
18 import com.ruoyi.common.exception.ServiceException; 18 import com.ruoyi.common.exception.ServiceException;
19 -import com.ruoyi.common.utils.ServletUtils;  
20 import com.ruoyi.common.utils.StringUtils; 19 import com.ruoyi.common.utils.StringUtils;
21 import com.ruoyi.common.utils.ip.IpUtils; 20 import com.ruoyi.common.utils.ip.IpUtils;
22 21
@@ -79,7 +78,7 @@ public class RateLimiterAspect @@ -79,7 +78,7 @@ public class RateLimiterAspect
79 StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); 78 StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
80 if (rateLimiter.limitType() == LimitType.IP) 79 if (rateLimiter.limitType() == LimitType.IP)
81 { 80 {
82 - stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-"); 81 + stringBuffer.append(IpUtils.getIpAddr()).append("-");
83 } 82 }
84 MethodSignature signature = (MethodSignature) point.getSignature(); 83 MethodSignature signature = (MethodSignature) point.getSignature();
85 Method method = signature.getMethod(); 84 Method method = signature.getMethod();
@@ -38,7 +38,7 @@ public class AsyncFactory @@ -38,7 +38,7 @@ public class AsyncFactory
38 final Object... args) 38 final Object... args)
39 { 39 {
40 final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); 40 final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
41 - final String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); 41 + final String ip = IpUtils.getIpAddr();
42 return new TimerTask() 42 return new TimerTask()
43 { 43 {
44 @Override 44 @Override
@@ -9,16 +9,18 @@ import org.springframework.security.core.Authentication; @@ -9,16 +9,18 @@ import org.springframework.security.core.Authentication;
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 import com.ruoyi.common.constant.CacheConstants; 10 import com.ruoyi.common.constant.CacheConstants;
11 import com.ruoyi.common.constant.Constants; 11 import com.ruoyi.common.constant.Constants;
  12 +import com.ruoyi.common.constant.UserConstants;
12 import com.ruoyi.common.core.domain.entity.SysUser; 13 import com.ruoyi.common.core.domain.entity.SysUser;
13 import com.ruoyi.common.core.domain.model.LoginUser; 14 import com.ruoyi.common.core.domain.model.LoginUser;
14 import com.ruoyi.common.core.redis.RedisCache; 15 import com.ruoyi.common.core.redis.RedisCache;
15 import com.ruoyi.common.exception.ServiceException; 16 import com.ruoyi.common.exception.ServiceException;
  17 +import com.ruoyi.common.exception.user.BlackListException;
16 import com.ruoyi.common.exception.user.CaptchaException; 18 import com.ruoyi.common.exception.user.CaptchaException;
17 import com.ruoyi.common.exception.user.CaptchaExpireException; 19 import com.ruoyi.common.exception.user.CaptchaExpireException;
  20 +import com.ruoyi.common.exception.user.UserNotExistsException;
18 import com.ruoyi.common.exception.user.UserPasswordNotMatchException; 21 import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
19 import com.ruoyi.common.utils.DateUtils; 22 import com.ruoyi.common.utils.DateUtils;
20 import com.ruoyi.common.utils.MessageUtils; 23 import com.ruoyi.common.utils.MessageUtils;
21 -import com.ruoyi.common.utils.ServletUtils;  
22 import com.ruoyi.common.utils.StringUtils; 24 import com.ruoyi.common.utils.StringUtils;
23 import com.ruoyi.common.utils.ip.IpUtils; 25 import com.ruoyi.common.utils.ip.IpUtils;
24 import com.ruoyi.framework.manager.AsyncManager; 26 import com.ruoyi.framework.manager.AsyncManager;
@@ -61,12 +63,10 @@ public class SysLoginService @@ -61,12 +63,10 @@ public class SysLoginService
61 */ 63 */
62 public String login(String username, String password, String code, String uuid) 64 public String login(String username, String password, String code, String uuid)
63 { 65 {
64 - boolean captchaEnabled = configService.selectCaptchaEnabled();  
65 - // 验证码开关  
66 - if (captchaEnabled)  
67 - {  
68 - validateCaptcha(username, code, uuid);  
69 - } 66 + // 验证码校验
  67 + validateCaptcha(username, code, uuid);
  68 + // 登录前置校验
  69 + loginPreCheck(username, password);
70 // 用户验证 70 // 用户验证
71 Authentication authentication = null; 71 Authentication authentication = null;
72 try 72 try
@@ -110,18 +110,58 @@ public class SysLoginService @@ -110,18 +110,58 @@ public class SysLoginService
110 */ 110 */
111 public void validateCaptcha(String username, String code, String uuid) 111 public void validateCaptcha(String username, String code, String uuid)
112 { 112 {
113 - String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");  
114 - String captcha = redisCache.getCacheObject(verifyKey);  
115 - redisCache.deleteObject(verifyKey);  
116 - if (captcha == null) 113 + boolean captchaEnabled = configService.selectCaptchaEnabled();
  114 + if (captchaEnabled)
  115 + {
  116 + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
  117 + String captcha = redisCache.getCacheObject(verifyKey);
  118 + redisCache.deleteObject(verifyKey);
  119 + if (captcha == null)
  120 + {
  121 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
  122 + throw new CaptchaExpireException();
  123 + }
  124 + if (!code.equalsIgnoreCase(captcha))
  125 + {
  126 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
  127 + throw new CaptchaException();
  128 + }
  129 + }
  130 + }
  131 +
  132 + /**
  133 + * 登录前置校验
  134 + * @param username 用户名
  135 + * @param password 用户密码
  136 + */
  137 + public void loginPreCheck(String username, String password)
  138 + {
  139 + // 用户名或密码为空 错误
  140 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
  141 + {
  142 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
  143 + throw new UserNotExistsException();
  144 + }
  145 + // 密码如果不在指定范围内 错误
  146 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
  147 + || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
  148 + {
  149 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
  150 + throw new UserPasswordNotMatchException();
  151 + }
  152 + // 用户名不在指定范围内 错误
  153 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH
  154 + || username.length() > UserConstants.USERNAME_MAX_LENGTH)
117 { 155 {
118 - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));  
119 - throw new CaptchaExpireException(); 156 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
  157 + throw new UserPasswordNotMatchException();
120 } 158 }
121 - if (!code.equalsIgnoreCase(captcha)) 159 + // IP黑名单校验
  160 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
  161 + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
122 { 162 {
123 - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));  
124 - throw new CaptchaException(); 163 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
  164 + throw new BlackListException();
125 } 165 }
126 } 166 }
127 167
@@ -134,7 +174,7 @@ public class SysLoginService @@ -134,7 +174,7 @@ public class SysLoginService
134 { 174 {
135 SysUser sysUser = new SysUser(); 175 SysUser sysUser = new SysUser();
136 sysUser.setUserId(userId); 176 sysUser.setUserId(userId);
137 - sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); 177 + sysUser.setLoginIp(IpUtils.getIpAddr());
138 sysUser.setLoginDate(DateUtils.getNowDate()); 178 sysUser.setLoginDate(DateUtils.getNowDate());
139 userService.updateUserProfile(sysUser); 179 userService.updateUserProfile(sysUser);
140 } 180 }
@@ -156,7 +156,7 @@ public class TokenService @@ -156,7 +156,7 @@ public class TokenService
156 public void setUserAgent(LoginUser loginUser) 156 public void setUserAgent(LoginUser loginUser)
157 { 157 {
158 UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); 158 UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
159 - String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); 159 + String ip = IpUtils.getIpAddr();
160 loginUser.setIpaddr(ip); 160 loginUser.setIpaddr(ip);
161 loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); 161 loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
162 loginUser.setBrowser(userAgent.getBrowser().getName()); 162 loginUser.setBrowser(userAgent.getBrowser().getName());
@@ -110,7 +110,7 @@ @@ -110,7 +110,7 @@
110 <dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/> 110 <dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
111 </template> 111 </template>
112 </el-table-column> 112 </el-table-column>
113 - <el-table-column label="操作信息" align="center" prop="msg" /> 113 + <el-table-column label="操作信息" align="center" prop="msg" :show-overflow-tooltip="true" />
114 <el-table-column label="登录日期" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180"> 114 <el-table-column label="登录日期" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
115 <template slot-scope="scope"> 115 <template slot-scope="scope">
116 <span>{{ parseTime(scope.row.loginTime) }}</span> 116 <span>{{ parseTime(scope.row.loginTime) }}</span>
@@ -545,6 +545,7 @@ insert into sys_config values(2, '用户管理-账号初始密码', 'sys @@ -545,6 +545,7 @@ insert into sys_config values(2, '用户管理-账号初始密码', 'sys
545 insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' ); 545 insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' );
546 insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)'); 546 insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)');
547 insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)'); 547 insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)');
  548 +insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
548 549
549 550
550 -- ---------------------------- 551 -- ----------------------------