我正在尝试在 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()
方法使用该服务吗?
我可以使用
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
未定义的问题。
它并不漂亮,但是我发现
socket.io
具有足够的弹性,可以在listen
阶段之后热插拔其大部分配置,这样我们就不需要适配器。知道这一点后,我将网关视为实现
OnGatewayInit, OnApplicationShutdown
的中间件,如下所示:
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
方法更好。正如我所说,这并不漂亮,但这会:
app.resolve(SomeClass)
,在最奇特的情况下,这可能会导致循环依赖错误
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
})
})