• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

通过ip获取用户登录地点,实现登录日志功能。——从零开始搭建高颜值后台管理系统全栈框架(十一)

武飞扬头像
前端小付
帮助2

往期回顾

前端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(一)

后端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(二)

实现登录功能jwt or token redis?——从零开始搭建一个高颜值后台管理系统全栈框架(三)

封装axios,让请求变得丝滑——从零开始搭建一个高颜值后台管理系统全栈框架(四)

实现前后端全自动化部署,解放你的双手。——从零开始搭建一个高颜值后台管理系统全栈框架(五)

雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

基于react-router v6实现动态菜单、动态路由。内含vue动态路由实现。——从零开始搭建一个高颜值后台管理系统全栈框架(七)

通过RBAC模型实现前后端动态菜单和动态路由——从零开始搭建一个高颜值后台管理系统全栈框架(八)

使用黑科技实现前端按钮权限控制,太优雅了。——从零开始搭建一个高颜值后台管理系统全栈框架(九)

集成WebSocket实现用户权限变更消息推送,自动刷新。——从零开始搭建一个高颜值后台管理系统全栈框架(十)

前言

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

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

实现思路

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

具体实现

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

学新通

  • 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 学新通 学新通

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

学新通

封装一个统一获取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个字段

学新通

获取浏览器信息

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

学新通

打印出来的结果如下:

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

学新通

用户登录日志功能实现

使用下面命令快速创建一个登录日志模块。

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

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

学新通

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

学新通

前端查询实现

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

效果展示

学新通

总结

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

帐号/密码:user/123456

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

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

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfgaekc
系列文章
更多 icon
同类精品
更多 icon
继续加载