不使用 Cookie 的嵌入式功能

当 Looker 通过签名嵌入嵌入到 iframe 中时,某些浏览器默认采用会阻止第三方 Cookie 的 Cookie 政策。当嵌入的 iframe 从与加载嵌入应用的网域不同的网域加载时,系统会拒绝第三方 Cookie。您通常可以通过申请和使用个性化网域来解决此限制。不过,在某些情况下,无法使用个性化域名。在这些情况下,可以使用 Looker 无 Cookie 嵌入。

无 Cookie 嵌入的工作原理

如果未屏蔽第三方 Cookie,用户首次登录 Looker 时会创建一个会话 Cookie。此 Cookie 会随每个用户请求一起发送,Looker 服务器会使用它来确定发起请求的用户的身份。如果屏蔽了 Cookie,则不会随请求一起发送 Cookie,因此 Looker 服务器无法确定与请求关联的用户。

为解决此问题,Looker 无 Cookie 嵌入会将令牌与每个请求相关联,以便在 Looker 服务器中重新创建用户会话。嵌入式应用负责获取这些令牌,并使其可供在嵌入式 iframe 中运行的 Looker 实例使用。本文档的其余部分将介绍获取和提供这些令牌的过程。

如需使用任一 API,嵌入式应用必须能够通过管理员权限向 Looker API 进行身份验证。嵌入网域还必须列在嵌入网域许可名单中,或者,如果使用 Looker 23.8 或更高版本,则可以在获取无 Cookie 会话时包含嵌入网域。

创建 Looker 嵌入 iframe

以下序列图展示了嵌入 iframe 的创建过程。可能会同时生成多个 iframe,也可能会在未来的某个时间点生成多个 iframe。如果实现正确,iframe 将自动加入由第一个 iframe 创建的会话。Looker Embed SDK 会自动加入现有会话,从而简化此流程。

