引言
本文记录一个关于 IP 白名单的构想。通过自定义注解启用 IP 限制。可以区分项目进行限制。
实现
定义 IP 策略枚举,对应自定义注解中的 type 属性。
@Getter
@AllArgsConstructor
public enum IpRestrictionsType {
//拒绝
DENY("deny"),
//允许
ALLOW("allow"),
//数据库
DB("db"),
;
public static IpRestrictionsType match(@NonNull String name){
return IpRestrictionsType.valueOf(name.toUpperCase());
}
private String value;
}
自定义IP注解,作用在方法上。
参数详解
- item
限制项目, 默认为空,启用全局限制。 - type
限制类别,默认使用数据库限制,即 IP 黑白名单存储在数据库中。 - allow
IP 白名单列表, 当 type = ALLOW 时,该属性如有填值,则使用属性值,否则取数据库配置。 - deny
IP 黑名单列表, 当 type = DENY 时,该属性如有填值,则使用属性值,否则取数据库配置。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IpRestrict {
/**
* 限制项目, 默认为空,全局限制
*/
String item() default "";
/**
* 限制类别, 默认取DB 数据库
*/
IpRestrictionsType type() default IpRestrictionsType.DB;
/**
* 允许IP列表, type = IpRestrictionsType.ALLOW
*/
String[] allow() default {};
/**
* 拒绝IP列表, type = IpRestrictionsType.DENY
*/
String[] deny() default {};
}
增加配置表实体,数据库的 IP 黑白名单配置在这张表中。
@Data
@TableName("TB_CONFIG_IP_RESTRICT")
@ApiModel(value="TbConfigIpRestrict对象", description="IP白名单配置表")
public class TbConfigIpRestrict extends BaseModel<TbConfigIpRestrict> {
@ApiModelProperty(value = "主键")
@TableId(value = "ID", type = IdType.ASSIGN_ID)
private String id;
@ApiModelProperty(value = "IP")
@TableField("IP")
private String ip;
@ApiModelProperty(value = "允许-allow 拒绝-deny")
@TableField("TYPE")
private String type;
@ApiModelProperty(value = "项目")
@TableField("ITEM")
private String item;
@ApiModelProperty(value = "备注")
@TableField("REMARK")
private String remark;
}
public interface TbConfigIpRestrictDao extends BaseMapper<TbConfigIpRestrict> {
}
创表语句
-- Create table
create table TB_CONFIG_IP_RESTRICT
(
id VARCHAR2(64) primary key,
ip VARCHAR2(20),
type VARCHAR2(5),
item VARCHAR2(20),
remark VARCHAR2(20)
);
-- Add comments to the table
comment on table TB_CONFIG_IP_RESTRICT
is 'IP白名单配置表';
-- Add comments to the columns
comment on column TB_CONFIG_IP_RESTRICT.id
is '主键';
comment on column TB_CONFIG_IP_RESTRICT.ip
is 'IP , 0.0.0.0-IP白名单配置';
comment on column TB_CONFIG_IP_RESTRICT.type
is '允许-allow 拒绝-deny , ip=0.0.0.0时表示采取的配置策略';
comment on column TB_CONFIG_IP_RESTRICT.item
is '项目 , ip=0.0.0.0时,item为空表示全局配置';
comment on column TB_CONFIG_IP_RESTRICT.remark
is '备注 , ip=0.0.0.0时为布尔值,表示是否开启IP白名单';
在切面类中实现对 IP 的限制, 从数据库中读取配置,确认是否开启 IP 限制,约定以 0.0.0.0
作为IP 策略配置开关, 如 IP = 0.0.0.0 , TYPE = allow , REMARK = false
表示关闭全局 IP 限制;IP = 0.0.0.0 , TYPE = allow , ITEM = test, REMARK = true
表示开启项目 test (即:注解中 item = test) 的 IP 白名单限制。第一次访问时从数据库读取配置信息,将其存入缓存,后续都将优先读取缓存配置,减少对数据库的频繁读取。
@Slf4j
@Aspect
@Component
public class IpRestrictAspect {
private static final String ipRestrictConfig = "ipRestrict:config";
@Autowired
private TbConfigIpRestrictDao tbConfigIpRestrictDao;
@Resource(name = "pubRedisTemplate")
RedisTemplate<String, String> redisTemplate;
@Before("@annotation(ipRestrict)")
public void doBefore(JoinPoint point, IpRestrict ipRestrict) throws Throwable {
TbConfigIpRestrict config = null;
String key = StringUtils.isEmpty(ipRestrict.item()) ? ipRestrictConfig : (ipRestrictConfig + ":" + ipRestrict.item());
String configStr = redisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(configStr)) {
//取 0.0.0.0 的配置确认是否开启IP白名单
config = tbConfigIpRestrictDao.selectOne(Wrappers.<TbConfigIpRestrict>lambdaQuery().eq(TbConfigIpRestrict::getIp, "0.0.0.0")
.eq(StringUtils.isNotEmpty(ipRestrict.item()), TbConfigIpRestrict::getItem, ipRestrict.item())
.isNull(StringUtils.isEmpty(ipRestrict.item()), TbConfigIpRestrict::getItem)
);
if(config == null){
throw new CustomException("请先增加IP白名单配置,");
}
redisTemplate.opsForValue().set(key, JSON.toJSONString(config), 1, TimeUnit.HOURS);
} else {
config = JSON.parseObject(configStr, TbConfigIpRestrict.class);
}
if (config != null && "true".equalsIgnoreCase(config.getRemark())) {
IpRestrictionsType type = ipRestrict.type();
String ip = IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
switch (type) {
case DB:
switch (IpRestrictionsType.match(config.getType())) {
case DENY:
if (ipAllow(ip, DENY, ipRestrict.item())) {
throw new CustomException(String.format("IP DENY %s ", ip));
}
break;
case ALLOW:
if (!ipAllow(ip, ALLOW, ipRestrict.item())) {
throw new CustomException(String.format("IP NOT ALLOW %s ", ip));
}
break;
}
break;
case ALLOW:
String[] allowArr = ipRestrict.allow();
if (allowArr.length == 0) {
allowArr = initIp(ALLOW, ipRestrict.item());
}
if (!Arrays.asList(allowArr).contains(ip)) {
throw new CustomException(String.format("IP NOT ALLOW %s ", ip));
}
break;
case DENY:
String[] denyArr = ipRestrict.deny();
if (denyArr.length == 0) {
denyArr = initIp(DENY, ipRestrict.item());
}
if (Arrays.asList(denyArr).contains(ip)) {
throw new CustomException(String.format("IP DENY %s ", ip));
}
break;
}
}
}
private String[] initIp(@NonNull IpRestrictionsType type, String value) {
String[] ip = new String[4];
List<TbConfigIpRestrict> ipList = tbConfigIpRestrictDao.selectList(Wrappers.<TbConfigIpRestrict>lambdaQuery()
.eq(TbConfigIpRestrict::getType, type.getValue())
.eq(StringUtils.isNotEmpty(value), TbConfigIpRestrict::getItem, value));
if (ipList != null && ipList.size() > 0) {
ip = ipList.stream().map(TbConfigIpRestrict::getIp).collect(Collectors.toList()).toArray(ip);
}
return ip;
}
private boolean ipAllow(@NonNull String ip, @NonNull IpRestrictionsType type, String item) {
return tbConfigIpRestrictDao.selectCount(Wrappers.<TbConfigIpRestrict>lambdaQuery()
.eq(StringUtils.isNotEmpty(ip), TbConfigIpRestrict::getIp, ip)
.eq(TbConfigIpRestrict::getType, type.getValue())
.eq(StringUtils.isNotEmpty(item), TbConfigIpRestrict::getItem, item)
) > 0;
}
}
由于约定以0.0.0.0
作为配置开关,所以严格来说工具类应过滤掉这个特殊ip。
/**
* 从http请求中获取ip地址
*/
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
//X-Forwarded-For:Squid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//Proxy-Client-IP:apache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//WL-Proxy-Client-IP:weblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//HTTP_CLIENT_IP:有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//X-Real-IP:nginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
//有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
if (ipAddresses != null && ipAddresses.length() != 0) {
ip = ipAddresses.split(",")[0];
}
//还是不能获取到,最后再通过request.getRemoteAddr();获取
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
配置示例
开关配置
黑白名单配置
注解应用
@IpRestrict
使用默认配置。@IpRestrict(item = "test", type = IpRestrictionsType.DB)
采用数据库配置, 应用项目为 test。@IpRestrict(type = IpRestrictionsType.ALLOW, allow = {"127.0.0.1"})
使用注解中的配置, 允许IP 为 127.0.0.1 的请求访问。