【前端】如何解决同源策略背景下的单点登陆问题?

什么是同源策略?

        同源策略(same origin policy)是netScape(网景)提出的一个安全策略,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。具体表现为浏览器在执行脚本前,会判断脚本是否与打开的网页是同源的,判断协议、域名、端口是否都相同,相同则表示同源。其中一项不相同就表示跨域访问。会在控制台报一个CORS异常,目的是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。

        浏览器采用同源策略,在没有明确授权的情况下,禁止页面加载或执行与自身不同源的任何脚本。

同源策略的分类

  1. DOM 同源策略: DOM和JS对象无法获得,即禁止对不同源页面 DOM 进行操作。
    这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的
    比如一个恶意网站的页面通过iframe嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的javascript脚本就可以在用户登录银行的时候获取用户名和密码
  2. XMLHttpRequest 同源策略: 禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求,包括 AJAX,AJAX发送请求后,会被浏览器拦截
  3. Cookie、LocalStorage、IndexedDB 等存储性内容同源策略: js中无法访问不属于同个源的cookie、LocalStorage中存储的内容。

        具体来说,cookie和LocalStorage在控制哪些源可以访问的问题上还是细微的差别:
        父域在设置cookie的时候可以设定允许子域访问这段cookie,同时Cookie只和域名以及路径关联,如果是同个域名不同端口的源依然是共享同个域名下的Cookie的
        而LocalStorage则是以源为单位进行管理,相互独立,不同源之间无法相互访问LocalStorage中的内容。

同源策略的"后门"

  1. 页面中的链接,重定向以及表单提交是不会受到同源策略限制的(未授权情况下,ajax 的表单提交是不被允许的,但是普通的表单是可以直接跨域的)。
  2. <script>、<img>、<link>这些包含 src 属性的标签可以加载跨域资源。但浏览器限制了JavaScript的权限使其不能读、写加载的内容。

为什么会有同源策略?

简单的举两个例子

(1)如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:

  1. 做一个假网站,里面用 iframe 嵌套一个银行网站mybank.com。
  2. 把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
  3. 这时如果用户输入账号密码,我们的主网站可以跨域访问到mybank.com 的 dom 节点,就可以拿到用户的账户密码了。

(2)如果 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:

  1. 用户登录了自己的银行页面mybank.com,银行页面向用户的 cookie 中添加用户标识。
  2. 用户浏览了恶意页面evil.com,执行了页面中的恶意 AJAX 请求代码。
  3. evil.com 向mybank.com 发起 AJAX HTTP 请求,浏览器会默认把mybank.com 对应 cookie 也同时发送过去。
  4. 银行页面从发送的 cookie 中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了,而且由于 Ajax 在后台执行,用户无法感知这一过程。

注:

  • cookie 是浏览器根据你请求的页面进行发送的,而与从哪个页面发送过去请求无关。比如说,我访问了 a.com 以后获取了 a.com 的cookie ,然后我访问 q.com ,由于 cookie 域的限制,浏览器在请求 q.com 的时候不会携带 a.com 的cookie ,但是当 q.com 有一个向 a.com 发起的请求的时候,浏览器就会自动的在这个请求中带上 a.com 的 cookie。

  • 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?
因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

小结:同源策略控制不同源之间的交互,这些交互通常分为三类:

  1. 跨域读: 同源策略是不允许这种事情发生的,如果可以你就能使用 js 读取嵌入在 iframe 中的页面的 dom 元素,获取敏感信息了
  2. 跨域写: 同源策略不阻止这种操作,比如向不同源的地址发送 POST 请求等,但是这种允许只限制在普通表单(而且是在没有 CSRF token 或者验证 referer 的情况下),对于 ajax 这种方式也是默认不允许的,如果随随便便允许就会出现使用 ajax 来进行 CSRF 请求的情况了
  3. 跨域嵌入: 这种方式是默认允许的,我们可以在一个源中通过 iframe 嵌入 另一个源的页面,但是如果想限制这种操作的话,我们可以设置 x-frame-options 这个头,这样设置了这个头的页面就允许被嵌入到不同源的页面中了

什么是域名分级?

        从专业的角度来说(根据《计算机网络》中的定义),.com、.cn 为一级域名(也称顶级域名),.http://com.cn、http://baidu.com 为二级域名,http://sina.com.cn、http://tieba.baidu.com 为三级域名,以此类推,N 级域名就是 N-1 级域名的直接子域名。

        从使用者的角度来说,一般把可支持独立备案的主域名称作一级域名,如 http://baidu.com、http://sina.com.cn 皆可称作一级域名,在主域名下建立的直接子域名称作二级域名,如 http://tieba.baidu.com 为二级域名。

        为了避免歧义,下文将使用“主域名“替代”一级域名“的说法。

什么是单点登陆?

        在 B/S 系统中,登录功能通常都是基于 Cookie 来实现的。

        当用户登录成功后,一般会将登录状态记录到 Session 中,或者是给用户签发一个 Token,无论哪一种方式,都需要在客户端保存一些信息(Session ID 或 Token ),并要求客户端在之后的每次请求中携带它们。

        在这样的场景下,使用 Cookie 无疑是最方便的,因此我们一般都会将 Session 的 ID 或 Token 保存到 Cookie 中,当服务端收到请求后,通过验证 Cookie 中的信息来判断用户是否登录 。

        单点登录(Single Sign On, SSO)是指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的应用系统。举例来说,百度贴吧和百度地图是百度公司旗下的两个不同的应用系统,如果用户在百度贴吧登录过之后,当他访问百度地图时无需再次登录,那么就说明百度贴吧和百度地图之间实现了单点登录。

        单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,比如可以将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。

        当然仅此是不够的,因为不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 Session ID 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在 http://app1.com 中登录后,Session ID 仅在浏览器访问 http://app1.com 时才会自动在请求头中携带,而当浏览器访问 http://app2.com 时,Session ID 是不会被带过去的。实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。