一个序列图,用于说明如何创建嵌入 iframe。

  1. 用户在嵌入式应用中执行操作,导致创建 Looker iframe。
  2. 嵌入式应用客户端获取 Looker 会话。Looker Embed SDK 可用于启动此会话,但必须提供端点网址或回调函数。如果使用回调函数,它将调用嵌入式应用服务器来获取 Looker 嵌入会话。否则,嵌入 SDK 将调用所提供的端点网址。
  3. 嵌入式应用服务器使用 Looker API 获取嵌入式会话。此 API 调用与 Looker 签名嵌入式签名流程类似,因为它接受嵌入式用户定义作为输入。如果调用用户已存在 Looker 嵌入式会话,则应在调用中包含关联的会话引用令牌。本文档的获取会话部分将对此进行更详细的说明。
  4. 获取嵌入会话端点处理与签名 /login/embed/(signed url) 端点类似,它需要将 Looker 嵌入用户定义作为请求正文,而不是在网址中。获取嵌入会话端点进程会验证嵌入用户,然后创建或更新该用户。它还可以接受现有会话引用令牌。这一点很重要,因为这样可以允许多个 Looker 嵌入式 iframe 共享同一会话。如果提供了会话引用令牌且会话未过期,则不会更新嵌入用户。这支持以下使用情形:一个 iframe 是使用签名嵌入网址创建的,而其他 iframe 是在没有签名嵌入网址的情况下创建的。在这种情况下,没有签名嵌入网址的 iframe 将继承第一个会话中的 Cookie。
  5. Looker API 调用会返回四个令牌,每个令牌都有一个存留时间 (TTL):
    • 授权令牌(TTL = 30 秒)
    • 导航令牌(TTL = 10 分钟)
    • API 令牌(TTL = 10 分钟)
    • 会话引用令牌(TTL = 会话的剩余生命周期)
  6. 嵌入式应用服务器必须跟踪 Looker 数据返回的数据,并将其与调用用户和调用用户浏览器的用户代理相关联。本文档的生成令牌部分提供了有关如何执行此操作的建议。此调用将返回授权令牌、导航令牌和 API 令牌,以及所有关联的 TTL。会话引用令牌应受到保护,并且不应在调用浏览器中公开。
  7. 将令牌返回给浏览器后,必须构建 Looker 嵌入登录网址。Looker Embed SDK 会自动构建嵌入登录网址。如需使用 windows.postMessage API 构建嵌入登录网址,请参阅本文档的使用 Looker windows.postMessage API 部分,查看相关示例。

    登录网址不包含已签名的嵌入用户详细信息。它包含目标 URI(包括导航令牌)和授权令牌(作为查询参数)。授权令牌必须在 30 秒内使用,并且只能使用一次。如果需要其他 iframe,则必须再次获取嵌入会话。不过,如果提供了会话引用令牌,则授权令牌将与同一会话相关联。

  8. Looker 嵌入登录端点会确定登录是否适用于无 Cookie 嵌入,这由授权令牌的存在与否来表示。如果授权令牌有效,则检查以下内容:

    • 关联的会话仍然有效。
    • 关联的嵌入用户仍然有效。
    • 与请求关联的浏览器用户代理与与会话关联的浏览器代理一致。
  9. 如果上一步中的检查通过,系统会使用网址中包含的目标 URI 重定向请求。此过程与 Looker 签名嵌入登录过程相同。

  10. 此请求是启动 Looker 信息中心的重定向。此请求将包含导航令牌作为参数。

  11. 在执行端点之前,Looker 服务器会在请求中查找导航令牌。如果服务器找到该令牌,则会检查以下内容:

    • 关联的会话仍然有效。
    • 与请求关联的浏览器用户代理与与会话关联的浏览器代理一致。

    如果有效,系统会为相应请求恢复会话,并运行信息中心请求。

  12. 用于加载信息中心的 HTML 会返回到 iframe。

  13. 在 iframe 中运行的 Looker 界面会确定该信息中心 HTML 是无 Cookie 的嵌入响应。此时,Looker 界面会向嵌入应用发送消息,请求在第 6 步中检索到的令牌。然后,该界面会等待,直到收到令牌。如果未收到令牌,系统会显示一条消息。

  14. 嵌入式应用将令牌发送到 Looker 嵌入式 iframe。

  15. 收到令牌后,在 iframe 中运行的 Looker 界面会开始渲染请求对象。在此过程中,界面将向 Looker 服务器发出 API 调用。在第 15 步中收到的 API 令牌会自动作为标头注入到所有 API 请求中。

  16. 在执行任何端点之前,Looker 服务器会在请求中查找 API 令牌。如果服务器找到该令牌,则会检查以下各项:

    • 关联的会话仍然有效。
    • 与请求关联的浏览器用户代理与与会话关联的浏览器代理一致。

    如果会话有效,系统会为请求恢复该会话,然后运行 API 请求。

  17. 返回信息中心数据。

  18. 系统会呈现信息中心。

  19. 用户可以控制信息中心。

生成新令牌

以下序列图说明了新令牌的生成。

说明生成新令牌的序列图。

  1. 在嵌入式 iframe 中运行的 Looker 界面会监控嵌入令牌的 TTL。
  2. 当令牌即将过期时,Looker 界面会向嵌入式应用客户端发送刷新令牌消息。
  3. 然后,嵌入式应用客户端会从嵌入式应用服务器中实现的端点请求新令牌。Looker Embed SDK 会自动请求新令牌,但必须提供端点网址或回调函数。如果使用回调函数,它将调用嵌入式应用服务器来生成新令牌。否则,嵌入 SDK 将调用所提供的端点网址。
  4. 嵌入式应用会查找与嵌入会话关联的 session_reference_tokenLooker Embed SDK Git 代码库中提供的示例使用会话 Cookie,但也可以使用分布式服务器端缓存(例如 Redis)。
  5. 嵌入式应用服务器调用 Looker 服务器,请求生成令牌。除了发起请求的浏览器的用户代理之外,此请求还需要最新的 API 和导航令牌。
  6. Looker 服务器会验证用户代理、会话引用令牌、导航令牌和 API 令牌。如果请求有效,系统会生成新令牌。
  7. 令牌会返回到调用嵌入应用服务器。
  8. 嵌入式应用服务器从响应中剥离会话引用令牌,并将剩余的响应返回给嵌入式应用客户端。
  9. 嵌入式应用客户端会将新生成的令牌发送到 Looker 界面。Looker Embed SDK 会自动执行此操作。使用 windows.postMessage API 的嵌入式应用客户端将负责发送令牌。Looker 界面收到令牌后,会在后续的 API 调用和网页导航中使用这些令牌。

