单点登录
单点登录
单点登录时不能将 token 拼接在 URL 上传递,因为 URL 参数会被浏览器历史、插件、Referer、恶意 JS、抓包工具等获取,存在很高的泄漏风险。
正确方式是:认证中心在登录成功后,仅通过重定向返回一个一次性授权码(code),该 code 短期有效(通常几十秒)且只能使用一次;业务系统使用此 code 从认证中心换取 token。
携带参数
业务系统跳转到单点登录页面时要携带如下参数:
https://sso.com/authorize?
response_type=code
&client_id=system-a
&redirect_uri=https%3A%2F%2Fsystem-a.com%2Fcallback
&scope=openid%20profile%20email
&state=abc123
&code_challenge=xyz
&code_challenge_method=S256
&nonce=n-0S6_WzA2Mj
response_type: 认证中心服务端通过此参数来判断此次登录是普通登录,还是需要重定向的单点登录;
client_id:用于标识发起认证请求的客户端(业务系统),认证中心根据该参数识别调用方,并加载对应的配置(如 redirect_uri 白名单、scope 权限范围等)。
redirect_uri: 登录后要跳转的业务系统地址;这是高危参数,必须在认证中心提前注册,且不允许通配符、不允许动态拼接,否则会导致 code 被发给攻击者;
scope:向认证中心声明“需要哪些权限 / 信息”,例如:openid(用户ID)、profile(用户基本信息)、email(用户邮箱)等;
state: 业务系统在跳转到登录页面前创建一个 state 保存到 sessionStorage,登录成功后必须验证此 state; 用于在登录发起阶段与回调阶段建立一一对应关系,从而防止第三方伪造登录结果(CSRF 攻击);例如:攻击者使用自己的合法 code 伪造一个的回调地址来访问业务系统,导致业务系统使用攻击者伪造的 code 去获取 token,从而登录到攻击者的账号中。
code_challenge:用于防止授权码(code)被截获后被滥用,使授权码只能被原始客户端使用。客户端在发起登录前生成一个高随机的 code_verifier,并对其进行 SHA-256 哈希计算得到 code_challenge(通过 code_challenge_method=S256 声明算法);随后在 /authorize 登录请求中只提交 code_challenge,认证中心在签发授权码(code)时会将该 code 与 code_challenge 进行绑定;登录成功后客户端在调用 /token 接口获取 token 时提交原始的 code_verifier,认证中心对其进行同样的哈希计算,并与绑定的 code_challenge 比对,只有一致时才会签发 token。由于 SHA-256 是不可逆的,攻击者即使获取了 code 和 code_challenge,也无法推导出 code_verifier,因此无法利用截获的授权码换取 token。
code_challenge_method:用于声明 code_challenge 的生成方式(哈希算法),通常为 S256(表示使用 SHA-256);该值是协议中定义的固定枚举值,而不是可自定义字符串。
nonce: 客户端在发起登录时生成一个一次性的高随机的 nonce,并存入到 sessionStorage 中,认证中心把它原样写进 id_token,客户端在收到 id_token 后校验两者一致,从而确保这个 token 就是本次登录产生的,防止旧 token 被重放;(因此,客户端在一次验证后就必须删除此 nonce,以防止攻击者使用旧请求误导业务系统)
CSRF攻击
CSRF(Cross-Site Request Forgery,跨站请求伪造),是利用用户已经登录身份,在用户不知情的情况下诱导浏览器发起恶意请求的攻击方式。
例如:攻击者伪造一个钓鱼网站,然后在钓鱼网站中向正常的业务网站 business.com 发起转账请求;由于用户已登录 business.com,因此浏览器会自动携带 cookie 至 business.com,如此一来钓鱼网站就通过伪造请求将用户的钱转到自己账户。
CSRF 攻击本质不是窃取用户身份,而是攻击者借用用户已有的登录身份,让服务器执行用户原本没有打算执行的操作。
防御措施:将 cookie 的 SameSite 设置为 Lax;浏览器看到此设置后,会限制同站请求正常携带 Cookie,除 GET 之外的跨站请求不携带 Cookie;这样以来,因为钓鱼网站与 cookie 所设置的域名不同站,所以在钓鱼网站中发起请求时,浏览器不会携带 cookie ,从而直接杜绝了 CSRF。
浏览器是否在请求中携带 cookie 取决于两个因素:
- 一是 cookie 的 Domain 域名是否匹配,如果 cookie Domain 为 chailv.sidoc.cn,则只有向 chailv.sidoc.cn 及其子域名发起请求时才会携带 cookie;
- 二是同站,如果
SameSite为Lax,则只有 cookie Domain 和 当前网站的顶级域名、协议都相同时,才会携带 cookie;如果 cookie Domain 为 chailv.sidoc.cn,则只有在顶级域名为 sidoc.cn 的网站中向 chailv.sidoc.cn 发起请求时才会携带 cookie ;
例如下,Cookie Domain 匹配,且同站时才会携带 cookie:

