Guard在day08,有初步介紹過,Guard機制不只可以用在Route上,也可以用在WebSocket Gateway上。
我們先實現Route Guard複習一下,在本篇會使用session存放user object,裏頭key有account、roles,讓route guard機制去驗證,然後也新增一個頁面作為加入聊天室使用,要訪問聊天室網頁必須先通過route guard機制。
npm install --save express-session @types/express-session
//使用session
instance.use(session({
secret: 'nestjs session',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}))
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<label>加入聊天室<label>
<form method="POST" action="/addInChatRoom">
<label>名字:<label>
<input name="Account">
<input type="submit" value="加入">
</form>
</body>
</html>
src/view/Chat/chatRoom.ejs
script部分
<script>
const socket = io('ws://localhost:81/messages');
$('#chatForm').submit(function(){
//推訊息
let Message=getCookie('name')+":"+$('#chatMessage').val();
console.log(Message);
socket.emit('pushMessage', Message);
$('#chatMessage').val('');
return false;
});
//監聽新訊息事件
socket.on('newMessage', function(msg){
//顯示訊息
$('#messages').append($('<li>').text(msg));
window.scrollTo(0, document.body.scrollHeight);
});
//監聽連線事件
socket.on('connect', function() {
console.log('Connected');
});
//監聽斷線事件
socket.on('disconnect', function() {
console.log('Disconnected');
});
//取得cookie 值
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
</script>
import { Controller, Get, Post, Request, Response, Next, HttpStatus, UseGuards } from '@nestjs/common';
import { RolesGuard } from '../Shared/Guards/roles.guard';
import { Roles } from '../Shared/decorators/roles.decorator';
@Controller()
@UseGuards(RolesGuard)
export class ChatController {
constructor() { }
@Get('toAddInChatRoom')
//使用Express的參數
async toAddInChatRoom( @Request() req, @Response() res, @Next() next) {
//跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
res.render('./Chat/toAddInChatRoom', { title: "加入聊天室" });
}
@Post('addInChatRoom')
//使用Express的參數
async addInChatRoom( @Request() req, @Response() res, @Next() next) {
/*
1.以下是要建立路由警衛機制,刻意給每個有輸入名稱的使用者一個role,
並且為了在聊天室顯示使用者名稱,透過cookie存放輸入名稱,這邊只是為了demo,
實際存放名稱要考量更多資安問題。
2.我們透過session存放account和role,在Guard機制裏頭,我們會去比對role。
*/
let tmpAccount: string = req.body.Account;
req.session.user={};
req.session.user.account = tmpAccount;
if (tmpAccount) {
//role給予general角色
req.session.user.roles = ["general"];
//將名稱存放前端cookie,推播訊息時,前端會抓取名稱加上訊息再做推播。
res.cookie('name', `${tmpAccount}`);
}
//跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
res.redirect('/chatRoom');
}
@Get('chatRoom')
@Roles('general')
//使用Express的參數
async chatRoom( @Request() req, @Response() res, @Next() next) {
//跟expressjs專案一樣,指定view路徑,後面帶變數可以直接render到view上
res.render('./Chat/chatRoom', { title: "聊天室" });
}
}
import { WebSocketGateway, SubscribeMessage, WsResponse, WebSocketServer, WsException, NestGateway } from '@nestjs/websockets';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import { CreateUserDTO } from '../Users/DTO/create-users.dto';
import Socket = SocketIO.Socket;
//WebSocket listen port 81,namespace:messages
@WebSocketGateway({ port: 81, namespace: 'messages' })
export class ChatGateway implements NestGateway {
//使用Socket.IO的API
socket: Socket;
constructor() { }
afterInit(server) { }
handleConnection(socket) { }
handleDisconnect(socket) { }
//新增訊息
@SubscribeMessage({ value: 'pushMessage' })
AddMessage(sender, message: string) {
//推訊息給自己的前端畫面。
let tmpMessage: string = `${message}`;
sender.emit('newMessage', tmpMessage);
//推訊息給其他已建立連線的前端畫面。
sender.broadcast.emit('newMessage', tmpMessage);
}
}
req.user = { "account": "Ted", "roles": ["general"] };
來實驗我們的Route Guard機制,現在改用session裏頭的user object資料做測試。
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { Reflector } from '@nestjs/core';
@Guard()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) { }
canActivate(req, context: ExecutionContext): boolean {
const { parent, handler } = context;
const roles = this.reflector.get<string[]>('roles', handler);
if (!roles) {
return true;
}
/*req.user是假資料,這是在模擬登入後,有一組user資訊放在req object裡,
也可以放在session等,登入資訊的roles表示角色權限,是陣列,一個帳號可能有多個角色。
而Ted的角色是general,能夠請求通過帶有@Roles('general')裝飾器的目標。
*/
//req.user = { "account": "Ted", "roles": ["general"] };
const user = req.session.user;
const hasRole = () => !!user.roles.find((role) => !!roles.find((item) => item === role));
return user && user.roles && hasRole();
}
}
符合我們的預期,要直接看到聊天室必須先到加入聊天室的網頁,加入後才能看到。
Route Guard有發揮功用。
9.來玩玩WebSocket Gateway上的Guard機制,新增websocket.roles.guard.ts。
src/modules/Shared/Guards/websocket.roles.guard.ts
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { Reflector } from '@nestjs/core';
@Guard()
export class WebSocketRolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) { }
canActivate(data, context: ExecutionContext): boolean {
const { parent, handler } = context;
const roles = this.reflector.get<string[]>('roles', handler);
if (!roles) {
return true;
}
/*
跟Route Guard有點不一樣,req改成data,data泛指傳遞過來的訊息。
為了 demo用,data是前端push過來的訊息,裏頭有roles 陣列,放著該前端使用者的角色。
*/
const hasRole = () => !!data.roles.find((role) => !!roles.find((item) => item === role));
return data && data.roles && hasRole();
}
}
import { WebSocketGateway, SubscribeMessage, WsResponse, WebSocketServer, WsException, NestGateway } from '@nestjs/websockets';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import { CreateUserDTO } from '../Users/DTO/create-users.dto';
import Socket = SocketIO.Socket;
import { UseGuards } from '@nestjs/common';
import { Roles } from '../Shared/decorators/roles.decorator';
import { WebSocketRolesGuard } from '../Shared/Guards/webSocket.roles.guard';
//WebSocket listen port 81,namespace:messages
@WebSocketGateway({ port: 81, namespace: 'messages' })
//Gateway Guard
@UseGuards(WebSocketRolesGuard)
export class ChatGateway implements NestGateway {
//使用Socket.IO的API
socket: Socket;
constructor() { }
afterInit(server) { }
handleConnection(socket) { }
handleDisconnect(socket) { }
//新增訊息
@SubscribeMessage({ value: 'pushMessage' })
@Roles('general')
AddMessage(sender, message: object) {
//推訊息給自己的前端畫面。
sender.emit('newMessage', message);
//推訊息給其他已建立連線的前端畫面。
sender.broadcast.emit('newMessage', message);
}
}
<script>
const socket = io('ws://localhost:81/messages');
$('#chatForm').submit(function(){
//推訊息
var data={};
var Message=getCookie('name')+":"+$('#chatMessage').val();
//demo Gateway Guard機制,直接給定角色
data.roles=['general'];
data.message=Message;
socket.emit('pushMessage', data);
$('#chatMessage').val('');
return false;
});
//監聽新訊息事件
socket.on('newMessage', function(msg){
//顯示訊息
$('#messages').append($('<li>').text(msg.message));
window.scrollTo(0, document.body.scrollHeight);
});
//監聽連線事件
socket.on('connect', function() {
console.log('Connected');
});
//監聽斷線事件
socket.on('disconnect', function() {
console.log('Disconnected');
});
//取得cookie 值
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
</script>
推播訊息原本是string改成object,讓Gateway Guard方便抓角色屬性的值。
12.1 測試一下Gateway Guard,在http://localhost:3000/chatRoom ,輸入訊息,有正常顯示訊息。
12.2 接著將chatRoom.ejs網頁的data.roles=['general'];
改成data.roles=['wrong role'];
,在http://localhost:3000/chatRoom ,網頁重新整理後,再一次輸入訊息就無法顯示訊息了。
Gateway Guard有正常發揮功用,對整體網站的保護又多一層防範。Nestjs的Exception Filters、Pipes、Guards、Interceptors、Adapter都能套用在WebSocket的Gateway上,不過我沒太多時間一一去實現demo,這點抱歉了,日後可以繼續關注我的github,我會主攻Nestjs,將Nestjs所有API都玩遍。
程式碼都在github