实现 Looker 不使用 Cookie 的嵌入式功能

您可以使用 Looker Embed SDK 或 windows.postMessage API 来实现 Looker 无 Cookie 嵌入。您可以使用 Looker Embed SDK 方法,但我们还提供了一个示例,展示了如何使用 windows.postMessage API。如需详细了解这两种实现,请参阅 Looker Embed SDK 自述文件Embed SDK Git 代码库还包含可正常运行的实现。

配置 Looker 实例

不使用 Cookie 的嵌入式功能与 Looker 签名嵌入具有相似的特征。

前提条件

如需使用无 Cookie 嵌入并公开无 Cookie 嵌入 API 端点,Looker 管理员必须在 Looker 的管理面板中验证并启用以下要求:

  • 嵌入式单点登录身份验证:在管理 > 平台 > 嵌入面板中,启用嵌入式单点登录身份验证。无 Cookie 嵌入依赖于基本的嵌入式单点登录身份验证功能。
  • 持久会话:在管理 > 身份验证 > 会话面板中,启用持久会话
  • API 用户权限:确保用于获取令牌的 API 凭据属于具有管理员角色或包含 manage_embed_settings 权限的自定义角色的 Looker 用户。

配置 JSON Web 令牌 Secret

与 Looker 签名嵌入不同,无 Cookie 嵌入不使用嵌入密钥设置。无 Cookie 嵌入使用 JSON Web 令牌 (JWT) 形式的嵌入 JWT 密钥设置,该设置可在管理菜单的平台部分中的嵌入页面上进行设置或重置。

设置 JWT Secret 是需要的,因为首次尝试创建无 Cookie 嵌入会话时会创建 JWT。请避免重置此令牌,因为这样做会使所有有效的无 Cookie 嵌入会话失效。

与嵌入密钥不同,嵌入 JWT 密钥永远不会公开,因为它仅在 Looker 服务器内部使用。

应用客户端实现

本部分包含有关如何在应用客户端中实现无 Cookie 嵌入的示例,并包含以下子部分:

安装或更新 Looker Embed SDK

如需使用无 Cookie 嵌入,必须使用以下 Looker SDK 版本:

@looker/embed-sdk >= 2.0.0
@looker/sdk >= 22.16.0

使用 Looker Embed SDK

Embed SDK 中新增了一种初始化方法,用于启动无 Cookie 会话。此方法接受两个网址字符串或两个回调函数。网址字符串应引用嵌入式应用服务器中的端点。有关应用服务器上这些端点的实现细节,请参阅本文档的应用服务器实现部分。

getEmbedSDK().initCookieless(
  runtimeConfig.lookerHost,
  '/acquire-embed-session',
  '/generate-embed-tokens'
)

以下示例展示了如何使用回调。只有在嵌入客户端应用需要了解 Looker 嵌入会话的状态时,才应使用回调。您还可以使用 session:status 事件,这样就不必将回调与 Embed SDK 搭配使用。

const acquireEmbedSessionCallback =
  async (): Promise<LookerEmbedCookielessSessionData> => {
    const resp = await fetch('/acquire-embed-session')
    if (!resp.ok) {
      console.error('acquire-embed-session failed', { resp })
      throw new Error(
        `acquire-embed-session failed: ${resp.status} ${resp.statusText}`
      )
    }
    return (await resp.json()) as LookerEmbedCookielessSessionData
  }

const generateEmbedTokensCallback =
  async ({ api_token, navigation_token }): Promise<LookerEmbedCookielessSessionData> => {
    const resp = await fetch('/generate-embed-tokens', {
      method: 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ api_token, navigation_token }),
    })
    if (!resp.ok) {
      console.error('generate-embed-tokens failed', { resp })
      throw new Error(
        `generate-embed-tokens failed: ${resp.status} ${resp.statusText}`
      )
    }
    return (await resp.json()) as LookerEmbedCookielessSessionData
  }

