后端开发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)) { 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
| private String createIp;
private IpDetail createIpDetail;
private String updateIp;
private IpDetail updateIpDetail;
|
1 2 3 4 5 6 7 8 9 10 11
| private String 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详情,我是怎么做的呢?