通过 ip 获取用户登录地点,实现登录日志功能

您所在的位置:网站首页 微信登陆ip查询不到 通过 ip 获取用户登录地点,实现登录日志功能

通过 ip 获取用户登录地点,实现登录日志功能

2024-06-30 18:54| 来源: 网络整理| 查看: 265

前言上一篇文章中留了一个坑,pm2开启多进程,会导致给用户推送消息失败,具体原因上一篇文章中已经说过了。这一篇我们先解决一下这个问题。现在各大平台都支持显示用户地址,其实实现起来很简单。我们这一篇就实现一下通过用户ip获取用户地址。使用redis消息广播解决上篇文章的坑实现思路

改造发消息的方法,通过redis消息广播把消息发给各个进程,各个进程监听对应频道,如果收到消息,通过userId找到用户websocket连接,然后把消息发出去。

具体实现

后端redis发布订阅方法和普通redis不能使用同一个redis实例,发布订阅也不能使用同一个实例,所以我们需要配置三个实例。

image.png

default:默认实例,给正常代码中使用。publish:发布消息使用subscribe:订阅消息使用

改造SocketService代码,代码很简单。其他代码不用改。

代码语言:javascript复制import { Autoload, Init, InjectClient, Singleton } from '@midwayjs/core'; import { Context } from '@midwayjs/ws'; import { SocketMessage } from './message'; import { RedisService, RedisServiceFactory } from '@midwayjs/redis'; const socketChannel = 'socket-message'; @Singleton() @Autoload() export class SocketService { connects = new Map(); // 导入发布消息的redis实例 @InjectClient(RedisServiceFactory, 'publish') publishRedisService: RedisService; // 导入订阅消息的redis实例 @InjectClient(RedisServiceFactory, 'subscribe') subscribeRedisService: RedisService; @Init() async init() { // 系统启动的时候,这个方法会自动执行,监听频道。 await this.subscribeRedisService.subscribe(socketChannel); // 如果接受到消息,通过userId获取连接,如果存在,通过连接给前端发消息 this.subscribeRedisService.on( 'message', (channel: string, message: string) => { if (channel === socketChannel && message) { const messageData = JSON.parse(message); const { userId, data } = messageData; const clients = this.connects.get(userId); if (clients?.length) { clients.forEach(client => { client.send(JSON.stringify(data)); }); } } } ); } /** * 添加连接 * @param userId 用户id * @param connect 用户socket连接 */ addConnect(userId: string, connect: Context) { const curConnects = this.connects.get(userId); if (curConnects) { curConnects.push(connect); } else { this.connects.set(userId, [connect]); } } /** * 删除连接 * @param connect 用户socket连接 */ deleteConnect(connect: Context) { const connects = [...this.connects.values()]; for (let i = 0; i < connects.length; i += 1) { const sockets = connects[i]; const index = sockets.indexOf(connect); if (index >= 0) { sockets.splice(index, 1); break; } } } /** * 给指定用户发消息 * @param userId 用户id * @param data 数据 */ sendMessage(userId: string, data: SocketMessage) { // 通过redis广播消息 this.publishRedisService.publish( socketChannel, JSON.stringify({ userId, data }) ); } } 获取登录用户ip

midway中可以从请求上下文获取ip

不过前面有::ffff:,我们可以使用replace方法给替换掉。

如果用这个方式获取不到ip,我们还可以this.ctx.req.socket.remoteAddress获取ip。

如果线上使用nginx配置了反向代理,我们可以从请求头上获取ip,使用this.ctx.req.headers['x-forwarded-for']或this.ctx.req.headers['X-Real-IP']这两个方法就行。

nginx配置反向代理的时候,这两个配置不要忘记加了。

image.png

封装一个统一获取ip的方法,this.ctx.req.headers['x-forwarded-for']有可能会返回两个ip地址,中间用,隔开,所以需要split一下,取第一个ip就行了。

代码语言:javascript复制export const getIp = (ctx: Context) => { const ips = (ctx.req.headers['x-forwarded-for'] as string) || (ctx.req.headers['X-Real-IP'] as string) || (ctx.ip.replace('::ffff:', '') as string) || (ctx.req.socket.remoteAddress.replace('::ffff:', '') as string); console.log(ips.split(',')?.[0], 'ip'); return ips.split(',')?.[0]; }; 通过ip获取地址

通过ip获取地址可以使用ip2region这个库,也可以调用一些公共接口获取,这里我们使用第一种方式。

封装公共方法

代码语言:javascript复制import IP2Region from 'ip2region'; export const getAddressByIp = (ip: string): string => { if (!ip) return ''; const query = new IP2Region(); const res = query.search(ip); return [res.province, res.city].join(' '); };

查询结果中包含国家、省份、城市、供应商4个字段

image.png

获取浏览器信息

可以从请求头上获取浏览器信息

image.png

打印出来的结果如下:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36

我们可以用useragent这个库来解析里面的数据,获取用户使用的是什么浏览器,以及操作系统。

封装一个公共方法:

代码语言:javascript复制import * as useragent from 'useragent'; export const getUserAgent = (ctx: Context): useragent.Agent => { return useragent.parse(ctx.headers['user-agent'] as string); };

返回这几个属性,family表示浏览器,os表示操作系统。

image.png

用户登录日志功能实现使用下面命令快速创建一个登录日志模块。代码语言:javascript复制node ./script/create-module login.log 改造LoginLogEntity实体代码语言:javascript复制import { Entity, Column } from 'typeorm'; import { BaseEntity } from '../../../common/base.entity'; @Entity('sys_login_log') export class LoginLogEntity extends BaseEntity { @Column({ comment: '用户名' }) userName?: string; @Column({ comment: '登录ip' }) ip?: string; @Column({ comment: '登录地点' }) address?: string; @Column({ comment: '浏览器' }) browser?: string; @Column({ comment: '操作系统' }) os?: string; @Column({ comment: '登录状态' }) status?: boolean; @Column({ comment: '登录消息' }) message?: string; } 在用户登录方法中添加登录日志

image.png

登录成功时,把status设置位true,message为成功。登录失败时把status设置位false,message为错误消息。最后在finally中把数据添加到数据库,这里不要用await,做成异步的,不影响正常接口响应速度。

image.png

前端查询实现

就是一个正常的表格展示,没啥好说的。

效果展示

image.png



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3