当前位置:   article > 正文

EdgeOne 边缘函数 - 如何实现“防盗”与访问控制

EdgeOne 边缘函数 - 如何实现“防盗”与访问控制

目前,各大主流厂商都推出了自己的边缘 Serverless 服务,如 CloudFlare Workers、 Vercel EdgeRuntime 等;腾讯云 EdgeOne 边缘函数提供了部署在边缘节点的 Serverless 代码执行环境,只需编写业务函数代码并设置触发规则,即可在靠近用户的边缘节点上弹性、安全地运行代码。

阅读本文之前,请先了解以下背景知识:
1. EdgeOne 边缘函数 

基于边缘函数的访问控制方案自定义程度高、安全性强,在保证加速效果的同时,最大程度保护资源的安全性,同时还可以降低带宽和存储费用,有效节约业务成本。

一、前言

多数情况下,通过 CDN 分发的内容默认为公开资源,任何拿到 URL 的用户都可以进行访问。但是有时候一些需要保护的资源也会使用 CDN 进行加速,这时,在 CDN 层进行访问控制就显得十分必要:

  1. 保护内容:对于网站的付费内容,需要进行保护以防止未授权的用户访问;

  2. 防止盗链:有些网站可能会直接使用他人的加速链接来提供内容,以此规避存储和带宽的费用。对资源进行访问控制,可以有效防止"盗链"行为的发生;

  3. 节省成本:如果内容被大量的未授权用户访问,带宽和存储费用可能会大幅度增加。通过访问控制,可以限制访问量,从而有效节省成本;

  4. 提高安全性:通过防止恶意用户对网站进行攻击,CDN 访问控制可以提高网站的整体安全性

总的来说,CDN 访问控制可以对特定资源进行保护,因此多数 CDN 厂商都提供了访问控制的功能。

但是,目前大多数 CDN 厂商提供的访问控制能力都相对简单,且不可定制,因此不能完全满足复杂的业务场景。对那些有较高安全需求的客户来说,这些通用的访问控制方案显得有些 “鸡肋”,不得不在源站服务或网关层实现自己的访问控制。

二、基于边缘函数的访问控制

一站式边缘安全加速平台 EdgeOne 被称为 下一代 CDN,其中,EdgeOne 边缘函数 提供了边缘代码执行环境,支持开发者编写、部署 JavaScript 代码。

得益于 EdgeOne 边缘函数的可编程能力,使得 定制 专属的边缘访问控制方案成为可能,更便捷更灵活更安全

访问控制在 EdgeOne 边缘节点完成,无需回源,最大程度 保护内容节省资源保护源站

访问控制效果:

三、代码示例

常见的访问控制有 远程鉴权 、 UA 黑白名单 、 Referer 防盗链 等,相当一部分 CDN 使用者有基于这些常规访问控制方案进一步定制的需求,例如:远程鉴权时,添加或修改请求头等。

使用 EdgeOne 边缘函数可以很方便的实现这些方案,并提供进一步定制的能力。

1. 代码示例旨在展示边缘函数的能力,仅列举常见方案的基本实现;
2. 文章中涉及的代码可在  边缘函数最佳实践仓库 查看;

3.1 远程鉴权

远程鉴权是指将用户请求转发至指定的鉴权服务器,由鉴权服务器对用户请求进行校验。校验通过即允许访问,校验失败拒绝访问或进行其他限制,有效避免资源被非授权用户访问。

使用边缘函数实现远程鉴权:

  1. const AUTH_URL = "...AUTH_URL...";
  2. async function handleRequest(request) {
  3. const checkAuthRes = await fetch(AUTH_URL, {
  4. // 可自定义头部
  5. headers: request.headers,
  6. });
  7. if (checkAuthRes.status === 200) {
  8. return await fetch(request);
  9. }
  10. return new Response(null, {
  11. status: checkAuthRes.status,
  12. });
  13. }
  14. addEventListener("fetch", (e) => {
  15. e.respondWith(handleRequest(e.request));
  16. });

3.2 UA 白名单

限制携带特定 User-Agent 的请求才能访问内容,可以有效防止恶意爬虫或者恶意软件的访问。同时可以根据不同 UA 头部提供定制化内容,例如:只允许移动设备或者特定版本的浏览器访问。

  1. const ALLOWED_UA = ["Mobile/15E148"];
  2. async function handleRequest(request) {
  3. const userAgent = request.headers.get("User-Agent") || "";
  4. if (ALLOWED_UA.some((allowedUA) => userAgent.includes(allowedUA))) {
  5. return await fetch(request);
  6. } else {
  7. return new Response(null, { status: 403 });
  8. }
  9. }
  10. addEventListener("fetch", (event) => {
  11. event.respondWith(handleRequest(event.request));
  12. });

