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

高级前端进阶

共 9042字,需浏览 19分钟

 · 2023-08-17

前言

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

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

使用redis消息广播解决上篇文章的坑

实现思路

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

具体实现

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

b1075f6848afbddb4cc3e7d6ea861698.webpimage.png
  • default:默认实例,给正常代码中使用。
  • 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<string, Context[]>();
  // 导入发布消息的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<T>(userId: string, data: SocketMessage<T>) {
    // 通过redis广播消息
    this.publishRedisService.publish(
      socketChannel,
      JSON.stringify({ userId, data })
    );
  }
}

获取登录用户ip

midway中可以从请求上下文获取ip 2b8baae9f69870065d8e5497c3519ef1.webpd29fc9ec97c648fc07fac6fa4ee0402f.webp

不过前面有::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配置反向代理的时候,这两个配置不要忘记加了。

d182eb695c34d9bebe06228b932c1c29.webpimage.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个字段

a2f57b9649229ea84e959ae3bbb5212a.webpimage.png

获取浏览器信息

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

b5e58b267fea448c04c9ed1e31018fb2.webpimage.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表示操作系统。

08c159ad0b6edf4f2b3c60fb17fe0963.webpimage.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;
}

在用户登录方法中添加登录日志

c2992fe8e478ca4225b10931ca6b1f6e.webpimage.png

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

a748edb40b7b67c2bc2f5dd81d0fc945.webpimage.png

前端查询实现

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

效果展示

68a8ecfdb191e443111b8e9778dc7d53.webpimage.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

The End


如果你觉得这篇内容对你挺有启发,我想请你帮我三个小忙: 1、点个 「在看」,让更多的人也能看到这篇内容 2、关注官网 https://muyiy.cn,让我们成为长期关系 3、关注公众号「高级前端进阶」,公众号后台回复 「加群」 ,加入我们一起学习并送你精心整理的高级前端面试题。

》》面试官都在用的题库,快来看看《


          
                

最后不要忘了点赞呦!


浏览 88
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报