问题场景

在开发配合外部系统的子系统过程中,外部系统需要实现”一网通管”并通过单点登录方式实现免登录。当前实现是外部系统传入sso_token信息,子系统返回重定向response,用户在新标签页中可以正常访问子系统的数据看板页面。

但当外部系统通过iframe方式嵌入子系统时,却无法正常查看数据看板页面,而是跳转到了登录页面。即使输入密码登录后仍然无法进入系统。需要实现外部系统在iframe中嵌入子系统时,子系统仍能正常免登录查看数据看板页面。

问题分析

通过浏览器开发者工具分析请求信息:

打开新标签页跳转数据看板页面的请求信息正常:
正常页面请求信息

但在iframe中跳转数据看板页面时,发现cookie无法正常设置,请求没有携带cookie。这是因为Set-Cookie标头未指定”SameSite”属性,默认为”SameSite=Lax”,而该cookie被浏览器屏蔽,因为它来自一个跨站响应,且不是顶级导航操作的响应。要使cookie能在跨站场景下使用,必须设置”SameSite=None”。

这导致请求没有携带任何用户信息,后台判断为未登录状态后跳转到登录页面:
iframe中页面请求信息

同样,在iframe中输入密码登录时也存在相同问题,无法设置cookie:
iframe中登录请求信息

Cookie属性详解

基本属性

  • domain=domain(例如,example.com或subdomain.example.com):指定cookie将发送到的主机。如果未指定,则默认为当前文档位置的主机部分,且cookie在子域上不可用。如果指定了域,则始终包含子域。注意:该域必须与JavaScript源的域匹配。

  • expires=date-in-GMTString-format:指定cookie的到期日期。如果既没有指定expires也没有指定max-age,则cookie将在会话结束时过期。

    警告:出于用户隐私考虑,Web应用程序应在一定超时后使Cookie数据无效,而不是依赖浏览器来执行此操作。

  • max-age=max-age-in-seconds:指定cookie的最长期限(以秒为单位),例如606024*365=31536000表示一年。

  • partitioned:表示cookie应该使用分区存储来存储。有关更多详细信息,请参阅具有独立分区状态的Cookie (CHIPS)。

  • path=path:指定请求URL中必须存在的路径,以便浏览器发送Cookie标头(例如”/“、”/mydir”)。如果未指定,则默认为当前文档位置的当前路径。

  • samesite:阻止浏览器随跨站点请求发送此cookie。可能的值包括:

    • lax:发送同站点请求和顶级导航GET请求的cookie。对于用户跟踪已经足够,但可以防止许多CSRF攻击。这是现代浏览器中的默认值。
    • strict:阻止浏览器在所有跨站点浏览上下文中将cookie发送到目标站点。
    • none:明确表明不会应用任何限制。该cookie将在所有请求中发送 - 跨站点和同站点。
  • httponly:如果在Cookie中设置了”HttpOnly”属性,则通过程序(JS脚本、Applet等)将无法读取到Cookie信息,能有效防止XSS攻击。

  • secure:指定cookie只能通过安全协议(HTTPS)传输。

Cookie前缀

一些用户代理实现支持以下cookie前缀:

  • __Secure-:向浏览器发出信号,表明它应该仅在通过安全通道传输的请求中包含cookie。

  • __Host-:向浏览器发出信号,表明除了仅使用来自安全来源的cookie的限制之外,cookie的范围还仅限于服务器传递的路径属性。它还表明域属性不能存在,这会阻止cookie被发送到其他域。

cookie发送规则

解决方案

在外部系统携带sso信息请求过来时,返回response时需要添加Set-Cookie响应头,设置SameSite属性为None,并搭配Secure属性使用:

1
Set-Cookie: sessionid=abc123; SameSite=None; Secure

设置 Set-Cookie 响应头

服务端实现示例

以常见的Web框架为例,展示如何正确设置Cookie:

Django示例

1
2
3
4
5
6
7
8
response = HttpResponseRedirect('/dashboard/')
response.set_cookie(
'sessionid',
'abc123',
samesite='None',
secure=True,
httponly=True
)

Flask示例

1
2
3
4
5
6
7
8
9
10
from flask import make_response

response = make_response(redirect('/dashboard/'))
response.set_cookie(
'sessionid',
'abc123',
samesite='None',
secure=True,
httponly=True
)

Node.js Express示例

1
2
3
4
5
6
res.cookie('sessionid', 'abc123', {
sameSite: 'none',
secure: true,
httpOnly: true
});
res.redirect('/dashboard/');

注意事项

  1. SameSite=None必须与Secure配合使用:当设置SameSite=None时,必须同时设置Secure属性,否则会被浏览器忽略。

  2. HTTPS环境要求:由于需要设置Secure属性,整个系统必须运行在HTTPS环境下。

  3. 浏览器兼容性:较老的浏览器可能不支持SameSite=None属性,需要进行兼容性处理。

  4. 安全性考虑

    • 使用HttpOnly属性防止XSS攻击
    • 设置适当的过期时间
    • 使用安全的会话管理机制

最佳实践总结

  1. 在跨站嵌入场景中,必须显式设置SameSite=None和Secure属性
  2. 确保整个应用运行在HTTPS环境下
  3. 合理设置cookie的过期时间和安全属性
  4. 在服务端正确处理跨域单点登录逻辑
  5. 充分测试各种浏览器环境下的兼容性