3.3 Referer 防盗

防盗链技术主要用于防止其他网站未经许可地使用你的网站资源,如图片、视频、文档等。

检查 HTTP 请求头中的 Referer 字段是最常见的防盗链方法。当浏览器发送请求获取网页上的资源时,它会在请求头中带上 Referer 字段,该字段指示了请求是从哪个页面发出的。通过检查 Referer 字段,服务器可以确定请求是否来自合法的源。

使用边缘函数实现 Referer 防盗链:

  1. const RE_URL = "...URL...";
  2. async function handleRequest(request) {
  3. const referer = request.headers.get("Referer");
  4. if (!referer) {
  5. return Response.redirect(RE_URL);
  6. }
  7. const urlInfo = new URL(request.url);
  8. const isHostMatch = new RegExp(`^https?:\/\/${urlInfo.hostname}`).test(
  9. referer
  10. );
  11. if (!isHostMatch) {
  12. return Response.redirect(RE_URL);
  13. }
  14. return await fetch(request);
  15. }
  16. addEventListener("fetch", (event) => {
  17. event.respondWith(handleRequest(event.request));
  18. });

3.4 URL 鉴权

URL 鉴权通过校验 URL 中的加密串和时间戳的方式,达到保护源站资源的目的。URL 鉴权方式有 Type A/B/C/D 四种,主要区别是签名计算方式和签名存放位置不同。

使用边缘函数实现 Type A 鉴权:

  1. const SECRET_KEY = "YOUR_SECRET_KEY";
  2. async function handleRequest(request) {
  3. const url = new URL(request.url);
  4. const params = url.searchParams;
  5. const authKey = params.get("auth_key");
  6. if (!authKey) {
  7. return new Response(null, { status: 401 });
  8. }
  9. const [timestamp, nonce, signature] = authKey.split("-");
  10. const now = Math.floor(Date.now() / 1000);
  11. if (now > timestamp) {
  12. return new Response(null, { status: 401 });
  13. }
  14. const encoder = new TextEncoder();
  15. const data = encoder.encode(
  16. `${url.pathname}-${timestamp}-${nonce}-${SECRET_KEY}`
  17. );
  18. const hashBuffer = await crypto.subtle.digest("MD5", data);
  19. const hashArray = Array.from(new Uint8Array(hashBuffer));
  20. const expectedSignature = hashArray
  21. .map((b) => b.toString(16).padStart(2, "0"))
  22. .join("");
  23. if (signature !== expectedSignature) {
  24. return new Response(null, { status: 401 });
  25. }
  26. return await fetch(request);
  27. }
  28. addEventListener("fetch", (event) => {
  29. event.respondWith(handleRequest(event.request));
  30. });

服务端生成的鉴权 URL 为: .../TencentCloud.svg?auth_key=1698224616-0-311b8888ccc20768895f1f3bd82dbb76

客户端发起请求:https://.../TencentCloud.svg?auth_key=1698224616-0-311b8888ccc20768895f1f3bd82dbb76

触发边缘函数执行,对 auth_key 进行校验,签名错误或过期都会被阻止访问:

3.5 Signed Cookies

Google Cloud Signed-Cookies
AmazonCloudFront Private-Content Signed-Cookies
Cloudflare-One authorization-cookie

使用 EdgeOne 边缘函数不仅可以定制常规的访问控制方案,还可以部署更加复杂的专属方案,以 Signed Cookies 方案为例进行演示。

Signed Cookies 方案采用非对称加密的方式进行访问控制(私钥签名、公钥验签),该方案允许为多个文件提供安全的访问权限,而不需要更改每个 URL。

Signed Cookies 的工作原理是:当用户请求访问受保护的内容前,应用程序或网站会使用私钥生成一组特殊的 HTTP Cookies(EO-SignatureEO-Policy),这些 Cookies 包含访问权限的信息,且被加密并签名。随后,这些 Cookies 被发送到用户的浏览器,用户的浏览器在后续的请求中会自动发送这些 Cookies。

当边缘函数处理一个带有 Signed Cookies 的请求时,会验证这些 Cookies 的签名,然后根据 Cookies 中的信息决定是否允许访问请求的内容。

EO-SignatureEO-Policy 在源站生成,EO-Policy 表示访问控制规则,由服务端对 policy.json 数据进行 url-safe base64 编码生成,policy.json 数据形式为:

  1. {
  2. "Statement": [
  3. {
  4. "Resource": "/TencentCloud.svg",
  5. "Condition": {
  6. "DateGreaterThan": {
  7. "EpochTime": 1698318316
  8. },
  9. "DateLessThan": {
  10. "EpochTime": 1698404716
  11. }
  12. }
  13. }
  14. ]
  15. }

