利用 HTTPOnly Cookie 启用 CORS 以保护访问令牌
本文将深入探讨如何运用 HTTPOnly cookie 技术结合跨域资源共享 (CORS),来增强我们访问令牌的安全性。
现今,前端客户端与后端服务器往往部署在不同的域上。因此,为了允许客户端在浏览器上与服务器顺利通信,服务器端必须启用 CORS。
此外,为了提升可扩展性,服务器正在逐步采用无状态身份验证机制。令牌在客户端存储和维护,而不是像传统会话那样在服务器端存储和管理。出于安全考虑,将令牌存储在 HTTPOnly cookie 中是更优的选择。
为何跨域请求会被浏览器阻止?
假设前端应用部署在 https://app.techblik.com
。在这种情况下,该域名下加载的脚本只能请求与其同源的资源。
每当我们试图向不同域名 (例如 https://api.techblik.com
),不同端口 (例如 https://app.techblik.com:3000
),或是不同协议 (例如 http://app.techblik.com
) 发送跨域请求时,浏览器通常会出于安全原因阻止这些请求。
但值得注意的是,同样是跨域请求,如果使用 curl
命令从任何后端服务器发起,或使用 Postman
等工具发送,却不会出现 CORS 问题。这主要是为了保护用户免受跨站请求伪造 (CSRF) 等攻击。
举例来说,如果用户在浏览器中登录了自己的 PayPal 账户,而我们能够从加载于另一个域名 (例如 https://malicious.com
) 的脚本向 paypal.com
发送跨域请求,且没有任何 CORS 错误或阻止,情况就会变得非常危险。这等同于我们可以发送同源请求一样自由。
攻击者可以轻易地将包含恶意代码的页面链接转换为短网址,隐藏其真实 URL,例如 https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account
。当用户点击这个恶意链接后,加载在 malicious.com
域名下的脚本可能会向 PayPal 发送跨域请求,将用户的资金转移到攻击者的 PayPal 账户中。所有登录了 PayPal 账户并点击了该恶意链接的用户都将遭受损失。攻击者可以在用户不知情的情况下轻易窃取资金。
基于以上安全风险考虑,浏览器默认会阻止所有跨域请求。
什么是 CORS (跨域资源共享)?
CORS 是一种基于 HTTP 头的安全机制,服务器通过它告知浏览器哪些域是被信任的,可以发送跨域请求。
当服务器启用了必要的 CORS 头部信息后,就可以避免浏览器阻止合法的跨域请求。
CORS 的工作原理
服务器会在其 CORS 配置中明确定义可信任的域名。当我们向服务器发送请求时,服务器的响应将包含相关信息,告知浏览器请求的域是否在信任列表中。
CORS 请求通常分为两种类型:
- 简单请求
- 预检请求
简单请求:
- 浏览器会向一个跨域域名发送带有
origin
头部 (例如:https://app.techblik.com
) 的请求。 - 服务器会返回带有允许方法和允许来源的响应。
- 浏览器在接收到响应后,会检查发送的
origin
头部值 (例如:https://app.techblik.com
) 和接收到的access-control-allow-origin
头部值是否相同或为通配符。
如果验证不一致,浏览器将引发 CORS 错误。
- 预检请求:
- 当跨域请求包含自定义请求参数(例如 PUT、DELETE 方法)或自定义头部,或者使用不同的内容类型时,浏览器会发送一个预检 OPTIONS 请求。这一步骤是为了验证实际请求是否能够安全发送。
当浏览器接收到预检请求的响应(状态码 204 表示无内容)后,会检查实际请求的 access-control-allow
参数。只有当服务器允许这些请求参数时,实际的跨域请求才会被发送和接收。
如果 access-control-allow-origin
设置为 *
,则代表允许所有来源的响应。但除非必要,否则这种做法并不安全。
如何启用 CORS?
要为特定的域名启用 CORS,需要配置 CORS 头部信息,指定允许的来源、方法、自定义头部、凭据等。
- 浏览器会读取服务器返回的 CORS 头部,只有在验证请求参数后,才允许客户端发送实际的请求。
Access-Control-Allow-Origin
:用于指定精确的域名 (例如https://app.geekflate.com
,https://lab.techblik.com
),或者使用通配符。Access-Control-Allow-Methods
:允许指定的 HTTP 方法 (例如 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)。Access-Control-Allow-Headers
:允许特定的头部信息 (例如 Authorization, csrf-token)。Access-Control-Allow-Credentials
:一个布尔值,用于指定是否允许跨域凭据 (例如 cookies, Authorization 头部)。Access-Control-Max-Age
:告知浏览器缓存预检响应的时间。Access-Control-Expose-Headers
:指定客户端脚本可以访问的头部信息。
请参考相关教程,了解如何在 Apache 和 Nginx Web 服务器中配置 CORS。
const express = require('express'); const app = express() app.get('/users', function (req, res, next) { res.json({msg: 'user get'}) }); app.post('/users', function (req, res, next) { res.json({msg: 'user create'}) }); app.put('/users', function (req, res, next) { res.json({msg: 'User update'}) }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') })
在 ExpressJS 中启用 CORS
让我们先来看一个没有启用 CORS 的 ExpressJS 应用:
npm install cors
在上面的例子中,我们为 POST
, PUT
, GET
方法启用了用户 API 端点,但没有为 DELETE
方法启用。
为了在 ExpressJS 应用程序中轻松启用 CORS,可以安装 cors
模块:
app.use(cors({ origin: '*' }));
为所有域启用 CORS
app.use(cors({ origin: 'https://app.techblik.com' }));
为单个域启用 CORS
如果需要允许来自 https://app.techblik.com
和 https://lab.techblik.com
域的 CORS 请求:
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ] }));
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'] }));
访问控制允许方法
要允许所有 HTTP 方法的 CORS 请求,可以直接在 ExpressJS 的 CORS 模块中省略 methods
选项。但如果只需允许特定的方法(例如 GET、POST、PUT):
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'] }));
访问控制允许标头
用于允许发送默认值之外的头部信息。
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true }));
访问控制允许凭据
如果不需要浏览器允许跨域凭据(即使 withCredentials
设置为 true
),可以忽略此选项。
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600 }));
访问控制最大年龄
提示浏览器将预检响应信息缓存指定秒数。如果不需要缓存响应,可以忽略此选项。
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['Content-Range', 'X-Content-Range'] }));
缓存的预检响应将在浏览器中保持 10 分钟。
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization', ] }));
访问控制公开标头
如果我们把通配符 *
放到 exposedHeaders
中, Authorization
标头不会被公开。 因此我们需要像下面这样显式地声明:
以上代码将同时公开所有标头和 Authorization 标头。
- 什么是 HTTP Cookie?
- Cookie 是服务器发送到客户端浏览器的一小段数据。在后续请求中,浏览器会在每个请求中发送与同一域名关联的所有 cookie。
- Cookie 有很多属性,可以根据需要以不同的方式配置 cookie 的行为。
name
: cookie 的名称。value
: cookie 的值,对应于 cookie 的名称。domain
: cookie 只会被发送到定义的域名。path
: cookie 只会在定义 URL 前缀路径之后发送。假设我们定义了 cookie 的路径为path='admin/'
,则 cookie 不会针对https://techblik.com/expire/
发送,但会针对以https://techblik.com/admin/
开头的 URL 发送。Max-Age/Expires
(秒数): cookie 的过期时间。cookie 的生命周期使得 cookie 在指定的时间后失效。HTTPOnly
(布尔值):当设置为true
时,该 HTTPOnly cookie 只能被后端服务器访问,而不能被客户端脚本访问。 Security (Boolean): 如果设置为true
, cookie 只会通过 SSL/TLS 域发送。SameSite
(字符串):用于启用或限制跨站点请求发送 cookie。要了解更多关于 cookie 的sameSite
属性,请参考 MDN。它接受三个选项:Strict
,Lax
和None
。 当 cookie 配置为sameSite=None
时,secure
的值必须设置为true
。
为什么为令牌使用 HTTPOnly Cookie?
将服务器发送的访问令牌存储在客户端存储中(例如 localStorage、IndexedDB 和未设置 HTTPOnly 的 cookie)更容易受到 XSS 攻击。如果网站的任何页面存在 XSS 漏洞,攻击者可能会滥用存储在浏览器中的用户令牌。
HTTPOnly cookie 只能由服务器/后端设置和获取,而不能在客户端访问。
- 客户端脚本无法访问 HTTPOnly cookie。因此 HTTPOnly cookie 更不容易受到 XSS 攻击,也更安全,因为它只能由服务器访问。
- 在启用 CORS 的后端中启用 HTTPOnly cookie。
- 要在 CORS 中启用 Cookie,需要在应用程序/服务器中进行以下配置:
- 将
Access-Control-Allow-Credentials
标头设置为true
。
Access-Control-Allow-Origin
和 Access-Control-Allow-Headers
不应设置为通配符。
const express = require('express'); const app = express(); const cors = require('cors'); app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization' ] })); app.post('/login', function (req, res, next) { res.cookie('access_token', access_token, { expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year secure: true, // set to true if your using https or samesite is none httpOnly: true, // backend only sameSite: 'none' // set to none for cross-request }); res.json({ msg: 'Login Successfully', access_token }); }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') });
Cookie 的 sameSite
属性应设置为 none
。
要将 sameSite
的值设为 none
,需要将 secure
的值设置为 true
:启用后端 SSL/TLS 证书以在域名中工作。
接下来,我们来看一个示例代码,该代码会在验证登录凭据后,在 HTTPOnly cookie 中设置访问令牌。
可以通过在后端语言和 Web 服务器中实现上述四个步骤来配置 CORS 和 HTTPOnly Cookie。
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://api.techblik.com/user', true); xhr.withCredentials = true; xhr.send(null);
您可以按照本教程中的步骤,在 Apache 和 Nginx 中启用 CORS。
fetch('http://api.techblik.com/user', { credentials: 'include' });
withCredentials
用于跨域请求
$.ajax({ url: 'http://api.techblik.com/user', xhrFields: { withCredentials: true } });
凭据 (Cookie, Authorization) 默认会与同源请求一起发送。对于跨域请求,必须将 withCredentials
指定为 true
。
axios.defaults.withCredentials = true
XMLHttpRequest API
Fetch API
jQuery Ajax
Axios
结论:希望上述文章能够帮助您理解 CORS 的工作原理,并了解如何在服务器中为跨域请求启用 CORS。此外,文章还阐述了为什么将 cookie 存储在 HTTPOnly 中是安全的,以及客户端如何使用 withCredentials
进行跨域请求。