過去のレビュー
フロントエンドフレームワークの構築 - 高価値のバックグラウンド管理システムのフルスタックフレームワークをゼロから構築する (1)
バックエンドフレームワーク構築 - 高価値なバックエンド管理システムのフルスタックフレームワークをゼロから構築する (2)
ログイン関数 jwt または token+redis を実現しますか? ——価値の高いバックグラウンド管理システムのフルスタックフレームワークをゼロから構築する(3)
axios をカプセル化してリクエストをスムーズに処理する - 高価値のバックグラウンド管理システムのフルスタック フレームワークをゼロから構築する (4)
完全に自動化されたフロントエンドとバックエンドの展開を実現し、手を解放します。——価値の高いバックグラウンド管理システムのフルスタックフレームワークをゼロから構築する (5)
スノーフレークアルゴリズム、添付ファイルスキーム、電子メール検証、パスワード変更。——高価値バックグラウンド管理システムのフルスタックフレームワークをゼロから構築する (6)
RBAC モデルを通じてフロントエンドとバックエンドの動的メニューと動的ルーティングを実現 - 高価値のバックグラウンド管理システムのフルスタック フレームワークをゼロから構築する (8)
ブラックテクノロジーを使用してフロントエンドボタンの許可制御を実現するのは非常にエレガントです。——高価値バックグラウンド管理システムのフルスタックフレームワークをゼロから構築する (9)
WebSocketを統合し、ユーザー権限変更メッセージのプッシュと自動更新を実現します。——高価値のバックグラウンド管理システムのフルスタック フレームワークをゼロから構築する (10)
序文
-
前回の記事には落とし穴があり、pm2 がマルチプロセスを開始することでユーザーへのメッセージのプッシュが失敗するという問題があり、具体的な理由は前回の記事で述べました。この記事では、まずこの問題を解決します。
-
すべての主要なプラットフォームがユーザー アドレスの表示をサポートするようになりました。これは実際には実装が非常に簡単です。この記事では、ユーザー IP を通じてユーザー アドレスを取得する方法を実現します。
Redis メッセージ ブロードキャストを使用して前の記事の落とし穴を解決する
実装のアイデア
メッセージの送信方法を変更し、redis メッセージ ブロードキャストを通じて各プロセスにメッセージを送信し、各プロセスが対応するチャネルを監視します。メッセージを受信した場合は、 userId を通じてユーザー接続を見つけて、メッセージを送信しますwebsocket
。
実装
バックエンド Redis のパブリッシュおよびサブスクライブ メソッドと通常の Redis は同じ Redis インスタンスを使用できません。また、パブリッシュとサブスクライブで同じインスタンスを使用することもできないため、3 つのインスタンスを構成する必要があります。
- default: 通常のコードで使用されるデフォルトのインスタンス。
- パブリッシュ: を使用してメッセージをパブリッシュします
- 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
设置位true
,message
为成功。登录失败时把status
设置位false
,message
为错误消息。最后在finally
中把数据添加到数据库,这里不要用await
,做成异步的,不影响正常接口响应速度。
フロントエンドクエリの実装
何も言うことのない、普通のテーブルディスプレイです。
結果を示す
要約する
前回の記事で残したピットとログインログ機能はここまでで完了です。記事が役に立った場合は、高評価をお願いします。ありがとうございます。
一般ユーザーを追加しました。兄弟はこのアカウントを使用して権限機能をテストできます。
アカウント/パスワード: user/123456
プロジェクト エクスペリエンス アドレス: fluxyadmin.cn/user/login
フロントエンド ウェアハウスのアドレス: github.com/dbfu/fluxy-…
バックエンド ウェアハウスのアドレス: github.com/dbfu/fluxy-…