IP 离线查询

148 阅读3分钟

Ip2regionEntity

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("ip2region entity")
public class Ip2regionEntity {

    @ApiModelProperty("国家")
    @NotNull
    private String country;

    @ApiModelProperty("区域")
    private String district;

    @ApiModelProperty("省份")
    private String province;

    @ApiModelProperty("市")
    private String city;

    @ApiModelProperty("运营商")
    private String operators;

    @ApiModelProperty("内网")
    private Boolean isInnerIp;

    @ApiModelProperty("ip")
    private String ip;

    @ApiModelProperty("long ip")
    private Long iplong;

    @ApiModelProperty("raw")
    private String raw;

    public Ip2regionEntity(Boolean isInnerIp) {
        this.isInnerIp = isInnerIp;
    }

    public static Ip2regionEntity innerIp() {
        return new Ip2regionEntity(true);
    }

    @Override
    public String toString() {
        return raw;
    }
}

UserEntity

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user", autoResultMap = true)
@Builder
@ApiModel(description = "用户实体对象")
public class UserEntity implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;
	
    ......

    @ApiModelProperty("ipv4 ipv6 数字地址")
    @TableField(fill = FieldFill.INSERT)
    private Long ip;

    @ApiModelProperty("ip 字符地址 国家|区域|省|市|宽带")
    @TableField(typeHandler = Ip2regionTypeHandler.class, jdbcType = JdbcType.VARCHAR, fill = FieldFill.INSERT)
    // @JsonSerialize(using = ToStringSerializer.class)
    private Ip2regionEntity ipAddr;
}

Ip2regionConfig

@Configuration
public class Ip2regionConfig {

    @Value("classpath:ip2region.xdb")
    private Resource xdb;

    @Bean(destroyMethod = "close")
    public Searcher search() throws IOException {
        byte[] db = xdb.getInputStream().readAllBytes();
        return Searcher.newWithBuffer(db);
    }
}

Ip2regionTypeUtil

package com.bluereba.pojo.utils;

import cn.hutool.core.net.NetUtil;
import com.bluereba.pojo.handler.Ip2regionEntity;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

/**
 * 工具类
 *
 * @author 叶大于
 * @since 2023/5/14 18:06
 */
public class Ip2regionTypeUtil {

    private static boolean check(String addr) {
        return !(StringUtils.isNotBlank(addr) && StringUtils.countMatches(addr, "|") == 5);
    }

    private static String[] safeSplit(String addr) {
        String[] split = StringUtils.split(addr, "|", 6);
        return ObjectUtils.isEmpty(split) ? new String[6] : split;
    }

    /**
     * 构建 Ip2region 实体
     *
     * @param addr ip地址
     * @return 构建实体
     */
    public static @NonNull Ip2regionEntity buildEntity(@Nullable String addr) {
        if (check(addr)) {
            return new Ip2regionEntity();
        }
        String[] split = safeSplit(addr);

        long ipv4 = NetUtil.ipv4ToLong(split[5]);

        return Ip2regionEntity.builder()
                .isInnerIp(false)
                .country(split[0])
                .district(split[1])
                .province(split[2])
                .city(split[3])
                .operators(split[4])
                .ip(split[5])
                .iplong(ipv4)
                .raw(addr)
                .build();
    }

    /**
     * 有效 ip2region results 拼接 += |ip
     * @param addr ip2region results
     * @param ip ip地址
     * @return 完整格式
     */
    public static String joinIp(@Nullable String addr, @Nullable String ip) {
        if (check(addr)) {
            return addr + "|" + ip;
        }
        return addr;
    }

    public static String joinIp(@Nullable String addr, @Nullable Long ip) {
        if (check(addr) && ObjectUtils.isNotEmpty(ip)) {
            return addr + "|" + NetUtil.longToIpv4(ip);
        }
        return addr;
    }

    public static String toString(@Nullable Ip2regionEntity entity) {
        return ObjectUtils.isEmpty(entity) ? null : entity.toString();
    }
}

IpSearcher

@Component
@RequiredArgsConstructor
public class IpSearcher {

    private final Searcher searcher;

    /**
     * 查询 ip地址
     * @param ip 必须符合 ip4 格式 (@Ipv4)
     * @return Ip2regionEntity
     */
    public Ip2regionEntity search(@NonNull String ip) {
        try {
            String addr = searcher.search(ip);
            String fullAddr = Ip2regionTypeUtil.joinIp(addr, ip);

            return Ip2regionTypeUtil.buildEntity(fullAddr);
        } catch (Exception e) {
            throw new IpSearchException();
        }
    }

    /**
     * 查询 ip地址
     * @param ip 必须符合 ip number 格式
     * @return Ip2regionEntity
     */
    public Ip2regionEntity search(@NonNull Long ip) {
        try {
            String addr = searcher.search(ip);
            String fullAddr = Ip2regionTypeUtil.joinIp(addr, ip);

            return Ip2regionTypeUtil.buildEntity(fullAddr);
        } catch (Exception e) {
            throw new IpSearchException();
        }
    }
}

