最新消息:

基于Cloudflare Wokers的Token Authentication(URL鉴权)

VPS xunihao 892浏览 0评论

在《低成本防CC攻击、薅羊毛风控反欺诈方案》提到,使用CloudFlare的Token Authentication机制来对URL做鉴权,防止CC攻击。

CloudFlare的Token Authentication有两种机制:

1、基于CloudFlare Workers

2、基于CloudFlare Firewall Rules

由于免费版的Firewall Rules、Rate Limiting Rules有诸多限制,基于低成本原则,没考虑开通Pro、Business或Enterprise account。

虽然CloudFlare Workers对免费用户有10万请求/每天的额度,但对大部分场景已经足够,即便升级,CloudFlare价格还比较公道。

一、Token Authentication的原理

CloudFlare Token Authentication在国内一般叫URL鉴权,可以用于防止CC攻击、防盗链等场景。

Token Authentication核心机制:
1、将DNS解析服务托管到CloudFlare,采用CloudFlare的CDN、DNS服务,以利用其边缘计算能力

2、CloudFlare 和 APP或应用服务器间共享相同的对称密钥

3、APP端在调用接口服务前,生成本次请求的Token签名,参与签名的要素包括:token有效期(Unix epoch timestamp)、对称密钥、URL地址、其他参数(例如IP、用户信息等),。签名采用HMAC加密。

此步一般在APP端本地按照报文签名规则生成。

怎样保证APP本地生成Token的安全性,又是另外一个话题,手段包括代码混淆、加密算法放到C++动态库完成等等。

4、APP在接口请求头或请求参数中带上签名sign1,调用相应接口

5、由于采用CloudFlare CDN服务,因此请求会通过CloudFlare 边缘结算网关,CloudFlare边缘计算网关会自动校验Token的有效性

CloudFlare Workers会根据token有效期、对称密钥、URL地址、其他参数,计算出对应签名sign2,比较sign1和sign是否相等,以及当前时间是否小于token有效期(是否在有效期内)

二、CloudFlare Edge 验证Token有效性 vs. 应用服务器自己验证Token有效性

从以上原理步骤可以看出,只要遵循同样的报文签名规则,由应用服务器直接验证Token有效性也可以,那么为何要采用CloudFlare Token Authentication服务呢?
核心原因:

CloudFlare Token Authentication是在CDN边缘完成Token有效性的,这样可以在CC攻击场景下,攻击根本没到达应用服务器就被CloudFlare 拦截,避免直接冲击应用服务器。

三、使用Cloudflare Wokers验证Token有效性

以下假定验证token有效性的URL地址为:/verify

步骤:

1、定义Wokers

a、登录->右下侧导航“Workers”->Create a Worker

b、在Create a Worker界面的Script输入Worker验证代码

// We'll need some super-secret data to use as a symmetric key.
const encoder = new TextEncoder()
const secretKeyData = encoder.encode('my secret symmetric key')

addEventListener('fetch', event => {
  event.respondWith(verifyAndFetch(event.request))
})

async function verifyAndFetch(request) {
  const url = new URL(request.url)

  // If the path doesn't begin with our protected prefix, just pass the request
  // through.
  if (!url.pathname.startsWith('/verify/')) {
    return fetch(request)
  }

  // Make sure we have the minimum necessary query parameters.
  if (!url.searchParams.has('mac') || !url.searchParams.has('expiry')) {
    return new Response('Missing query parameter', { status: 403 })
  }

  const key = await crypto.subtle.importKey(
    'raw',
    secretKeyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify'],
  )

  // Extract the query parameters we need and run the HMAC algorithm on the
  // parts of the request we're authenticating: the path and the expiration
  // timestamp.
  const expiry = Number(url.searchParams.get('expiry'))
  const dataToAuthenticate = url.pathname + expiry

  // The received MAC is Base64-encoded, so we have to go to some trouble to
  // get it into a buffer type that crypto.subtle.verify() can read.
  const receivedMacBase64 = url.searchParams.get('mac')
  const receivedMac = byteStringToUint8Array(atob(receivedMacBase64))

  // Use crypto.subtle.verify() to guard against timing attacks. Since HMACs use
  // symmetric keys, we could implement this by calling crypto.subtle.sign() and
  // then doing a string comparison -- this is insecure, as string comparisons
  // bail out on the first mismatch, which leaks information to potential
  // attackers.
  const verified = await crypto.subtle.verify(
    'HMAC',
    key,
    receivedMac,
    encoder.encode(dataToAuthenticate),
  )

  if (!verified) {
    const body = 'Invalid MAC'
    return new Response(body, { status: 403 })
  }

  if (Date.now() > expiry) {
    const body = `URL expired at ${new Date(expiry)}`
    return new Response(body, { status: 403 })
  }

  // We've verified the MAC and expiration time; we're good to pass the request
  // through.
  return fetch(request)
}

// Convert a ByteString (a string whose code units are all in the range
// [0, 255]), to a Uint8Array. If you pass in a string with code units larger
// than 255, their values will overflow!
function byteStringToUint8Array(byteString) {
  const ui = new Uint8Array(byteString.length)
  for (let i = 0; i < byteString.length; ++i) {
    ui[i] = byteString.charCodeAt(i)
  }
  return ui
}

 

c、Save and Deploy

d、返回到Create a Worker页面,点击刚创建的Worker,可以修改名称

2、关联域名和Workers

进入托管到CloudFlare的域名详情页->点击“Workers”->Add route

在“Add route”弹出框
route:token验证URL地址,例如 api.mydomain.com/verify/*

Worker:选择第一步创建的Worker

3、测试验证

四、在CloudFlare Workers生成Token

上面提到的方案,都是在APP端生成Token,因为如果由服务器端生成Token,很容易被模拟生成过程。但在某些场景下,需要在服务器端生成Token,例如防止页面图片盗链。
此种场景下可以在CloudFlare Workers中生成Token,代码:

addEventListener('fetch', event => {
  const url = new URL(event.request.url)
  const prefix = '/generate/'
  if (url.pathname.startsWith(prefix)) {
    // Replace the "/generate/" path prefix with "/verify/", which we
    // use in the first example to recognize authenticated paths.
    url.pathname = `/verify/${url.pathname.slice(prefix.length)}`
    event.respondWith(generateSignedUrl(url))
  } else {
    event.respondWith(fetch(event.request))
  }
})

async function generateSignedUrl(url) {
  // We'll need some super-secret data to use as a symmetric key.
  const encoder = new TextEncoder()
  const secretKeyData = encoder.encode('my secret symmetric key')
  const key = await crypto.subtle.importKey(
    'raw',
    secretKeyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )

  // Signed requests expire after one minute. Note that you could choose
  // expiration durations dynamically, depending on, e.g. the path or a query
  // parameter.
  const expirationMs = 60000
  const expiry = Date.now() + expirationMs
  const dataToAuthenticate = url.pathname + expiry

  const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(dataToAuthenticate))

  // `mac` is an ArrayBuffer, so we need to jump through a couple of hoops to get
  // it into a ByteString, and then a Base64-encoded string.
  const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)))

  url.searchParams.set('mac', base64Mac)
  url.searchParams.set('expiry', expiry)

  return new Response(url)
}

五、参考资料

Signing Requests

Token Authentication for Cached Private Content and APIs

worker-examples

转载请注明:虚拟号之家 » 基于Cloudflare Wokers的Token Authentication(URL鉴权)

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址