在《低成本防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) }
五、参考资料
Token Authentication for Cached Private Content and APIs
转载请注明:虚拟号之家 » 基于Cloudflare Wokers的Token Authentication(URL鉴权)