Ip2regionTypeHandler

数据库中存储格式 中国|0|河南省|焦作市|联通|42.226.158.210

依赖于Mybatis,通过 setNonNullParameter 存储至数据库

通过 getNullableResult 在 java 中获取 Ip2regionEntity 对象

@Component
@MappedJdbcTypes(JdbcType.VARCHAR)
public class Ip2regionTypeHandler extends BaseTypeHandler<Ip2regionEntity> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Ip2regionEntity entity, JdbcType jt)
            throws SQLException {
        ps.setObject(i, Ip2regionTypeUtil.toString(entity));
    }

    @Override
    public Ip2regionEntity getNullableResult(ResultSet resultSet, String s) throws SQLException {
        String addr = resultSet.getString(s);
        return Ip2regionTypeUtil.buildEntity(addr);
    }

    @Override
    public Ip2regionEntity getNullableResult(ResultSet resultSet, int i) throws SQLException {
        String addr = resultSet.getString(i);
        return Ip2regionTypeUtil.buildEntity(addr);
    }

    @Override
    public Ip2regionEntity getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        String addr = callableStatement.getString(i);
        return Ip2regionTypeUtil.buildEntity(addr);
    }
}

Mybatis Plus 自动填充

@Slf4j
@Component
@RequiredArgsConstructor
public class MyMetaObjectHandler implements MetaObjectHandler {

    private final IpSearchService ipSearchService;

    private final HttpServletRequest request;

    @Override
    public void insertFill(MetaObject meta) {
        // TODO add insert operate user info
        this.strictInsertFill(meta, "createAt", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(meta, "updateAt", LocalDateTime::now, LocalDateTime.class);

        // 模拟非内网
        addressFill(meta, "42.226.158.210");
        // addressFill(meta, ServletUtil.getClientIP(request));
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // TODO add update operate user info
        this.strictUpdateFill(metaObject, "updateAt", LocalDateTime::now, LocalDateTime.class);
    }

    private void addressFill(MetaObject metaObject, String ip) {
        ipSearchService.getAddressOpt(ip).ifPresent(address -> {
            this.strictInsertFill(metaObject, "ip", address::getIplong, Long.class);

            this.strictInsertFill(metaObject, "ipAddr", () -> address, Ip2regionEntity.class);
        });
    }
}

Test

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class IpSearchTest {

    @Autowired
    IpSearchService ipSearchService;

    @Test
    public void getAddress() {
        ipSearchService.getAddressOpt("14.117.140.125").ifPresent(System.out::println);
        // 中国|0|广东省|江门市|电信|14.117.140.125
    }

    @Test
    public void longToAddress() {
        ipSearchService.getAddressOpt(24251L).ifPresent(System.out::println);
        // 0|0|0|内网IP|内网IP|0.0.94.187
    }
}

View

POST /user/add
body---
{
    "username": "张敏^张敏^张敏^张敏",
    "password": "aute aliquip",
    "email": "i.kgsz@qq.com"
}

@PostMapping("add")
@ApiOperation("添加用户")
public UserEntity addUser(@RequestBody @Valid AddUserVo vo) {
    String username = vo.getUsername();
    String password = vo.getPassword();
    String email = vo.getEmail();

    UserEntity entity = UserEntity.builder()
            .username(username)
            .password(password)
            .email(email)
            .build();

    userService.save(entity);
    return entity;
}
@Data
@ApiModel(description = "添加用户vo")
public class AddUserVo implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    @Size(min = 6, max = 12)
    @NotEmpty(message = "用户名不能为空")
    @ApiModelProperty("用户名")
    private String username;

    @Size(min = 6, max = 12)
    @NotNull(message = "用户密码不能为空")
    @ApiModelProperty("用户密码")
    private String password;

    @Email
    @ApiModelProperty("用户邮件")
    private String email;
}
{
    "status": 200,
    "message": "请求成功",
    "data": {
        "nickname": null,
        "id": 1657809333670019073,
        "username": "张敏^张敏^张敏^张敏",
        "email": "i.kgsz@qq.com",
        "address": null,
        "gender": null,
        "icon": null,
        "age": null,
        "createAt": "2023-05-15T02:05:20.4228379",
        "updateAt": "2023-05-15T02:05:20.4238371",
        "createBy": null,
        "updateBy": null,
        "isStatus": null,
        "isDeleted": null,
        "remark": null,
        "version": null,
        "ip": 719494866,
        "ipAddr": {
            "country": "中国",
            "district": "0",
            "province": "河南省",
            "city": "焦作市",
            "operators": "联通",
            "isInnerIp": false,
            "ip": "42.226.158.210",
            "iplong": 719494866,
            "raw": "中国|0|河南省|焦作市|联通|42.226.158.210"
        }
    }
}