Websocket 适配器中的 NestJS 依赖注入

问题描述 投票:0回答:2

我正在尝试在 NestJS 应用程序中建立 Websocket 连接时验证和检查用户的权限。

我发现这个讨论建议使用NestJS Websocket适配器。您可以在

options.allowRequest
回调中执行令牌验证,如下所示。

export class AuthenticatedSocketIoAdapter extends IoAdapter {

  private readonly authService: AuthService;
  constructor(private app: INestApplicationContext) {
    super(app);
    this.authService = app.get(AuthService);
  }

  createIOServer(port: number, options?: SocketIO.ServerOptions): any {
    options.allowRequest = async (request, allowFunction) => {

      const token = request.headers.authorization.replace('Bearer ', '');

      const verified = this.authService.verifyToken(token);
      if (verified) {
        return allowFunction(null, true);
      }
      
      return allowFunction('Unauthorized', false);
    };

    return super.createIOServer(port, options);
  }
}

但是,我在 websocket 适配器中的依赖注入方面遇到了问题。

IoAdapter
的构造函数有一个
INestApplicationContext
参数,我尝试使用
AuthService
从中获取
app.get(AuthService)
,如您在上面看到的。

AuthService 注入另外两个服务,一个

UserService
JwtService
来检查 JWT 令牌。我的问题是这些服务在该上下文中仍未定义。

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService, private jwtService: JwtService) {}

  verifyToken(token: string): boolean {
    // Problem: this.jwtService is undefined
    const user = this.jwtService.verify(token, { publicKey });
    // ... check user has permissions and return result
  }

仅供参考,

AuthService
位于另一个模块中,而不是定义 Websocket 的模块中。我还尝试在当前模块中导入 AuthService (及其依赖项),但这没有帮助。

可以通过

app.get()
方法使用该服务吗?

websocket dependency-injection service nestjs adapter
2个回答
2
投票

我可以使用

app.resolve()
而不是
app.get()

来解决 DI 问题
export class AuthenticatedSocketIoAdapter extends IoAdapter {
  private authService: AuthService;

  constructor(private app: INestApplicationContext) {
    super(app);
    app.resolve<AuthService>(AuthService).then((authService) => {
      this.authService = authService;
    });
  }
}

这解决了注入

jwtService
中的
AuthService
未定义的问题。


0
投票

它并不漂亮,但是我发现

socket.io
具有足够的弹性,可以在listen阶段之后
热插拔其大部分配置,这样我们就不需要适配器。

知道这一点后,我将网关视为实现

OnGatewayInit, OnApplicationShutdown

 的中间件,如下所示:

    这个用于处理与 Redis 的连接并设置服务器,就像在适配器中一样。
import { Inject, Injectable, Logger, type OnApplicationShutdown } from '@nestjs/common' import { WebSocketGateway, WebSocketServer, type OnGatewayInit, type OnGatewayConnection, type OnGatewayDisconnect, } from '@nestjs/websockets' import { createAdapter } from '@socket.io/redis-adapter' import { Redis } from 'ioredis' import { Server, type Socket } from 'socket.io' import { WebSocketConfig } from './config' @Injectable() @WebSocketGateway({ transports: ['websocket'] }) export class SocketIOServer implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, OnApplicationShutdown { private readonly logger = new Logger(SocketIOServer.name) private pub?: Redis private sub?: Redis @WebSocketServer() io: Server constructor (private readonly config: ConfigService<WebSocketConfig>) {} afterInit() { this.io.serveClient(false) // setup the redis cluster if (this.config.has('REDIS')) { this.pub = new Redis(this.config.get('REDIS')) this.sub = this.pub.duplicate() const events = ['close', 'connect', 'end', 'error', 'reconnecting'] events.forEach((event) => { this.pub!.on(event, (arg: unknown) => { this.logger.log(`{pub, event}: ${event} - (${arg ?? ''})`) }) this.sub!.on(event, (arg: unknown) => { this.logger.log(`{sub, event}: ${event} - (${arg ?? ''})`) }) }) this.io.adapter(createAdapter(this.pub!, this.sub!)) } } async onApplicationShutdown() { await this.pub?.quit() await this.sub?.quit() } handleConnection(socket: Socket) { this.logger.log(`connected: ${socket.id}`) this.logger.debug(`clients: ${this.io.sockets.sockets.size}`) } handleDisconnect(socket: Socket) { this.logger.log(`disconnected: ${socket.id}`) this.logger.debug(`clients: ${this.io.sockets.sockets.size}`) } }

    对于身份验证,请按以下方式处理:
import { Inject, Injectable, Logger } from '@nestjs/common' import { WebSocketGateway, type OnGatewayConnection, type OnGatewayDisconnect, } from '@nestjs/websockets' import type { Socket } from 'socket.io' @Injectable() @WebSocketGateway({ transports: ['websocket'] }) export class AuthGateway implements OnGatewayConnection, OnGatewayDisconnect { private readonly logger = new Logger(AuthGateway.name) constructor (private readonly config: ConfigService<AuthConfig>) {} handleConnection(socket: Socket) { try { const user = getAuth( socket.handshake.headers, this.config.get('JWT_PUBLIC_KEY'), ) // we use `socket.data` as a mean to store token Object.assign(socket.data, { user }) } catch (err) { this.logger.debug(err) socket.disconnect(true) } } handleDisconnect(socket: Socket) { // wiping token related data socket.data = {} } }
对于第二个,我首先成功地使用 

OnGatewayInit

allowRequest
 实现了它,如下所示:

class AuthGateway implements OnGatewayInit { @WebSocketServer() io: Server afterInit() { this.io.allowRequest = (req, cb) => { cb(null, true) // or cb(err, false) } } }
但是这种方法无法访问套接字,所以我做了 2 个 jwt 解密,这让我认为使用 

handleConnection

 方法更好。

正如我所说,这并不漂亮,但这会:

    避免需要
  1. app.resolve(SomeClass)
    ,在最奇特的情况下,这可能会导致循环依赖错误
  2. 允许拆分关注点,这在适配器中无法轻松完成(您可以在其中创建方法,但它不是密封的)
  3. 有利于 e2e 测试,因为您的测试应用程序样板不需要适配器的特定实例化(因为您没有适配器),并且所有内容都可以在您的
  4. AppModule
    中。
供参考,↑ 的笑话测试是:

describe(SocketIOServer.name, () => { let app: INestApplication let ref: TestingModule beforeAll(async () => { ref = await Test.createTestingModule({ imports: [AppModule], }).compile() app = ref.createNestApplication() await app.init() await app.listen(3000, '0.0.0.0') }) beforeEach(async () => { jest.restoreAllMocks() }) afterAll(async () => { await app.close() }) it('checks that the ws server is running', async () => { const gateway = app.get(SocketIOServer) // https://github.com/websockets/ws/blob/b73b11828d166e9692a9bffe9c01a7e93bab04a8/lib/websocket-server.js#L18 expect((gateway.io as any).eio.ws._state).toBe(0) // RUNNING }) })
    
© www.soinside.com 2019 - 2024. All rights reserved.