You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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-ancestors CSP配置(因为使用弹出窗口而非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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:24:54