如何实现基于iframe的单点登陆?

父域Cookie

        在将具体实现之前,我们先来聊一聊 Cookie 的作用域。Cookie 的作用域由 domain 属性和 path 属性共同决定。

        domain 属性的有效值为当前域或其父域的域名/IP地址,在 Tomcat 中,domain 属性默认为当前域的域名/IP地址。

        path 属性的有效值是以“/”开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。

        Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的Cookie。利用 Cookie 的这个特点,不难想到,将 Session ID(或 Token)保存到父域中不就行了。

        没错,我们只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。

        不过这要求应用系统的域名需建立在一个共同的主域名之下,如 http://tieba.baidu.com 和 http://map.baidu.com,它们都建立在 http://baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。

        总结:此种实现方式比较简单,但不支持跨主域名。

认证中心

        我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的。)

        应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。

                如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。

应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。

        确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。这里顺便介绍两款认证中心的开源实现:

  • Apereo CAS 是一个企业级单点登录系统,其中 CAS 的意思是”Central Authentication Service“。它最初是耶鲁大学实验室的项目,后来转让给了 JASIG 组织,项目更名为 JASIG CAS,后来该组织并入了Apereo 基金会,项目也随之更名为 Apereo CAS。
  • XXL-SSO 是一个简易的单点登录系统,由大众点评工程师许雪里个人开发,代码比较简单,没有做安全控制,因而不推荐直接应用在项目中,这里列出来仅供参考。

        总结:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

LocalStorage 跨域

        前面,我们说实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。父域 Cookie 确实是一种不错的解决方案,但是不支持跨域。那么有没有什么奇淫技巧能够让 Cookie 跨域传递呢?很遗憾,浏览器对 Cookie 的跨域限制越来越严格。

        Chrome 浏览器还给 Cookie 新增了一个 SameSite 属性,此举几乎禁止了一切跨域请求的 Cookie 传递(超链接除外),并且只有当使用 HTTPs 协议时,才有可能被允许在 AJAX 跨域请求中接受服务器传来的 Cookie。

        不过,在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。

        这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端。在这样的场景下,单点登录完全可以在前端实现。

        前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。关键代码如下:

// 获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
    iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
    iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
    localStorage.setItem('token', event.data)
}, false);

        前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。

        总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。

实际代码

系统一

vue文件

<script setup>
const btn = () => {
  const iframe = document.createElement('iframe')
  iframe.setAttribute(
    'style',
    'position:absolute;width:0px;height:0px;left:-500px;top:-500px;'
  )
  iframe.src = 'http://172.22.130.82:3001'
  document.body.append(iframe)
  setTimeout(function () {
	  iframe.contentWindow.postMessage('123123', "http://172.22.130.82:3001");
	}, 1000);

  setTimeout(function () {
    iframe.remove();
  }, 3000);
}
</script>
<template>
  <button @click="btn">点击</button>
</template>

vite.config.js文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    port: 3000,
    host: "172.22.130.82",
    open: true
  }
})

系统二

vue文件

<script setup>
window.onmessage = function (ev) {
  if (ev.origin === 'http://172.22.130.82:3000') {
    localStorage.setItem('token', ev.data)
  }
}
</script>

<template>
</template>

vite.config.js文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    port: 3001,
    host: "172.22.130.82",
    open: true
  }
})

postMessage的安全风险?

postMessage是一个非常有用的API,它允许不同的JavaScript上下文之间进行通信,例如在主窗口和iframe,或者在不同的浏览器标签页之间。然而,如果不正确地使用,postMessage也可能带来一些安全风险。下面是一些可能的风险和如何防止它们:

  1. 接收端没有正确验证发送者(origin):当你在接收postMessage事件时,你应该总是检查事件的origin属性以确保消息来自一个你信任的源。如果你不这样做,那么任何其他的网站都可以发送消息给你的页面,这可能会导致一些安全问题。

    例如:

    window.addEventListener('message', function(event) {
        // Always check the origin of the data!
        if (event.origin !== 'http://example.com') return;
    
        // ...your code here...
    }, false);
    
  2. 发送敏感数据:如果你使用postMessage发送敏感数据,如用户凭证或个人信息,你需要确保接收者是你信任的,否则这些信息可能会被恶意网站接收。

  3. 没有验证消息的内容:除了检查消息的来源,你还应该验证消息的内容。恶意网站可能会尝试发送格式不正确的消息来破坏你的应用。

  4. 使用了通配符作为目标origin:在使用postMessage发送消息时,你可以指定一个目标origin。这应该是你信任的一个特定的origin。如果你使用通配符("*"),那么任何网站都可以接收你的消息,这可能会导致数据泄露。

    不安全的使用方式:

    otherWindow.postMessage(message, '*');
    

    更安全的使用方式:

    otherWindow.postMessage(message, 'http://example.com');
    

        在使用postMessage时,总是要确保你正确地验证了来源和消息内容,并且只向你信任的网站发送敏感数据。

📖 参考文章:

【精选】什么是同源策略?_新手前端小鹿的博客-CSDN博客

实现iframe_单点登录的三种实现方式,你会几种?-蒲公英云

前端单点登录 iframe_iframe 登录态_Jevin:的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值