如何使用HTTPOnly Cookie启用CORS以保护令牌?
在本文中,我们将了解如何使用HTTPOnly cookie启用CORS(跨源资源共享)来保护我们的访问令牌。
现在,后端服务器和前端客户端部署在不同的域上。因此,服务器必须启用CORS以允许客户端在浏览器上与服务器通信。
此外,服务器正在实施无状态身份验证以实现更好的可伸缩性。令牌存储和维护在客户端,而不是像会话一样存储在服务器端。出于安全考虑,最好将令牌存储在HTTPOnly cookie中。
为什么跨源请求被阻止?
假设我们的前端应用程序部署在https://app.yaoweibin.com
。加载在https://app.yaoweibin.com
中的脚本只能请求同源资源。
每当我们尝试发送跨源请求到另一个域https://api.yaoweibin.com
或另一个端口https://app.yaoweibin.com:3000
或另一个协议http://app.yaoweibin.com
时,浏览器会阻止跨源请求。
但为什么浏览器阻止的相同请求可以通过任何后端服务器使用curl请求发送,或者通过像Postman这样的工具发送而没有任何CORS问题?实际上,这是为了保护用户免受跨站点请求伪造(CSRF)等攻击。
让我们举个例子,假设任何用户在其浏览器中登录了自己的PayPal账户。如果我们可以从加载在另一个域malicious.com
的脚本中向paypal.com
发送跨源请求,而不会出现任何CORS错误/阻止,就像发送同源请求一样。
Attackers可以通过将其转换为短URL来隐藏实际URL,并轻松发送它们的恶意页面https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account
。当用户点击恶意链接时,加载在域malicious.com
中的脚本将向PayPal发送跨源请求,将用户的金额转移到攻击者的PayPal账户。所有已登录其PayPal账户并点击此恶意链接的用户都将失去他们的钱。任何人都可以在没有PayPal账户用户知情的情况下轻松窃取钱财。
出于上述原因,浏览器阻止所有跨源请求。
什么是CORS(跨源资源共享)?
CORS是一种基于头部的安全机制,服务器用它告诉浏览器从受信任的域发送跨源请求。
启用CORS头的服务器用于避免浏览器阻止跨源请求。
CORS的工作原理是什么?
由于服务器已在其CORS配置中定义了其受信任的域。当我们发送请求到服务器时,响应将在其标头中告诉浏览器所请求的域是受信任的还是不受信任的。
有两种类型的CORS请求:
- 简单请求
- 预检请求
简单请求:
- 浏览器将请求发送到跨源域,带有origin(https://app.yaoweibin.com)。
- 服务器返回相应的响应,带有允许的方法和允许的源。
- 收到请求后,浏览器将检查发送的origin标头值(https://app.yaoweibin.com)和接收到的access-control-allow-origin值(https://app.yaoweibin.com)是否相同或通配符(*)。否则,它将抛出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.yaoweibin.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、授权标头)的布尔值。
- Access-Control-Max-Age: 告诉浏览器缓存预检响应的时间。
- Access-Control-Expose-Headers: 指定客户端脚本可以访问的标头。
要在apache和Nginx web服务器中启用CORS,请参考此链接。
在ExpressJS中启用CORS
让我们以没有CORS的示例 app 为例:
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')
})
在上面的示例中,我们已经为POST、PUT、GET方法启用了users API端点,但未启用DELETE方法。
为了在ExpressJS应用程序中轻松启用CORS,您可以安装cors
npm install cors
Access-Control-Allow-Origin
为所有域启用CORS
app.use(cors({
origin: '*'
}));
为单个域启用CORS
app.use(cors({
origin: 'https://app.yaoweibin.com'
}));
如果要允许来自https://app.yaoweibin.com和https://lab.yaoweibin.com的CORS
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
]
}));
Access-Control-Allow-Methods
要为所有方法启用CORS,请在ExpressJS的CORS模块中省略此选项。但是,要启用特定的方法(GET,POST,PUT)。
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
],
methods: ['GET', 'PUT', 'POST']
}));
Access-Control-Allow-Headers
用于允许发送实际请求时的默认标头之外的其他标头。
app.use(cors({
origin: [
‘https://app.yaoweibin.com',
‘https://lab.yaoweibin.com'
],
methods: [‘GET', ‘PUT', ‘POST'],
allowedHeaders: [‘Content-Type', ‘Authorization', ‘x-csrf-token']
}));
Access-Control-Allow-Credentials
如果不想告诉浏览器在withCredentials设置为true时允许请求中的凭证,则可以省略此项。
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true
}));
Access-Control-Max-Age
让浏览器在缓存指定秒数的预检响应信息。如果不想缓存响应,则可以省略此项。
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600
}));
预检响应将在浏览器中缓存10分钟。
Access-Control-Expose-Headers
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['Content-Range', 'X-Content-Range']
}));
如果在exposedHeaders中放入通配符(*),则不会公开Authorization标头。因此,我们必须明确公开如下:
app.use(cors({
origin: [
'https://app.yaoweibin.com',
'https://lab.yaoweibin.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['*', 'Authorization', ]
}));
以上将公开所有标头,包括Authorization标头。
什么是HTTP cookie?
Cookie是服务器发送给客户端浏览器的一小块数据。在后续请求中,浏览器将在每个请求中发送与同一域关联的所有cookie。
Cookie具有其属性,可以根据需要定义使cookie以不同方式工作。
- Name:cookie的名称。
- value:与cookie名称相对应的数据
- Domain:仅将cookie发送到定义的域
- Path:仅在定义的URL前缀路径之后发送cookie。假设我们将cookie路径定义为path='admin/'。则不会将cookie发送到https://yaoweibin.cn/expire/,但会随着URL前缀https://yaoweibin.cn/admin/一起发送。
- Max-Age/Expires(以秒为单位):cookie的过期时间。cookie的生命周期在指定时间后将使cookie无效。
- HTTPOnly(布尔值):当为true时,后端服务器可以访问HTTPOnly cookie,但客户端脚本不能访问。
- Secure(布尔值):仅在SSL/TLS域上发送cookie时,值为true。
- sameSite(string [Strict, Lax, None]):用于启用/限制在跨站点请求上发送的cookie。要了解有关cookiesameSite的更多详细信息,请参见MDN。它接受三个选项Strict、Lax、None。将cookie配置sameSite=None的cookie的secure值设置为true。
为什么使用HTTPOnly cookie来存储令牌?
将从服务器发送的访问令牌存储在客户端存储中,如本地存储,索引数据库和cookie(HTTPOnly不设置为true),对XSS attack更容易受到攻击。假设您的任何一个页面都容易受到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不能使用通配符(*)。
Cookie sameSite属性应为None。
要将sameSite值设置为none,请将secure值设置为true:在域名中启用具有SSL/TLS证书的后端。
让我们看一个示例代码,在检查登录凭据后设置HTTPOnly cookie中的访问令牌。
您可以通过在后端语言和Web服务器中实现上述四个步骤来配置CORS和HTTPOnly cookie。
您可以按照上述步骤查看此链接将Apache和Nginx配置为启用CORS。
对于跨源请求的withCredentials
默认情况下,同源请求会发送凭据(Cookie、授权)。对于跨源请求,我们必须将withCredentials设置为true。
XMLHttpRequest API
var xhr = new XMLHttpRequest();
xhr.open(‘GET', ‘http://api.yaoweibin.com/user', true);
xhr.withCredentials = true;
xhr.send(null);
Fetch API
fetch(‘http://api.yaoweibin.com/user', {
credentials: ‘include'
});
JQuery Ajax
$.ajax({
url: ‘http://api.yaoweibin.com/user',
xhrFields: {
withCredentials: true
}
});
Axios
axios.defaults.withCredentials = true
结论
希望上述文章能帮助您了解CORS的工作原理,并在服务器中启用CORS以进行跨源请求。为什么将cookie存储在HTTPOnly中是安全的,以及客户端如何使用withCredentials进行跨源请求。