当 Looker 通过单点登录 (SSO) 嵌入方式嵌入到 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 创建的会话。Looker Embed SDK 通过自动加入现有会话来简化此过程。
- 用户在嵌入应用中执行一个操作,从而创建 Looker iframe。
- 嵌入应用客户端获取 Looker 会话。Looker Embed SDK 可用于启动此会话,但必须提供端点网址或回调函数。如果使用回调函数,则会调用嵌入应用服务器来获取 Looker 嵌入会话。否则,Embed SDK 将调用提供的端点网址。
- 嵌入应用服务器使用 Looker API 获取嵌入会话。此 API 调用类似于 Looker 单点登录签名流程,因为它接受嵌入的用户定义作为输入。如果发起调用的用户已存在 Looker 嵌入会话,则该调用中应包含关联的会话参考令牌。本文档的获取会话部分将对此进行详细介绍。
- 获取嵌入会话端点的处理类似于由 SSO 签名的
/login/embed/{signed url)
端点,因为前者希望 Looker 嵌入用户定义成为请求正文,而不是网址。获取嵌入会话端点进程会验证并创建或更新嵌入用户。它还可以接受现有的会话引用令牌。这非常重要,因为这允许多个 Looker 嵌入式 iframe 共享同一会话。如果已提供会话参考令牌且会话未过期,嵌入用户将不会更新。这一使用情形支持这样一种情况:使用签名的 SSO 网址创建一个 iframe,而在没有签名 SSO 网址的情况下创建其他 iframe。在这种情况下,没有签名单点登录网址的 iframe 将继承第一个会话的 Cookie。 - Looker API 调用会返回四个令牌,每个令牌都有一个存留时间 (TTL):
- 授权令牌 (TTL = 30 seconds)
- 导航令牌 (TTL = 10 minutes)
- API 令牌 (TTL = 10 分钟)
- 会话参考令牌 (TTL = 会话的剩余生命周期)
- 嵌入应用服务器必须跟踪 Looker 数据返回的数据,并将其与调用方用户的浏览器以及调用方用户的用户代理相关联。请参阅本文档的生成令牌部分,了解方法建议。此调用将返回授权令牌、导航令牌、API 令牌以及所有关联的 TTL。会话参考令牌应受到妥善保护,且不会在发起调用的浏览器中公开。
将令牌返回给浏览器后,必须构建 Looker 嵌入登录网址。Looker Embed SDK 会自动构建嵌入登录网址。如需使用
windows.postMessage
API 构建嵌入登录网址,请参阅本文档的使用 Lookerwindows.postMessage
API 部分。登录网址不包含已签名的嵌入用户详细信息。它包含目标 URI,包括导航令牌,以及作为查询参数的授权令牌。授权令牌必须在 30 秒内使用,且只能使用一次。如果需要其他 iframe,则必须重新获取嵌入会话。不过,如果提供了会话参考令牌,授权令牌将与同一会话相关联。
Looker 嵌入式登录端点确定登录是否用于无 Cookie 嵌入(由授权令牌指示)。如果授权令牌有效,则会检查以下内容:
- 关联的会话仍然有效。
- 关联的嵌入用户仍然有效。
- 与请求关联的浏览器用户代理与会话关联的浏览器代理相匹配。
如果上一步中的检查通过,则系统会使用网址中包含的目标 URI 重定向请求。这与 Looker Embed 单点登录的登录流程相同。
该请求是启动 Looker 信息中心的重定向。此请求会将导航令牌作为参数。
在执行端点之前,Looker 服务器会在请求中查找导航令牌。如果服务器找到该令牌,它会检查以下方面:
- 关联的会话仍然有效。
- 与请求关联的浏览器用户代理与会话关联的浏览器代理相匹配。
如果有效,系统会恢复该请求的会话,并且信息中心请求会运行。
用于加载信息中心的 HTML 会返回给 iframe。
在 iframe 中运行的 Looker 界面确定信息中心 HTML 是无 Cookie 的嵌入响应。此时,Looker 界面会向嵌入应用发送消息,请求第 6 步中检索到的令牌。然后界面会等待其收到令牌。如果令牌没有到达,系统会显示一条消息。
嵌入应用将令牌发送到 Looker 嵌入式 iframe。
收到令牌后,在 iframe 中运行的 Looker 界面会启动呈现请求对象的进程。在此过程中,界面将对 Looker 服务器进行 API 调用。在第 15 步中收到的 API 令牌作为标头自动注入到所有 API 请求。
在执行任何端点之前,Looker 服务器会在请求中查找 API 令牌。如果服务器找到此令牌,就会检查以下内容:
- 关联的会话仍然有效。
- 与请求关联的浏览器用户代理与会话关联的浏览器代理相匹配。
如果会话有效,则请求会针对该请求恢复,并且 API 请求会运行。
系统返回信息中心数据。
信息中心已呈现。
用户可以控制信息中心。
生成新令牌
- 在嵌入式 iframe 中运行的 Looker 界面会监控嵌入令牌的 TTL。
- 当令牌即将到期时,Looker 界面会向嵌入的应用客户端发送刷新令牌消息。
- 然后,嵌入应用客户端从嵌入应用服务器中实现的端点请求新令牌。Looker Embed SDK 会自动请求新令牌,但必须提供端点网址或回调函数。如果使用的是回调函数,则会调用嵌入应用服务器以生成新令牌。否则,Embed SDK 将调用提供的端点网址。
- 嵌入应用会查找与嵌入会话相关联的
session_reference_token
。Looker Embed SDK Git 代码库中提供的示例使用会话 Cookie,但也可以使用分布式服务器端缓存(例如 Redis)。 - 嵌入应用服务器调用 Looker 服务器,并请求生成令牌。除了发起请求的浏览器的用户代理外,此请求还需要最近的 API 和导航令牌。
- Looker 服务器会验证用户代理、会话参考令牌、导航令牌和 API 令牌。如果请求有效,则会生成新令牌。
- 这些令牌会返回给调用嵌入应用服务器。
- 嵌入应用服务器会从响应中去除会话引用令牌,并将剩余响应返回给嵌入应用客户端。
- 嵌入应用客户端会将新生成的令牌发送到 Looker 界面。Looker Embed SDK 会自动执行此操作。使用
windows.postMessage
API 的嵌入式应用客户端将负责发送令牌。Looker 界面收到令牌后,它们就会在后续的 API 调用和页面导航中使用。
实现 Looker 无 Cookie 嵌入
Looker 无 Cookie 嵌入可通过 Looker Embed SDK 或 windows.postMessage
API 来实现。您也可以使用 Looker Embed SDK 的相应方法,但也可以使用展示 windows.postMessage
API 的示例。有关这两种实现的详细说明,请参阅 Looker Embed SDK 自述文件。Embed SDK git 代码库也包含有效的实现。
配置 Looker 实例
无 Cookie 嵌入与 Looker 单点登录 (SSO) 嵌入具有通用性。无 Cookie 嵌入依赖于嵌入单点登录身份验证。不过,与 Looker 单点登录不同,无 Cookie 嵌入不使用嵌入 Secret 设置。无 Cookie 嵌入使用 JSON Web 令牌 (JWT),格式为 Embed JWT Secret 设置,您可以在管理菜单的平台部分的嵌入页面上设置或重置该令牌。
您不需要设置 JWT 密钥,因为首次尝试创建无 Cookie 嵌入会话时将会创建 JWT。避免重置此令牌,因为这样做会导致所有有效的无 Cookie 嵌入会话失效。
与嵌入密钥不同,嵌入 JWT 密钥永远不会公开,因为它仅在 Looker 服务器内部使用。
应用客户端实现
本部分举例说明了如何在应用客户端中实现无 Cookie 嵌入,具体包含以下子部分:
安装或更新 Looker Embed SDK
如需使用无 Cookie 嵌入,必须使用以下 Looker SDK 版本:
@looker/embed-sdk >= 1.8
@looker/sdk >= 22.16.0
使用 Looker Embed SDK
Embed SDK 中添加了一种新的初始化方法,用于启动无 Cookie 的会话。此方法接受两个网址字符串或两个回调函数。网址字符串应引用嵌入应用服务器中的端点。本文档的应用服务器实现部分介绍了在应用服务器上这些端点的实现细节。
LookerEmbedSDK.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 (): Promise<LookerEmbedCookielessSessionData> => {
const { api_token, navigation_token } = getApplicationTokens() || {}
const resp = await fetch('/generate-embed-tokens', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ api_token, navigation_token }),
})
if (!resp.ok) {
if (resp.status === 400) {
return { session_reference_token_ttl: 0 }
}
console.error('generate-embed-tokens failed', { resp })
throw new Error(
`generate-embed-tokens failed: ${resp.status} ${resp.statusText}`
)
}
return (await resp.json()) as LookerEmbedCookielessSessionData
}
LookerEmbedSDK.initCookieless(
runtimeConfig.lookerHost,
acquireEmbedSessionCallback,
generateEmbedTokensCallback
)
使用 Looker windows.postMessage
API
您可以在 Embed SDK Git 代码库的 message_example.ts
和 message_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 端点的服务器端端点。这是为了确保会话参考令牌始终安全。这些端点为:
- 获取会话 - 如果会话引用令牌已存在且仍然有效,那么会话请求将加入现有会话。在创建 iframe 时调用获取会话。
- 生成令牌 - 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 响应。这种情况应该不会发生,但如果确实发生了,最好终止会话。
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')) {
// Currently the Looker UI does not know how to handle bad
// tokens. This should not 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,则应对其进行加密。嵌入式 SDK 代码库中的示例使用会话 Cookie 来存储会话参考令牌。
Looker 嵌入会话过期后,嵌入式 iframe 中会显示对话框。此时,用户将无法在嵌入式实例中执行任何操作。发生这种情况时,将生成 session:status
事件,使嵌入应用能够检测嵌入式 Looker 应用的当前状态,并执行某种操作。
运行 Looker 无 Cookie 嵌入示例
嵌入式 SDK 存储库包含一个以 TypeScript 编写的简单节点 Express 服务器和客户端,用于实现一个简单的嵌入应用。前面显示的示例来自此实现。下文假定您的 Looker 实例已配置为使用如无 Cookie 的嵌入(如前所述)。
您可以按照以下步骤运行服务器:
- 克隆 Embed SDK 代码库 -
git clone git@github.com:looker-open-source/embed-sdk.git
- 更改目录 -
cd embed-sdk
- 安装依赖项 -
npm install
- 配置服务器,如本文档的配置服务器部分所示。
- 运行服务器 -
npm run server
配置服务器
在克隆的代码库的根目录(包含在 .gitignore
中)中创建 .env
文件。
其格式如下所示:
LOOKER_EMBED_HOST=your-looker-instance-url.com.
LOOKER_EMBED_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