getEmbedSDK().initCookieless(
  runtimeConfig.lookerHost,
  acquireEmbedSessionCallback,
  generateEmbedTokensCallback
)

使用 Looker windows.postMessage API

您可以在 Embed SDK Git 代码库的 message_example.tsmessage_utils.ts 文件中查看使用 windows.postMessage API 的详细示例。此处详细介绍了该示例的亮点。

以下示例演示了如何构建 iframe 的网址。回调函数与之前看到的 acquireEmbedSessionCallback 示例相同。

  private async getCookielessLoginUrl(): Promise<string> {
    const { authentication_token, navigation_token } =
      await this.embedEnvironment.acquireSession()
    const url = this.embedUrl.startsWith('/embed')
      ? this.embedUrl
      : `/embed${this.embedUrl}`
    const embedUrl = new URL(url, this.frameOrigin)
    if (!embedUrl.searchParams.has('embed_domain')) {
      embedUrl.searchParams.set('embed_domain', window.location.origin)
    }
    embedUrl.searchParams.set('embed_navigation_token', navigation_token)
    const targetUri = encodeURIComponent(
      `${embedUrl.pathname}${embedUrl.search}${embedUrl.hash}`
    )
    return `${embedUrl.origin}/login/embed/${targetUri}?embed_authentication_token=${authentication_token}`
  }

以下示例演示了如何监听令牌请求、生成新令牌并将其发送给 Looker。回调函数与之前的 generateEmbedTokensCallback 示例相同。

      this.on(
        'session:tokens:request',
        this.sessionTokensRequestHandler.bind(this)
      )

  private connected = false

  private async sessionTokensRequestHandler(_data: any) {
    const contentWindow = this.getContentWindow()
    if (contentWindow) {
      if (!this.connected) {
        // When not connected the newly acquired tokens can be used.
        const sessionTokens = this.embedEnvironment.applicationTokens
        if (sessionTokens) {
          this.connected = true
          this.send('session:tokens', this.embedEnvironment.applicationTokens)
        }
      } else {
        // If connected, the embedded Looker application has decided that
        // it needs new tokens. Generate new tokens.
        const sessionTokens = await this.embedEnvironment.generateTokens()
        this.send('session:tokens', sessionTokens)
      }
    }
  }

  send(messageType: string, data: any = {}) {
    const contentWindow = this.getContentWindow()
    if (contentWindow) {
      const message: any = {
        type: messageType,
        ...data,
      }
      contentWindow.postMessage(JSON.stringify(message), this.frameOrigin)
    }
    return this
  }

应用服务器实现

本部分包含有关如何在应用服务器中实现无 Cookie 嵌入的示例,并包含以下子部分:

基本实现

嵌入式应用需要实现两个将调用 Looker 端点的服务器端点。这是为了确保会话引用令牌保持安全。这两个端点如下:

  1. 获取会话 - 如果会话引用令牌已存在且仍处于有效状态,则对会话的请求将加入现有会话。创建 iframe 时会调用获取会话。
  2. 生成令牌 - Looker 会定期触发对该端点的调用。

获取会话

此 TypeScript 示例使用会话来保存或恢复会话引用令牌。端点不必以 TypeScript 实现。

  app.get(
    '/acquire-embed-session',
    async function (req: Request, res: Response) {
      try {
        const current_session_reference_token =
          req.session && req.session.session_reference_token
        const response = await acquireEmbedSession(
          req.headers['user-agent']!,
          user,
          current_session_reference_token
        )
        const {
          authentication_token,
          authentication_token_ttl,
          navigation_token,
          navigation_token_ttl,
          session_reference_token,
          session_reference_token_ttl,
          api_token,
          api_token_ttl,
        } = response
        req.session!.session_reference_token = session_reference_token
        res.json({
          api_token,
          api_token_ttl,
          authentication_token,
          authentication_token_ttl,
          navigation_token,
          navigation_token_ttl,
          session_reference_token_ttl,
        })
      } catch (err: any) {
        res.status(400).send({ message: err.message })
      }
    }
  )