同站是指主域名和协议都相同, SameSite 设置为 Lax 表示仅允许同站携带 cookie ;例如: https://chailv.sidoc.cn 和 https://other.sidoc.cn 就是同站,因为它们主域名和协议相同;
SameSite 设置为 Lax 时,之所以允许 GET 跨站可携带 cookie ,主要是因为从浏览器需要在防御 CSRF 攻击的同时,保证用户从其他网站正常跳转到已登录网站时,仍然保持登录状态; 例如:从 百度、Google 等搜索引擎中跳转到正常网站时。
在使用 httpOnly cookie 时,要做好子业务间的凭证隔离;例如有3个子站:a.sidoc.cn、b.sidoc.cn、c.sidoc.cn;这些子站都是同站,因此在这些任意一个子站中向 service.a.sidoc.cn 发起请求时,都会携带 cookie ,所以任何一个子站被攻破,都会导致其它子站产生 CRSF 风险;所以要确保各子站之间的 cookie 不通用,例如:差旅 Token 不能用于财税 API;例如:不要加 token 放在 cookie 中,不仅体积大,还不安全;最好是将 token 放在业务服务的 redis 中,然后 cookie 中只存 key,这样也好控制 token 有效期;也弥补无状态 jwt token 无法控制有效期的问题。
切勿将 cookie 的域名设置为顶级域名,因为这会增加 cookie 的暴露面,例如下:

缩小 Cookie 的 Domain 可以有效减少凭证暴露范围,但无法单独解决同站 CSRF 问题。
最好是在单点登录成功后,调用对应的业务网站服务端,让业务服务端通过一性 code 去授权中心换取 token ,然后将 cookie 的域名设置为当前业务域名,例如: chailv.sidoc.cn, 这样以来就只有 chailv.sidoc.cn 、service.chailv.sidoc.cn 等当前业务服务域名才可同站携带 cookie。
HttpOnly Cookie 与普通 Cookie 的核心区别是:JavaScript 无法读取和修改 HttpOnly Cookie,因此它安全性高。
同站 cookie 是第一方 cookie ,跨站 cookie 是第三方 cookie,浏览器对第一方 cookie 友好,对第三方 cookie 正在收紧。
用户可能禁用所有 cookie ,从而使基于 httponly cookie 的授权系统失效,但这种用户极少,远低于1%;不需要为了这极少数用户而专门设计另一套登录认证机制。原因是 Cookie 是浏览器的基础功能,大量网站依赖它维持登录状态;例如:Google、Github、微信公众号网站等;如果用户禁用 cookie,那主流网站全部都会受影响。
访问某些网站时,会弹出“允许全部 Cookie(Accept All Cookies)”提示,这是网站在征求你同意:除了正常运行必需的 Cookie,还允许它使用统计分析、广告追踪、个性化推荐等非必要 Cookie。因此,如果你的网站只使用 cookie 来存储 登录状态、Session、购物车、安全验证等必要信息 ,则无须征求用户同意。