如何在OfficeJS Outlook Web Add-in中基于Keycloak实现OpenId认证调用API
在Office Web Add-In中基于Keycloak实现OpenID认证的解决方案
由于Office Web Add-In的运行环境限制(网页版的iframe CSP约束、桌面版的浏览器API支持限制),直接使用Keycloak JS适配器无法正常工作,推荐采用Office Dialog API + Keycloak授权码流程的方案,具体实现步骤如下:
1. 配置Keycloak客户端
- 将客户端类型设置为Public(Office插件属于前端应用,无后端存储Client Secret)
- 开启Authorization Code Flow Enabled,关闭Implicit Flow Enabled
- 在Valid Redirect URIs中添加插件的回调页面地址,例如:
https://your-add-in-domain/auth-callback.html - 无需修改Keycloak的
frame-ancestorsCSP配置(因为使用弹出窗口而非iframe加载登录页)
2. 实现认证流程(基于Office Dialog API)
插件主页面(例如taskpane.js)
负责触发认证流程、接收回调消息、存储Token并调用Web API:
async function initAuth() { // 检查本地存储的有效Token const storedToken = Office.context.roamingSettings.get('access_token'); const tokenExpiry = Office.context.roamingSettings.get('token_expiry'); if (storedToken && tokenExpiry > Date.now()) { useToken(storedToken); return; } // 构造Keycloak授权URL const authUrl = `${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/auth` + `?client_id=${CLIENT_ID}` + `&redirect_uri=${ENCODED_REDIRECT_URI}` + '&response_type=code' + '&scope=openid email profile'; // 弹出认证窗口 Office.context.ui.displayDialogAsync(authUrl, { height: 50, width: 40 }, asyncResult => { const dialog = asyncResult.value; // 监听回调页面的消息 dialog.addEventHandler(Office.EventType.DialogMessageReceived, args => { const message = JSON.parse(args.message); if (message.type === 'auth_success') { // 存储Token和过期时间(roamingSettings为加密存储) Office.context.roamingSettings.set('access_token', message.access_token); Office.context.roamingSettings.set('token_expiry', Date.now() + (message.expires_in * 1000)); Office.context.roamingSettings.saveAsync(); dialog.close(); useToken(message.access_token); } else if (message.type === 'auth_error') { console.error('认证失败:', message.error); dialog.close(); } }); // 处理窗口异常关闭 dialog.addEventHandler(Office.EventType.DialogEventReceived, args => { if (args.error) console.error('窗口出错:', args.error); }); }); } function useToken(token) { // 调用你的Web API fetch('https://your-web-api-domain/api/mail-integration', { headers: { 'Authorization': `Bearer ${token}` } }) .then(res => res.json()) .then(data => { // 处理API返回数据 console.log('API响应:', data); }) .catch(err => console.error('API调用失败:', err)); } // 插件初始化时触发认证 Office.onReady().then(initAuth); // 替换为你的Keycloak配置常量 const KEYCLOAK_URL = 'https://localhost:8443/auth'; const REALM = 'DevTeamRealm'; const CLIENT_ID = 'devteam-client'; const ENCODED_REDIRECT_URI = encodeURIComponent('https://your-add-in-domain/auth-callback.html');
认证回调页面(auth-callback.html)
负责接收授权码、交换Token并将结果传递回插件主页面:
<!DOCTYPE html> <html> <head> <script> window.onload = async () => { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const error = urlParams.get('error'); if (error) { Office.context.ui.messageParent(JSON.stringify({ type: 'auth_error', error: error })); return; } if (code) { try { // 交换授权码获取Token const res = await fetch(`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, grant_type: 'authorization_code', code: code }) }); const tokenData = await res.json(); if (tokenData.access_token) { Office.context.ui.messageParent(JSON.stringify({ type: 'auth_success', access_token: tokenData.access_token, expires_in: tokenData.expires_in })); } else { Office.context.ui.messageParent(JSON.stringify({ type: 'auth_error', error: tokenData.error_description || '获取Token失败' })); } } catch (err) { Office.context.ui.messageParent(JSON.stringify({ type: 'auth_error', error: err.message })); } } }; // 替换为你的配置常量 const KEYCLOAK_URL = 'https://localhost:8443/auth'; const REALM = 'DevTeamRealm'; const CLIENT_ID = 'devteam-client'; const REDIRECT_URI = 'https://your-add-in-domain/auth-callback.html'; </script> </head> <body></body> </html>
3. 补充优化建议
- Token刷新:当Token过期时,使用Keycloak的Refresh Token流程(无需用户交互)获取新的Access Token,避免重复触发登录窗口
- 本地开发调试:使用ngrok等工具将本地插件服务器暴露为HTTPS地址,满足Office插件的安全要求
- 安全防护:确保回调页面仅处理认证逻辑,避免引入不必要的脚本;限制roamingSettings中存储的Token过期时间,降低安全风险
内容的提问来源于stack exchange,提问作者NatsuDragonEye
相关产品推荐
相关产品推荐