async function acquireEmbedSession(
  userAgent: string,
  user: LookerEmbedUser,
  session_reference_token: string
) {
  await acquireLookerSession()
    try {
    const request = {
      ...user,
      session_reference_token: session_reference_token,
    }
    const sdk = new Looker40SDK(lookerSession)
    const response = await sdk.ok(
      sdk.acquire_embed_cookieless_session(request, {
        headers: {
          'User-Agent': userAgent,
        },
      })
    )
    return response
  } catch (error) {
    console.error('embed session acquire failed', { error })
    throw error
  }
}

从 Looker 23.8 开始,获取无 Cookie 会话时可以包含嵌入网域。这是使用 Looker 管理 > 嵌入 面板添加嵌入网域的替代方法。Looker 会将嵌入网域保存在 Looker 内部数据库中,因此不会显示在管理 > 嵌入面板上。相反,嵌入网域会与无 Cookie 会话相关联,并且仅在会话期间存在。如果您决定利用此功能,请查看安全最佳实践

生成令牌

此 TypeScript 示例使用会话来保存或恢复会话引用令牌。端点不必以 TypeScript 实现。

请务必了解如何处理令牌无效时出现的 400 响应。虽然不应返回 400 响应,但如果返回了,最佳做法是终止 Looker 嵌入会话。您可以通过销毁嵌入 iframe 或在 session:tokens 消息中将 session_reference_token_ttl 值设置为零来终止 Looker 嵌入会话。如果您将 session_reference_token_ttl 值设置为零,Looker iframe 会显示会话过期对话框。

