前后端分离架构下Azure AD SAML SSO前端如何获取验证完成通知?
前后端分离下Azure AD SAML SSO的Token传递方案
针对React+NestJS分离架构下的SAML集成问题,以下是几种成熟的实现方案,按推荐程度排序:
方案一:弹出窗口跨页面通信(最常用)
利用浏览器postMessage API实现登录窗口与主应用窗口的直接通信,步骤如下:
前端登录逻辑:
- 用户点击登录按钮时,打开新窗口指向Azure AD登录地址(可由NestJS生成SAML请求后返回跳转URL,前端直接调用)。
- 主窗口监听
message事件,严格验证消息来源合法性:// React主应用代码 useEffect(() => { const handleMessage = (event) => { // 仅接收信任来源的消息,防止恶意攻击 if (event.origin !== process.env.REACT_APP_BASE_URL) return; if (event.data.type === 'SAML_LOGIN_SUCCESS') { const { token } = event.data; // 存储Token到localStorage或全局状态 localStorage.setItem('authToken', token); // 刷新页面或跳转到首页 window.location.reload(); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []);
NestJS处理SAML响应:
- 接收Azure AD POST的SAML响应,完成签名验证、断言解析等逻辑,生成业务Token(如JWT)。
- 返回内嵌JavaScript的HTML页面,通过
window.opener.postMessage将Token发送给主窗口后关闭当前窗口:// NestJS的SAML回调控制器 @Post('/saml') async samlCallback(@Body() samlResponse: any, @Res() res: Response) { // 验证SAML响应并生成Token const token = await this.authService.generateJwt(); // 返回用于跨窗口通信的HTML页面 const html = ` <!DOCTYPE html> <html> <body> <script> window.opener.postMessage( { type: 'SAML_LOGIN_SUCCESS', token: '${token}' }, '${process.env.FRONTEND_BASE_URL}' ); window.close(); </script> </body> </html> `; res.send(html); }
优点:实现简单、实时性好,无额外依赖;缺点:需确保浏览器允许弹出窗口,且postMessage的来源验证必须严格。
方案二:HttpOnly Cookie + 前端轮询
通过HttpOnly Cookie传递Refresh Token,结合前端轮询状态接口完成登录流程:
NestJS处理逻辑:
- 验证SAML响应后,生成Access Token和Refresh Token,将Refresh Token设置为HttpOnly、Secure、SameSite属性的Cookie(跨域场景需配置CORS允许携带Cookie)。
- 返回提示登录成功的HTML页面,自动关闭窗口或引导用户返回主应用:
@Post('/saml') async samlCallback(@Body() samlResponse: any, @Res() res: Response) { const { accessToken, refreshToken } = await this.authService.generateTokens(); // 设置HttpOnly Cookie,防止XSS窃取 res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'none', // 跨域场景需设置为none,同时必须开启secure maxAge: 7 * 24 * 60 * 60 * 1000 // 有效期7天 }); // 返回自动关闭的提示页面 const html = ` <!DOCTYPE html> <html> <body> <p>登录成功,即将关闭窗口...</p> <script>setTimeout(() => window.close(), 1000)</script> </body> </html> `; res.send(html); }
前端轮询逻辑:
- 打开登录窗口后,前端定时调用后端状态接口(如
/auth/saml/status),该接口检查Cookie中的Refresh Token,返回有效的Access Token:// React主应用代码 const handleLogin = () => { window.open('/api/saml/login', '_blank', 'width=600,height=600'); // 轮询状态接口,检查登录是否完成 const interval = setInterval(async () => { try { const res = await fetch('/api/auth/saml/status', { credentials: 'include' // 携带Cookie }); if (res.ok) { const { accessToken } = await res.json(); localStorage.setItem('authToken', accessToken); window.location.reload(); clearInterval(interval); } } catch (err) { // 未登录成功,继续轮询 } }, 1000); };
- 打开登录窗口后,前端定时调用后端状态接口(如
优点:安全性高(HttpOnly Cookie避免XSS窃取);缺点:轮询会产生额外请求,跨域Cookie配置需注意浏览器兼容性。
方案三:WebSocket实时推送(你考虑的方案)
通过WebSocket连接实现后端主动向前端推送Token,适合对实时性要求高的场景:
前端WebSocket连接:
- 打开登录窗口前,建立WebSocket连接到NestJS,生成临时会话ID(如UUID)并传递给登录窗口:
// React主应用代码 const handleLogin = () => { const sessionId = crypto.randomUUID(); const ws = new WebSocket(`ws://localhost:3000/ws/saml/${sessionId}`); ws.onmessage = (event) => { const { token } = JSON.parse(event.data); localStorage.setItem('authToken', token); window.location.reload(); ws.close(); }; // 打开登录窗口并携带sessionId window.open(`/api/saml/login?sessionId=${sessionId}`, '_blank', 'width=600,height=600'); };
- 打开登录窗口前,建立WebSocket连接到NestJS,生成临时会话ID(如UUID)并传递给登录窗口:
NestJS WebSocket与SAML处理:
- 实现WebSocket网关,维护sessionId与WebSocket连接的映射:
// NestJS WebSocket网关 @WebSocketGateway() export class SamlWsGateway { private connections = new Map<string, WebSocket>(); @SubscribeMessage('connect') handleConnect(client: WebSocket, payload: { sessionId: string }) { this.connections.set(payload.sessionId, client); } // 供SAML控制器调用的Token推送方法 sendToken(sessionId: string, token: string) { const client = this.connections.get(sessionId); if (client) { client.send(JSON.stringify({ token })); this.connections.delete(sessionId); } } } - SAML回调控制器中调用网关推送Token:
@Post('/saml') async samlCallback(@Body() samlResponse: any, @Query('sessionId') sessionId: string, @Res() res: Response) { const token = await this.authService.generateJwt(); this.samlWsGateway.sendToken(sessionId, token); // 返回关闭窗口的页面 res.send('<script>window.close()</script>'); }
- 实现WebSocket网关,维护sessionId与WebSocket连接的映射:
优点:实时性强,无需轮询;缺点:实现复杂度高,需处理WebSocket连接断开、重连等异常,服务器需维护连接映射。
内容的提问来源于stack exchange,提问作者prof chaos
相关产品推荐
相关产品推荐