EO-Signature 存储签名信息,由服务端对 policy.json 数据使用私钥进行签名,然后进行 url-safe base64 编码生成,EO-SignatureEO-Policy 种植后,站点上的受限制资源请求都会携带这两个特殊的 HTTP Cookies,形如:

  • EO-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiL1RlbmNlbnRDbG91ZC5zdmciLCJDb25kaXRpb24iOnsiRGF0ZUdyZWF0ZXJUaGFuIjp7IkVwb2NoVGltZSI6MTY5ODMxODMxNn0sIkRhdGVMZXNzVGhhbiI6eyJFcG9jaFRpbWUiOjE2OTg0MDQ3MTZ9fX1dfQ__; Path=/;

  • EO-Signature=HddK~ZBb2Rc7C3Vpat2QnD2rMqnGAjktNbJ9jON2sh1r0o3YX3ohRkvDowdp2PkhBx1erUQbyiL2T6gSW-E5vbYQfrOPC2aA-7a9ZtnkImDZlk-BVfEL1cZgq3bjsG9FxIy-kc0B5~JgHtQvRsecEyie0yRyZJZoiMoWxN~haP3ykimFa4kl49eIwtOxCnXTvclHea4Rb1jp7iqTz2W-LevjIeSsqe9dL2qehMSamy-wbOCUTZBSqeoB7dS1hMH4SMSIaBUieBFqs-yrDCtA-E4nNd6FjHoMQi4DJcIkSJiwZl371OJdYyRDX65pWHaUHwbr3GrC4XgpyGWwvMhmaw__; Path=/;

使用边缘函数在边缘节点上对 Signed Cookies 进行校验:

  1. // RSA 公钥
  2. const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
  3. ...
  4. -----END PUBLIC KEY-----`;
  5. ...
  6. async function verifySignature(request) {
  7. ...
  8. const eoSignature = getCookieValue(cookies, 'EO-Signature');
  9. const eoPolicy = getCookieValue(cookies, 'EO-Policy');
  10. ...
  11. const signatureBuffer = getSignatureBuff(eoSignature);
  12. const policyString = getPolicyStr(eoPolicy);
  13. const policyBuffer = stringToArrayBuffer(policyString);
  14. const publicKeyBuffer = pemToArrayBuffer(PUBLIC_KEY_PEM);
  15. const publicKey = await crypto.subtle.importKey(
  16. 'spki',
  17. publicKeyBuffer,
  18. {
  19. name: 'RSASSA-PKCS1-v1_5',
  20. hash: 'SHA-1',
  21. },
  22. true,
  23. ['verify'],
  24. );
  25. const signatureValid = await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signatureBuffer, policyBuffer);
  26. if (!signatureValid) {
  27. return null;
  28. }
  29. return JSON.parse(policyString);
  30. }
  31. function verifyPolicy(request, policy) {
  32. const { pathname } = new URL(request.url);
  33. const { Statement } = policy;
  34. const now = Math.floor(Date.now() / 1000);
  35. for (const statement of Statement) {
  36. if (statement.Resource === pathname) {
  37. const dateGreaterThan = statement.Condition.DateGreaterThan.EpochTime;
  38. const dateLessThan = statement.Condition.DateLessThan.EpochTime;
  39. if (now > dateGreaterThan &amp;&amp; now < dateLessThan) {
  40. return true;
  41. }
  42. return false;
  43. }
  44. }
  45. return true;
  46. }
  47. async function handleEvent(event) {
  48. const { request } = event;
  49. const policy = await verifySignature(request);
  50. if (!policy) {
  51. return event.respondWith(new Response(null, { status: 401 }));
  52. }
  53. const valid = verifyPolicy(request, policy);
  54. if (!valid) {
  55. return event.respondWith(new Response(null, { status: 403 }));
  56. }
  57. event.respondWith(await fetch(request));
  58. }
  59. addEventListener('fetch', handleEvent);

Signed Cookies 校验不通过,资源访问会被 Block:

四、总结

EdgeOne 边缘函数拥有 分布式部署贴近用户超低延迟弹性扩容Serverless 环境 等优势。

借助 EdgeOne 边缘函数,开发者可以在边缘节点定制访问控制方案,满足更复杂的业务需求,对自己的资源进行保护,降低带宽和存储费用,有效节约成本。

参考 API 列表

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/360909
推荐阅读
相关标签
  

闽ICP备14008679号