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

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

大厂技术 高级前端 Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

前言

上一篇文章中留了一个坑,pm2开启多进程,会导致给用户推送消息失败,具体原因上一篇文章中已经说过了。这一篇我们先解决一下这个问题。

现在各大平台都支持显示用户地址,其实实现起来很简单。我们这一篇就实现一下通过用户ip获取用户地址。

使用redis消息广播解决上篇文章的坑实现思路改造发消息的方法,通过redis消息广播把消息发给各个进程,各个进程监听对应频道,如果收到消息,通过userId找到用户websocket连接,然后把消息发出去。

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

image.pngdefault:默认实例,给正常代码中使用。

publish:发布消息使用

subscribe:订阅消息使用

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

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 })

);

}

}获取登录用户ipmidway中可以从请求上下文获取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就行了。

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这个库,也可以调用一些公共接口获取,这里我们使用第一种方式。

封装公共方法

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这个库来解析里面的数据,获取用户使用的是什么浏览器,以及操作系统。

封装一个公共方法:

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用户登录日志功能实现使用下面命令快速创建一个登录日志模块。node ./script/create-module login.log改造LoginLogEntity实体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总结到此我们把上篇文章中留下的坑和登录日志功能搞定了。如果文章对你有帮忙,帮忙点个赞吧,谢谢了。

加了一个普通用户,兄弟们可以用这个帐号测试权限功能。

帐号/密码:user/123456

项目体验地址:fluxyadmin.cn/user/login[1]

前端仓库地址:github.com/dbfu/fluxy-…[2]

后端仓库地址:github.com/dbfu/fluxy-…[3]

参考资料[1]

https://fluxyadmin.cn/user/login: https://link.juejin.cn/?target=https%3A%2F%2Ffluxyadmin.cn%2Fuser%2Flogin

[2]https://github.com/dbfu/fluxy-admin-web: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdbfu%2Ffluxy-admin-web

[3]https://github.com/dbfu/fluxy-admin-server: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdbfu%2Ffluxy-admin-server

关于本文

作者:前端小付

https://juejin.cn/post/7257511618824355877

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

“分享、点赞、在看” 支持一下

清芳推荐

亿连手机互联使用教程 亿连手机互联安装教程
约彩365安卓老版本

亿连手机互联使用教程 亿连手机互联安装教程

📅 08-21 👀 9416
希腊语30句常用语(附带中文谐音读法)
365bet亚洲版登录

希腊语30句常用语(附带中文谐音读法)

📅 07-17 👀 7767
Apple(苹果)一体电脑报价
约彩365安卓老版本

Apple(苹果)一体电脑报价

📅 09-12 👀 2649