支持登录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.password.delete=对不起,您的账号已被删除
|
||||||
user.blocked=用户已封禁,请联系管理员
|
user.blocked=用户已封禁,请联系管理员
|
||||||
role.blocked=角色已封禁,请联系管理员
|
role.blocked=角色已封禁,请联系管理员
|
||||||
|
login.blocked=很遗憾,访问IP已被列入系统黑名单
|
||||||
user.logout.success=退出成功
|
user.logout.success=退出成功
|
||||||
|
|
||||||
length.not.valid=长度必须在{min}到{max}个字符之间
|
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.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import com.ruoyi.common.utils.ServletUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,6 +13,23 @@ import com.ruoyi.common.utils.StringUtils;
|
||||||
*/
|
*/
|
||||||
public class IpUtils
|
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
|
* 获取客户端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);
|
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();
|
SysOperLog operLog = new SysOperLog();
|
||||||
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
|
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
|
||||||
// 请求的地址
|
// 请求的地址
|
||||||
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
String ip = IpUtils.getIpAddr();
|
||||||
operLog.setOperIp(ip);
|
operLog.setOperIp(ip);
|
||||||
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
|
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
|
||||||
if (loginUser != null)
|
if (loginUser != null)
|
||||||
|
|
|
@ -16,7 +16,6 @@ import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.common.annotation.RateLimiter;
|
import com.ruoyi.common.annotation.RateLimiter;
|
||||||
import com.ruoyi.common.enums.LimitType;
|
import com.ruoyi.common.enums.LimitType;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.ServletUtils;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.ip.IpUtils;
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
|
|
||||||
|
@ -79,7 +78,7 @@ public class RateLimiterAspect
|
||||||
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
||||||
if (rateLimiter.limitType() == LimitType.IP)
|
if (rateLimiter.limitType() == LimitType.IP)
|
||||||
{
|
{
|
||||||
stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-");
|
stringBuffer.append(IpUtils.getIpAddr()).append("-");
|
||||||
}
|
}
|
||||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
|
|
|
@ -38,7 +38,7 @@ public class AsyncFactory
|
||||||
final Object... args)
|
final Object... args)
|
||||||
{
|
{
|
||||||
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
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()
|
return new TimerTask()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,16 +9,18 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.common.constant.CacheConstants;
|
import com.ruoyi.common.constant.CacheConstants;
|
||||||
import com.ruoyi.common.constant.Constants;
|
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.entity.SysUser;
|
||||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||||
import com.ruoyi.common.core.redis.RedisCache;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
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.CaptchaException;
|
||||||
import com.ruoyi.common.exception.user.CaptchaExpireException;
|
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.exception.user.UserPasswordNotMatchException;
|
||||||
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
import com.ruoyi.common.utils.MessageUtils;
|
||||||
import com.ruoyi.common.utils.ServletUtils;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.ip.IpUtils;
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
import com.ruoyi.framework.manager.AsyncManager;
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
@ -61,12 +63,10 @@ public class SysLoginService
|
||||||
*/
|
*/
|
||||||
public String login(String username, String password, String code, String uuid)
|
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;
|
Authentication authentication = null;
|
||||||
try
|
try
|
||||||
|
@ -109,6 +109,9 @@ public class SysLoginService
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public void validateCaptcha(String username, String code, String uuid)
|
public void validateCaptcha(String username, String code, String uuid)
|
||||||
|
{
|
||||||
|
boolean captchaEnabled = configService.selectCaptchaEnabled();
|
||||||
|
if (captchaEnabled)
|
||||||
{
|
{
|
||||||
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
|
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
|
||||||
String captcha = redisCache.getCacheObject(verifyKey);
|
String captcha = redisCache.getCacheObject(verifyKey);
|
||||||
|
@ -124,6 +127,43 @@ public class SysLoginService
|
||||||
throw new CaptchaException();
|
throw new CaptchaException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录前置校验
|
||||||
|
* @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("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 sysUser = new SysUser();
|
||||||
sysUser.setUserId(userId);
|
sysUser.setUserId(userId);
|
||||||
sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
|
sysUser.setLoginIp(IpUtils.getIpAddr());
|
||||||
sysUser.setLoginDate(DateUtils.getNowDate());
|
sysUser.setLoginDate(DateUtils.getNowDate());
|
||||||
userService.updateUserProfile(sysUser);
|
userService.updateUserProfile(sysUser);
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ public class TokenService
|
||||||
public void setUserAgent(LoginUser loginUser)
|
public void setUserAgent(LoginUser loginUser)
|
||||||
{
|
{
|
||||||
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||||
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
String ip = IpUtils.getIpAddr();
|
||||||
loginUser.setIpaddr(ip);
|
loginUser.setIpaddr(ip);
|
||||||
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||||
loginUser.setBrowser(userAgent.getBrowser().getName());
|
loginUser.setBrowser(userAgent.getBrowser().getName());
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
<dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
|
<dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="登录日期" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ parseTime(scope.row.loginTime) }}</span>
|
<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(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(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(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