引言

本文记录一个关于 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;
    }
}

配置示例

开关配置

容器 ip白名单 ip白名单怎么填_tcp/ip

黑白名单配置

容器 ip白名单 ip白名单怎么填_tcp/ip_02

注解应用

  • @IpRestrict 使用默认配置。
  • @IpRestrict(item = "test", type = IpRestrictionsType.DB) 采用数据库配置, 应用项目为 test。
  • @IpRestrict(type = IpRestrictionsType.ALLOW, allow = {"127.0.0.1"}) 使用注解中的配置, 允许IP 为 127.0.0.1 的请求访问。