当嵌入会话过期时,不会返回 400 响应。如果嵌入会话已过期,则会返回 200 响应,并将 session_reference_token_ttl 值设置为零。

  app.put(
    '/generate-embed-tokens',
    async function (req: Request, res: Response) {
      try {
        const session_reference_token = req.session!.session_reference_token
        const { api_token, navigation_token } = req.body as any
        const tokens = await generateEmbedTokens(
          req.headers['user-agent']!,
          session_reference_token,
          api_token,
          navigation_token
        )
        res.json(tokens)
      } catch (err: any) {
        res.status(400).send({ message: err.message })
      }
    }
  )
}
async function generateEmbedTokens(
  userAgent: string,
  session_reference_token: string,
  api_token: string,
  navigation_token: string
) {
  if (!session_reference_token) {
    console.error('embed session generate tokens failed')
    // missing session reference  treat as expired session
    return {
      session_reference_token_ttl: 0,
    }
  }
  await acquireLookerSession()
  try {
    const sdk = new Looker40SDK(lookerSession)
    const response = await sdk.ok(
      sdk.generate_tokens_for_cookieless_session(
        {
          api_token,
          navigation_token,
          session_reference_token: session_reference_token || '',
        },
        {
          headers: {
            'User-Agent': userAgent,
          },
        }
      )
    )
    return {
      api_token: response.api_token,
      api_token_ttl: response.api_token_ttl,
      navigation_token: response.navigation_token,
      navigation_token_ttl: response.navigation_token_ttl,
      session_reference_token_ttl: response.session_reference_token_ttl,
    }
  } catch (error: any) {
    if (error.message?.includes('Invalid input tokens provided')) {
      // The Looker UI does not know how to handle bad
      // tokens. This shouldn't happen but if it does expire the
      // session. If the token is bad there is not much that that
      // the Looker UI can do.
      return {
        session_reference_token_ttl: 0,
      }
    }
    console.error('embed session generate tokens failed', { error })
    throw error
  }

实现方面的注意事项

嵌入式应用必须跟踪会话引用令牌,并确保其安全性。此令牌应与嵌入式应用用户相关联。嵌入式应用令牌可以通过以下任一方式存储:

  • 在嵌入式应用的用户会话中
  • 在集群环境中可用的服务器端缓存中
  • 在与用户关联的数据库表中

如果将会话存储为 Cookie,则应加密该 Cookie。嵌入式 SDK 代码库中的示例使用会话 Cookie 来存储会话引用令牌。

当 Looker 嵌入会话过期时,嵌入的 iframe 中会显示一个对话框。此时,用户将无法在嵌入的实例中执行任何操作。发生这种情况时,系统会生成 session:status 事件,从而使嵌入应用能够检测嵌入的 Looker 应用的当前状态并采取某种操作。

嵌入式应用可以通过检查 generate_tokens 端点返回的 session_reference_token_ttl 值是否为零来检测嵌入会话是否已过期。如果该值为零,则表示嵌入会话已过期。请考虑在无 Cookie 嵌入初始化时使用回调函数来生成令牌。然后,回调函数可以确定嵌入会话是否已过期,并销毁嵌入式 iframe,而不是使用默认的嵌入会话过期对话框。

运行 Looker 无 Cookie 嵌入示例

嵌入 SDK 代码库包含一个用 TypeScript 编写的 Node Express 服务器和客户端,用于实现嵌入式应用。前面显示的示例就是基于此实现。以下内容假设您的 Looker 实例已配置为使用无 Cookie 嵌入(如前所述)。

您可以按如下方式运行服务器:

  1. 克隆 Embed SDK 代码库 - git clone git@github.com:looker-open-source/embed-sdk.git
  2. 更改目录 - cd embed-sdk
  3. 安装依赖项 - npm install
  4. 配置服务器,如本文档的配置服务器部分所示。
  5. 运行服务器 - npm run server

配置服务器

在克隆的代码库的根目录中创建一个 .env 文件(此文件包含在 .gitignore 中)。

其格式如下所示:

LOOKER_WEB_URL=your-looker-instance-url.com
LOOKER_API_URL=https://your-looker-instance-url.com
LOOKER_DEMO_HOST=localhost
LOOKER_DEMO_PORT=8080
LOOKER_EMBED_SECRET=embed-secret-from-embed-admin-page
LOOKER_CLIENT_ID=client-id-from-user-admin-page
LOOKER_CLIENT_SECRET=client-secret-from-user-admin-page
LOOKER_DASHBOARD_ID=id-of-dashboard
LOOKER_LOOK_ID=id-of-look
LOOKER_EXPLORE_ID=id-of-explore
LOOKER_EXTENSION_ID=id-of-extension
LOOKER_VERIFY_SSL=true
LOOKER_REPORT_ID=id-of-report
LOOKER_QUERY_VISUALIZATION_ID=id-of-query-visualization

使用 Looker 无 Cookie 嵌入时,尝试在新浏览器标签页或窗口中打开链接(例如信息中心下钻、探索或文档下载)的用户可能会遇到 HTTP 401 Unauthorized 错误或被重定向到 Looker 登录页面。此问题最常在浏览器中停用第三方 Cookie 时出现。

原因

为防止会话劫持漏洞,Looker(版本 25.18.61 及更高版本、25.20.44 及更高版本,以及 26.0.22 及更高版本)引入了一项安全补丁,该补丁有意停止在初始无 Cookie 嵌入响应中返回会话 Cookie。由于浏览器不再接收此会话 Cookie,因此在新标签页或窗口中打开 Looker 网址会丢失身份验证上下文,导致请求失败。

分辨率和解决方法

  • 升级 Looker:将 Looker 实例升级到 26.0.36 版或更高版本。此版本引入了一项修复,可在无 Cookie 嵌入会话期间安全地向在新窗口中打开的网址添加一次性令牌,从而恢复用户在新标签页中打开安全链接的功能。
  • 在同一 iframe 中打开链接:如果无法立即升级,请将嵌入式信息中心内的所有链接配置为在同一 iframe 中打开,方法是将链接目标设为 _self(例如 target="_self")。
  • 通过宿主应用进行路由:如果必须在新标签页或窗口中打开,请将链接定位到宿主应用内的新网页,该网页会初始化新的无 Cookie 嵌入 iframe,而不是直接链接到 Looker 实例。
  • 使用自定义网域进行签名嵌入:或者,考虑使用自定义网域进行标准签名嵌入