ip归属地的获取和使用

ip获取

http请求

对于controller的请求,我们只需要写个拦截器,将用户的ip设置进上下文即可,非常方便。

1
2
3
4
5
6
7
8
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestInfo info = new RequestInfo();
info.setUid(Optional.ofNullable(request.getAttribute(TokenInterceptor.ATTRIBUTE_UID)).map(Object::toString).map(Long::parseLong).orElse(null));
info.setIp(ServletUtil.getClientIP(request));
RequestHolder.set(info);
return true;
}

ip在请求头中都会携带。直接用hutool的工具类获取ip

1
2
3
4
5
6
7
8
public static String getClientIP(HttpServletRequest request, String... otherHeaderNames) {
String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
headers = ArrayUtil.addAll(headers, otherHeaderNames);
}

return getClientIPByHeader(request, headers);
}

这里有点要注意,如果我们开启了nginx来带来请求,需要在nginx里面保存用户真实ip到X-Real-IP,否则你拿到的就是nginx的ip地址了。

websocket请求

对于websocket请求获取ip就会麻烦一些。

首先我们要有个概念,websocket初期会借助http来升级协议。所以我们需要在http升级之前就要获取ip,并且将用户ip保存起来。

在协议升级前,我们加入了HttpHeadersHandler处理器,这时候还是能拿到http的request的,想获取header很容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
private AttributeKey<String> key = AttributeKey.valueOf("Id");

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
HttpHeaders headers = ((FullHttpRequest) msg).headers();
String ip = headers.get("X-Real-IP");
if (Objects.isNull(ip)) {//如果没经过nginx,就直接获取远端地址
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
ip = address.getAddress().getHostAddress();
}
NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);
}
ctx.fireChannelRead(msg);
}
}

之后协议升级后,请求就不会再走这个处理器了,所以我们的ip需要保存起来。正好channel其实有个附件功能,我们可以直接把ip作为附件保存进channel。之后每次的websocket请求,都用的是同一个channel,从里面取ip就好了。

正好所有连接的用户,我们也会去保存uid和channel的映射关系,保存这个channel。

ip的更新

ip的更新时机其实也是一个话题。总不可能用户每次请求,我们都要去做一次ip更新吧,那也太麻烦了。我们可以在用户首次认证去更新ip即可。

用户首次认证有两个场景:

1.用户浏览器里有token。前端拿它来后端认证下即可。

2.用户token失效重新扫码登录。

针对于第二种登录,扫码的时候是wx给我们的回调。我们通过回调的code,去找出code对应的连接channel。再从channel里找到用户信息以及ip

我们可以选用用户认证的时间点来触发ip的刷新。

ip的保存

ip的信息其实是个比较复杂的数据类型,我们可以直接通过json格式存成user的扩展信息。

json格式需要mysql5.7+。

ip获取两个入口,http,websokcet。

1
2
3
4
5
6
7
8
//注册时的ip
private String createIp;
//注册时的ip详情
private IpDetail createIpDetail;
//最新登录的ip
private String updateIp;
//最新登录的ip详情
private IpDetail updateIpDetail;
1
2
3
4
5
6
7
8
9
10
11
//注册时的ip
private String ip;
//最新登录的ip
private String isp;
private String isp_id;
private String city;
private String city_id;
private String country;
private String country_id;
private String region;
private String region_id;

json格式在数据库里就是一个字符串,可以通过sql很方便的提取或更新其中的某个字段。通过mybatisplus获取实体类的时候,也可以自动帮忙反序列化。具体配置可看json字段整合

ip归属地解析

ip的归属地解析也是个很有意思的话题,本质就是利用ip解析出所属地区。

基于淘宝开放接口

淘宝有提供了ip地址库的查询接口,大家可以自己postman测试下。

淘宝的IP地址库API可以提供IP地址的详细信息,包括国家、省份、城市、经纬度等。使用淘宝的IP地址库API,可以轻松获取IP地址的详细信息,从而获取IP地址的地理位置。

1
2
3
curl --request GET \
--url 'https://ip.taobao.com/outGetIpInfo?ip=112.96.166.230&accessKey=alibaba-inc' \
--header 'content-type: application/json'

淘宝自己的地址库会一直更新,比较全。而且没有任何依赖,直接接口解析。它只有一个缺点,就是有频控o(╥﹏╥)o。

1
2
3
4
{
"msg": "the request over max qps for user ,the accessKey=alibaba-inc",
"code": 4
}

如果想用淘宝的地址解析,我们需要写一套框架,能够适应他的频控,匀速的排队的能够重试的去慢慢异步解析我们的ip详情,我是怎么做的呢?