前言

大多数同学可能使用过一些随机图API,类似于下面的URL:
- https://source.unsplash.com/random/1920x1080/
- https://api.yimian.xyz/img/
- https://img.fudaoyuan.icu/api/1/random?scale_min=1.5&webp=true&md=false&format=302

一打开就自动跳转到一张随机的图片,很多人喜欢使用它们来装饰自己的博客网站等。

这些随机图API原理基本都是:
通过MySQL随机取一条数据,甚者不需要MySQL,直接读取一个存放着所有图片url的txt文件并随机取出一行,最后向客户端发送301或者302响应,并带上Location: 图片URL的响应头,来跳转到对应的图片地址。

这样的随机图API网上还有很多,它们提供的图片或许不同,但是胜在使用门槛非常的低。

不像必应官方的每日一图API,你还得编写一个解析JSON的JS小函数来解析JSON的内容并显示图片,这些随机图API并不需要你懂任何的JS知识,只要会一点点百度复制粘贴就可以使用。

例如,可以加在HTML的img元素中

<img src="//img.fudaoyuan.icu/api/1/random?scale_min=1.5&webp=true&md=false&format=302"/>

再例如,还能加在CSS里

.my-cool-div{
    background: no-repeat center / 80% url(//img.fudaoyuan.icu/api/1/random?format=302&webp=true&md=false&scale_min=1.5);
}

那么,如此方便的随机图API为何会让现代前端抓狂,这就带来了CSRF与CORS问题

CSRF与CORS问题

CSRF - 跨站点请求伪造

跨站点请求伪造是一种攻击,其中第三方强制用户对他们当前登录的站点执行操作。
例如,恶意用户张三发现了B站一键三连的APi是这个(假设)
https://bilibili.com/index.php?action=onekeyThree&bvid=Bv12345678
只要是登陆的B站用户,使用Get请求这个Url就可以对Bv12345678的视频一件三连,那么聪明的张三就把这个Url放在了自己的博客上,像这样

<img src="https://bilibili.com/index.php?action=onekeyThree&bvid=Bv12345678">

那么只要访问这个博客的人近期登陆过B站并且cookie没有过期,那就会自动的给张三的视频一键三联了。
这就是CSRF攻击,CSRF 攻击针对的是状态更改请求,而不是直接窃取数据,因为攻击者(张三)看不到伪造请求的响应。

为了解决上述问题,于是就有了CORS - 跨源资源共享

CORS - 跨源资源共享

跨源资源共享仅适用于浏览器上下文,是一种允许一个源向另一个源发出请求的安全机制。
所有浏览器都遵循单一来源策略,这意味着默认情况下脚本无法向其他来源发出请求 - 但如果服务器提供正确配置的 CORS 标头,则可以有选择地放宽此策略。

简单来说,也就是浏览器现在默认不允许网站随便请求非网站当前域名的get与post请求了(加载来自cdn的js脚本等除外),除非网站设置了对应的CORS Header头,允许别的网站的跨域请求。

这样CSRF攻击没有了,但是需要加载随机图的前端程序员就头疼了,因为浏览器不允许ajax请求非自己域名下的图片。但是有的同学会发现,像下面的随机图API加在img标签里虽然跨域,但还是可以正常加载的

<img src="//img.fudaoyuan.icu/api/1/random?scale_min=1.5&webp=true&md=false&format=302"/>

什么双标.jpg 我就只是想读个图片啊啊啊

当然,有聪明的同学可能想到了,我可以先用img标签加载随机图片API绕过CORS,然后等图片加载完了,再把图片读到js里进行操作(), 最后再导回img标签展示,这样不就可以完美解决CORS问题吗

Chrome:你想得美,你以为我预判不到吗,画布污染警告

绕了一大圈,想读取非同源图片,似乎根本就绕不开CORS的问题。那么有没有什么方法,可以绕过CORS

写一封Email给随机图API的站长?让他们改CORS标头,但是这好像不太可能....

还有没有其他方法呢,还真有,让自己的服务器代理请求远程图片并返回,服务器可没有什么蛋疼的CORS限制,这样不就解决同源问题了。

使用服务器代理请求图片

Github上有个很详细的介绍Proxy的Gist,可以去看看

https://gist.github.com/jimmywarting/ac1be6ea0297c16c477e17f8fbe51347

我只用过倒数第二个CloudFlare的Worker的CORS代理 cloudflare-cors-anywhere

https://github.com/Zibri/cloudflare-cors-anywhere

这个代理用起来还是蛮方便的,只需要在CloudFlare新建一个Worker,然后粘贴代码就可以运行了。

你需要请求CORS资源只需要在Worker的地址加个 ? 后再加上需要代理的网址,如下,就可以返回被代理的图片地址了,解决CORS问题

https://xxx.workers.dev/?https://img.fudaoyuan.icu/api/1/random?scale_min=1.5&webp=true&md=false&format=302

CORS的问题解决了,但是又遇到了新的问题,那就是缓存

如果你尝试多刷新几次,你就会发现你的随机图片API总是返回同一张图片,也就是你的请求被浏览器缓存

没错,这就是这个脚本的第一个坑,其实也不能算坑,毕竟脚本作者也不会想到居然有人用这个来专门获取每一次302跳转的图片地址

针对这个问题,我们可以在服务端返回no-cache的响应头,也可以在客户端(JS)加这个头进去,如果你选择在客户端加这个头,那么你就会掉到这个CloudFlare Worker代理脚本的第二个坑里去

  await fetch('https://xxx.workers.dev/?https://img.fudaoyuan.icu/api/1/random?scale_min=1.5&webp=true&md=false&format=302', {
    method: 'GET',
    redirect: 'follow',
     headers: {
       "Cache-Control": "no-cache"
     }
  });

如上,我在我的前端JS里加入了 no-cache的请求头,请求是不会被缓存了,但是,CORS错误又回来了。

折腾半天,我才发现错误的原因在于,我加的这个no-cache的请求头,触发了Chrome的预检操作。

之前不会触发预检操作是因为,之前的请求没有加Header,属于CORS定义里的 简单请求,而加了Header,则不属于简单请求,则会在请求之前触发一个预检操作


然后这个白痴CloudFlare Worker代理脚本,则是无脑转发请求到源服务器,再把CORS头加上再转发回来。

预检请求的请求方式是 OPTIONS 发到源服务器,而有些服务器只放行GetPost请求。碰到OPTIONS判断不属于GetPost直接就返回405状态码回去了。
然后Chrome发现,服务端预检返回的居然是405而不是200,判定服务端不允许CORS请求,这也导致代理脚本CORS错误的原因。

那么解决办法就显而易见了,我们需要修改代理脚本,让它只要遇到OPTION请求就无脑返回200就好了。主要代码如下

if(isOPTIONS){
    var myHeaders = new Headers();
    myHeaders = fix(myHeaders);
    return new Response("helo",
        {status: 200, headers: myHeaders}
    );
}

可能有的小伙伴不知道改在哪里,这里也贴出修改好的Git提交:
https://gitea.fudaoyuan.icu/fudaoyuan.icu/cloudflare-cors-anywhere/commit/df0eb405d1c8ebed334ba7422d716e9c88a14d5f


代码萌新,热爱折腾,喜欢花草和养鱼