無 Cookie 嵌入

當 Looker 使用已簽署的嵌入功能嵌入 iframe 時,部分瀏覽器會預設為採用 Cookie 政策,禁止第三方 Cookie。如果嵌入的 iframe 是從不同網域載入,而該網域並未載入嵌入應用程式,系統就會拒絕第三方 Cookie。一般來說,您可以要求並使用虛構網域來解決這項限制。不過,在某些情況下,您無法使用自訂網域。在這些情況下,您可以使用 Looker 無 Cookie 嵌入功能。

無 Cookie 嵌入功能的運作方式為何?

如果未封鎖第三方 Cookie,系統會在使用者首次登入 Looker 時建立工作階段 Cookie。這個 Cookie 會隨每個使用者要求傳送,Looker 伺服器會使用該 Cookie 建立提出要求的使用者身分。封鎖 Cookie 後,系統就不會在要求中傳送 Cookie,因此 Looker 伺服器無法識別與要求相關聯的使用者。

為解決這個問題,Looker 無 cookie 嵌入功能會將權杖與每個要求建立關聯,這些權杖可用於在 Looker 伺服器中重建使用者工作階段。嵌入應用程式負責取得這些符記,並將這些符記提供給在嵌入式 iframe 中執行的 Looker 例項。本文件的其餘部分會說明取得和提供這些符記的程序。

如要使用這兩種 API,嵌入應用程式必須能夠以管理員權限驗證 Looker API。嵌入網域也必須列在嵌入網域許可清單中,或是在使用 Looker 23.8 以上版本時,在取得無 Cookie 工作階段時加入嵌入網域。

建立 Looker 嵌入 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 UI 會判斷資訊主頁 HTML 是否為無 Cookie 嵌入回應。此時,Looker UI 會傳送訊息給嵌入應用程式,要求在步驟 6 中擷取的符記。接著,UI 會等到收到符記。如果代碼未到達,系統會顯示訊息。

  14. 嵌入應用程式會將符記傳送至 Looker 嵌入 iframe。

  15. 收到符記後,在 iframe 中執行的 Looker UI 就會啟動轉譯要求物件的程序。在這段期間,UI 會向 Looker 伺服器發出 API 呼叫。在步驟 15 中收到的 API 權杖會自動以標頭的形式插入所有 API 要求。

  16. 在執行任何端點之前,Looker 伺服器會在要求中尋找 API 權杖。如果伺服器找到權杖,就會檢查下列項目:

    • 相關聯的工作階段仍有效。
    • 與要求相關聯的瀏覽器使用者代理程式,與與工作階段相關聯的瀏覽器代理程式相符。

    如果工作階段有效,系統會為要求還原工作階段,並執行 API 要求。

  17. 傳回資訊主頁資料。

  18. 系統會算繪資訊主頁。

  19. 使用者可控管資訊主頁。

產生新權杖

下列順序圖說明新符記的產生方式。

序列圖:說明產生新符記的過程。

  1. 在嵌入式 iframe 中執行的 Looker UI 會監控嵌入符記的 TTL。
  2. 權杖即將到期時,Looker UI 會傳送權杖重新整理訊息給嵌入應用程式用戶端。
  3. 接著,嵌入應用程式用戶端會從嵌入應用程式伺服器中實作的端點要求新的符記。Looker Embed SDK 會自動要求新的權杖,但必須提供端點網址或回呼函式。如果使用回呼函式,則會呼叫內嵌應用程式伺服器來產生新的權杖。否則,嵌入 SDK 會呼叫提供的端點網址。
  4. 嵌入應用程式會找出與嵌入工作階段相關聯的 session_reference_tokenLooker Embed SDK Git 存放區中提供的範例使用工作階段 Cookie,但也可以使用分散式伺服器端快取,例如 Redis。
  5. 嵌入應用程式伺服器會呼叫 Looker 伺服器,要求產生權杖。除了提出要求的瀏覽器使用者代理程式外,這項要求還需要最近的 API 和導覽符記。
  6. Looker 伺服器會驗證使用者代理程式、工作階段參照權杖、導覽權杖和 API 權杖。如果要求有效,系統就會產生新的符記。
  7. 權杖會傳回至呼叫的嵌入應用程式伺服器。
  8. 嵌入應用程式伺服器會從回應中移除工作階段參照權杖,並將剩餘的回應傳回給嵌入應用程式用戶端。
  9. 嵌入應用程式用戶端會將新產生的權杖傳送至 Looker UI。Looker 嵌入 SDK 會自動執行這項作業。使用 windows.postMessage API 的嵌入式應用程式用戶端將負責傳送權杖。Looker UI 收到權杖後,就會在後續的 API 呼叫和網頁導覽中使用這些權杖。

實作 Looker 無 Cookie 嵌入

您可以使用 Looker 嵌入 SDK 或 windows.postMessage API 實作 Looker 無 Cookie 嵌入功能。您可以使用 Looker Embed SDK 方法,但我們也提供範例,說明如何使用 windows.postMessage API。如需這兩種實作項目的詳細說明,請參閱 Looker 嵌入 SDK README 檔案嵌入 SDK Git 存放區也包含可用的實作項目。

設定 Looker 執行個體

無 Cookie 嵌入功能與 Looker 已簽署嵌入功能有許多相似之處。如要使用無 Cookie 嵌入功能,管理員必須啟用「嵌入單一登入 (SSO) 驗證」。不過,與 Looker 簽署嵌入功能不同的是,無 Cookie 嵌入功能不會使用「Embed Secret」設定。無 Cookie 嵌入功能會使用 JSON Web Token (JWT),其形式為「Embed JWT Secret」設定,可在「Admin」選單的「Platform」部分的「Embed」頁面中設定或重設。

不必設定 JWT 密鑰,因為系統會在首次嘗試建立無 Cookie 嵌入工作階段時建立 JWT。請勿重設這個權杖,否則會使所有有效的無 Cookie 嵌入工作階段失效。

與內嵌密鑰不同,內嵌 JWT 密鑰不會公開,因為它只會在 Looker 伺服器內部使用。

應用程式用戶端實作

本節提供如何在應用程式用戶端中實作無 Cookie 嵌入功能的範例,並包含下列子節:

安裝或更新 Looker 嵌入 SDK

如要使用無 Cookie 嵌入功能,必須使用下列 Looker SDK 版本:

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

使用 Looker 嵌入 SDK

Embed SDK 已新增新的初始化方法,用於啟動無 Cookie 工作階段。這個方法會接受兩個網址字串或兩個回呼函式。網址字串應參照嵌入應用程式伺服器中的端點。本文件的「應用程式伺服器導入方式」一節將說明應用程式伺服器上這些端點的導入方式。

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

以下範例說明如何使用回呼。只有在嵌入用戶端應用程式需要瞭解 Looker 嵌入工作階段狀態時,才應使用回呼。您也可以使用 session:status 事件,這樣就不必使用回呼與嵌入 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 時會呼叫 AcquireSession。
  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 編寫的節點 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