Obtain the user login location through ip to realize the login log function. ——Build a full-stack framework of a high-value background management system from scratch (11)

Past review

Front-end framework construction - build a high-value background management system full-stack framework from scratch (1)

Back-end framework construction - build a high-value background management system full-stack framework from scratch (2)

Realize the login function jwt or token+redis? ——Build a high-value background management system full-stack framework from scratch (3)

Encapsulate axios to make the request silky - build a high-value background management system full-stack framework from scratch (4)

Realize fully automated front-end and back-end deployment, freeing your hands. ——Build a high-value background management system full-stack framework from scratch (5)

Snowflake algorithm, attachment scheme, email verification, password change. ——Build a full-stack framework of high-value background management system from scratch (6)

Realize dynamic menu and dynamic routing based on react-router v6. Contains vue dynamic routing implementation. ——Build a full-stack framework of a high-value background management system from scratch (7)

Realize front-end and back-end dynamic menus and dynamic routing through the RBAC model - build a high-value background management system full-stack framework from scratch (8)

It is so elegant to use black technology to realize front-end button permission control. ——Build a full-stack framework of a high-value background management system from scratch (9)

Integrate WebSocket to realize user permission change message push and automatic refresh. ——Build a full-stack framework of a high-value background management system from scratch (10)

foreword

  • There is a pit in the previous article, pm2 starts multi-process, which will lead to the failure of pushing messages to users. The specific reason has been mentioned in the previous article. In this article, we first solve this problem.

  • All major platforms now support the display of user addresses, which is actually very simple to implement. In this article, we will realize how to obtain the user address through the user ip.

Use redis message broadcast to solve the pit of the previous article

Implementation ideas

Modify the method of sending messages, send messages to each process through redis message broadcast, and each process monitors the corresponding channel. If a message is received, find the user websocketconnection through userId, and then send the message.

Implementation

The back-end redis publish and subscribe method and ordinary redis cannot use the same redis instance, nor can publish and subscribe use the same instance, so we need to configure three instances.

image.png

  • default: The default instance, used in normal code.
  • publish: publish message using
  • 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 image.png image.png

不过前面有::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设置位truemessage为成功。登录失败时把status设置位falsemessage为错误消息。最后在finally中把数据添加到数据库,这里不要用await,做成异步的,不影响正常接口响应速度。

image.png

Front-end query implementation

It's just a normal table display, nothing to say.

Show results

image.png

Summarize

So far we have completed the pit and login log function left in the previous article. If the article is helpful to you, please give it a thumbs up, thank you.

Added an ordinary user, brothers can use this account to test the authority function.

Account/password: user/123456

Project experience address: fluxyadmin.cn/user/login

Front-end warehouse address: github.com/dbfu/fluxy-…

Backend warehouse address: github.com/dbfu/fluxy-…

Guess you like

Origin juejin.im/post/7257511618824355877