使用 curl 获取 123 网盘不限速下载链接
123 网盘具有上传和下载不限速的特点,而且新用户注册送 2T 的网盘空间,但是非会员只能下载 100M 以内的文件不限速,要么开会员,要么按次收费下载,那有没有什么方法绕过收费,实现任意大小的文件无限制下载呢?
该网盘在下载资源的时候会校验下载者是否为 vip 用户,如果是 vip 用户,那么下载别人分享的链接资源时,不限制下载大小,而我们实现无限制下载的的原理是 vip 用户可以获取该资源的下载链接,而通过获取到这个下载链接,就可以实现非 vip 用户的无限制下载,所以在进行本教程操作之前,需要先开通一个 vip,作为一个 24 小时生成下载链接的服务端。
然后以下面的无提取码的示例链接为例,该文件大小 18.30G:
https://www.123684.com/s/fBcNTd-T4MUh

一、使用 curl 提交 POST 请求进行登录
我们知道 POST 请求的信息是携带在信息体中的,因此我们需要知道,在登录 123 网盘的时候,携带用户名的字段是什么,携带密码的字段又是什么?
在浏览器中输入资源的分享链接,我们点击下载后可以看到,需要我们进行付费才能进行单次下载:
如果我们是 vip,且为登录状态的话,就可以直接进行下载,因此我们需要先利用 curl 工具进行 POST 登录,我们点击登录跳转到登录界面,填入账号和密码:
先不要进行登录,因为我们需要捕捉登录时提交的 POST 表单信息,因此我们按下 F12,进入开发者界面,我们用的是 Microsoft Edge,勾选 保存日志
和 选择 Fetch/XHR
:
勾选 保存日志
是因为登录完成后浏览器会降登录的 POST 请求删除掉,而选择 Fetch/XHR
是为了筛选出 所有的 GET 和 POST 请求,避免被其他无用的资源信息而干扰。
然后接下来我们选择登录,我们可以在调试器界面看到一个 sign_in
的请求,应该是和登录操作相关的请求:
我们点进去观察,发现这是一个 POST 请求,在 响应标头
部分,我们发现了请求成功后返回了一个 token
,猜测这个 token
应该是登录后继续和服务器通信的凭证:
接下来我们点开 负载
部分,这个部分会展示 GET 和 POST 请求所提交的字段和信息,我们可以看到,我们浏览器发送了一个携带三个信息的 POST 请求实现了登录:
接下来我们可以测试一下,是否能够通过 curl 构造 POST 请求进行登录,首先我们利用上面的三个字段构造一个 POST 请求:
curl -X POST https://login.123pan.com/api/user/sign_in -d "passport=myName&password=myPwd&remember=true"
结果如下:
通过上面的结果,我们可以发现我们已经登录成功了,而且返回了两个比较重要的数据,第一个是登录状态的超时时间,第二个是超时时间的时间戳(1970年1月1日午夜到现在所经过的秒数)表示形式,第三个是后续用于验证身份的 token
,时间戳转换工具可以参考下面网址:
https://tool.lu/timestamp/#:~:text=时间戳,是从1970
二、分析登录后如何获取到高速下载链接
通过 curl 能够实现成功登录后,我们继续回到浏览器,并且手动登录上账号,由于我们现在登录的是 vip 账号,当我们点击下载的时候,浏览器就会对资源进行下载操作,我们需要观察,浏览器是如何得到无限制的下载链接的,我们同样按 F12 打开调试器界面,然后点击下载,对发送的请求进行捕获:
可以看到我们捕获了六个请求,我们依次查看 响应
部分,可以看到,第三个请求中的 响应
部分包含了一个 DownLoadURL
字段,那么这个字段的大概率就是和下载链接相关的字段了,我们继续查看下一个请求:
请求的响应部分是一段 html 代码,应该是我们发送了请求,然后接收到了这段 html,对我们的请求 url 进行了一些操作,为了证实我们的猜测,我们点开 标头
部分,看看发送了什么请求:
原来是将上一个请求返回的 DownLoadURL
字段重新进行了一次 GET 请求,然后返回了一堆 html 代码,我们继续看下面一个请求的 响应
部分:
我们又看到了一个新的 redirect_url
,顾名思义,是一个重定位的 url 地址,经过测试,这条地址是我们最终进行资源下载的地址,我们可以在浏览器的下载界面右键刚刚下载的文件,复制链接,比较两个链接就会发现是同一个:
既然我们已经找到了最终的下载链接,那么我们现在开始逆向,浏览器是经过了哪些操作,得到了这个下载链接。
我们首先看一下是发送了什么请求返回了这个链接,我们点回该请求的 标头
字段:
可以看到,浏览器发送了一个返回状态码为 210
的 GET 请求,得到了最终的下载地址,那么这个用于 GET 请求的地址又是哪里来的呢?我们继续往上追:
来到了刚刚返回的一堆 html 代码,并没有返回我们刚刚的上一个 GET 请求的 url 地址,但是这一段 html 代码是我们通过提交下面这个 GET 请求得到的:
那就奇怪了,我们对一个 url 提交了一个 GET 请求,得到了一段 html 代码,然后不知道从哪里冒出来一个新的 url ,对这个新的 url 进行 GET 请求并重定向后得到了最终的地址,那我们有充分的理由怀疑,这一段 html 代码肯定是对 标头
请求得 url 进行了某些不为人知的操作,才得到了一个新的 url,那么我们必须对这段 html 进行分析,看看它到底做了什么才行。
查看返回的 html 代码也可以通过在浏览器中输入:view-source:url,这样会把对这个 url 的 GET 返回的 html 源代码直接展示出来,适合查看
当然虽然我们也不是专门写网页端代码的专家,但是也不是说完全看不懂,为了提高效率,我们直接把这段 html 代码丢到 GPT 中逐行解析,分析如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>index</title>
</head>
<script type="text/javascript">
// 关闭页面,根据不同的浏览器进行不同的操作
function ClosePage(){
if (navigator.userAgent.indexOf("MSIE") > 0){
if (navigator.userAgent.indexOf("MSIE 6.0") > 0){
window.opener = null;
window.close();
} else {
window.open('', '_top');
window.top.close();
}
} else if (navigator.userAgent.indexOf("Firefox") > 0) {
window.location.href = 'about:blank';
} else {
window.opener = null;
window.open('', '_self', '');
window.close();
}
}
// 用于从 URL 中获取特定查询字符串(?号后面跟着的字符串)的值
function GetQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
var context = "";
if (r != null)
context = r[2];
reg = null;
r = null;
return context == null || context == "" || context == "undefined" ? "" : context;
}
</script>
// 在 html 中添加一个 div 标签
<body>
<div id="app"></div>
</body>
<script type="text/javascript">
const targetOrigin = '*';
// 用于初始化 AJAX 请求的错误处理
function _errorAjaxInit () {
let protocol = window.location.protocol;
if (protocol === 'file:') return;
if (!window.XMLHttpRequest) {
return;
}
let xmlhttp = window.XMLHttpRequest;
let _oldSend = xmlhttp.prototype.send;
let _handleEvent = function (event) {
try {
if (event && event.currentTarget && (event.currentTarget.status < 200 || event.currentTarget.status > 299)) {
window.parent.postMessage({status:event.currentTarget.status,type:'fail',statusText: event.currentTarget.statusText,resourceUrl:event.currentTarget.responseURL || event.currentTarget.ajaxUrl}, targetOrigin);
}
} catch (e) {
window.parent.postMessage({status:event.currentTarget.status,type:'fail',statusText: 'error',resourceUrl:event.currentTarget.responseURL || event.currentTarget.ajaxUrl}, targetOrigin);
console.log('Tools error: ' + e);
}
};
xmlhttp.prototype.send = function () {
this.addEventListener('error', _handleEvent);
this.addEventListener('load', _handleEvent);
// this.addEventListener('abort', _handleEvent);
return _oldSend.apply(this, arguments);
};
var _oldOpen = xmlhttp.prototype.open;
xmlhttp.prototype.open = function (method, url) {
_oldOpen.apply(this, arguments);
this.ajaxUrl = url;
};
}
_errorAjaxInit();
// 从 url 中获取查询字符串 params 和 is_s3 的值
const params = GetQueryString('params');
const isS3 = GetQueryString('is_s3');
// 对 params 进行 Base64 解码
let dCode = atob(params);
// decodeURI() 函数可对 encodeURI() 函数编码过的 URI 进行解码
let dCodeStr = decodeURI(dCode);
if(params){
if(isS3 == 1){ // s3(在我们的请求链接中 isS3 查询字符串一直为 0)
// 将两次解码后的 url 赋值给了 a 标签,并把 a 标签隐藏了
var download_file = {};
if (typeof (download_file.iframe) == "undefined") {
var iframe = document.createElement("a");
download_file.iframe = iframe;
document.body.appendChild(download_file.iframe);
}
download_file.iframe.style.display = "none";
// 对 a 标签进行了模拟点击,这通常用于下载文件
download_file.iframe.href = dCodeStr;
iframe.click();
}else{
// 我们的浏览器执行了接下来的代码
var xhr = new XMLHttpRequest();
// 对两次解码后的 url 进行了一次 GET 请求
xhr.open('GET', dCodeStr, true);
xhr.onreadystatechange = async function () {
if(xhr.readyState == 3){ // 请求状态为 3 表示处理请求
if(xhr.status == 200){
xhr.abort();
window.parent.postMessage({status:xhr.status,type:'success',statusText: xhr.statusText,resourceUrl:xhr.responseURL}, targetOrigin);
var download_file = {};
if (typeof (download_file.iframe) == "undefined") {
var iframe = document.createElement("a");
download_file.iframe = iframe;
document.body.appendChild(download_file.iframe);
}
download_file.iframe.style.display = "none";
download_file.iframe.href = dCodeStr;
iframe.click();
}else if(xhr.status == 210){ // 我们可以看到我们浏览器的这次 GET 请求返回码为 210
if(xhr.response.indexOf("code") > -1){ // 返回的 code 为 0
let resData = JSON.parse(xhr.response);
if(resData.code == 0){
window.parent.postMessage({status:xhr.status,type:'success',statusText: xhr.statusText,resourceUrl:xhr.responseURL}, targetOrigin);
var download_file = {};
if (typeof (download_file.iframe) == "undefined") { // 检查download_file对象是否已经有一个iframe属性
var iframe = document.createElement("a");
download_file.iframe = iframe;
document.body.appendChild(download_file.iframe); // 将 a 标签添加到 body 中
}
download_file.iframe.style.display = "none";
download_file.iframe.href = resData.data.redirect_url; // 把重定向的下载地址赋值给 a 标签
iframe.click(); // 对 a 标签进行了模拟点击,这通常用于下载文件
}else{
window.parent.postMessage({status:resData.code,type:'fail',statusText: resData.message,resourceUrl:xhr.responseURL}, targetOrigin);
}
}
}
}
};
xhr.ontimeout = function(event){
window.parent.postMessage({status:0,type:'fail',statusText: '超时',resourceUrl:dCodeStr}, targetOrigin);
};
xhr.timeout = 5 * 1000;
xhr.send(null);
}
}
</script>
</html>
通过上面对 html 代码的分析可以得出,其核心逻辑就是对我们该次 GET 请求的 url 进行了两次解码操作,用到的函数分别是 JS 脚本语言中的 atob
和 decodeURI
,我们可以在下面的网址方便的查询这两个函数的作用,并通过示例代码来对我们的 url 进行解码,非常方便:
https://www.runoob.com/jsref/met-win-atob.html#:~:text=Window ato
通过查询可知,atob
函数是对 Base64 进行解码,而 encodeURI
函数用于将 url 中的中文进行编码,提高 url 的兼容性,而 decodeURI
进行反向操作,那接下来明了了,原来这段 html 的操作是对该次请求 GET 请求的 url 中的查询字符串 param 进行了两次解码操作,得到了一个新的 url,然后再对这个新的 url 发起了一个 GET 请求,得到了最终的下载链接,最后将下载链接赋值给一个 a 标签,并将其隐藏,最后通过模拟点击 a 标签来实现下载的操作。
为了验证我们的想法,我们进入刚才的网址,找到 atob
函数,然后找到示例代码,将查询字符串中的 params
丢进去进行 Base64 解码操作:
果然如此,decodeURI
也是同样的操作,那么通过对下面 url 的 GET 请求返回的一段 html 代码的分析,成功解析出了下一步的资源下载地址,那这段 GET 请求的 url 又是哪里来的呢?
我们之前分析过,来源于上面这个请求的 响应
:
而这个请求的 标头
如下:
可以看到这个请求的 URL 应该是固定的,它是一个 POST 请求,所以我们还要看看它所携带的 请求负载
:
这个 POST 请求有五个 请求负载
,而这部分不同的资源文件是不同的,所以我们还需要往上追这部分是哪里来的,然后不管我们怎么翻上面两个请求的内容,也找不到关于这五个 请求负载
的一点信息了,那就只用一种可能了,那就是这部分信息在我们执行登录操作的时候就已经发给我们了,所以我们还得退出登录,看看到底浏览器发送了哪个请求,得到了这个链接。
于是我们在浏览器中退出登录登录,输入资源链接,点击下载,然后点击登录,继续弹出登录界面(必须从资源链接这里进入登录界面),按下 F12,点击登录:
因为我们是从 sign_in
登录后才能进行其他操作,所有我们从这个请求开始往下排查,我们发现在 发起程序
的 请求发起程序链
里面有一条引人注目的 http 链接,而且后面的查询字符串带了登录成功后返回给我们的 token
,这就有意思了,我们逆向了这么久,这个 token
一直没有用到,才想起来,原来在这里憋着坏呢?
我们直接把这个 url 丢到浏览器里面,回车:
可以看到,这个链接是已经处于 vip 账号登录阶段的链接,直接把 url 扔到浏览器可以是作为一个 GET 请求,那么这个返回的 html 已登录界面底层有没有藏着我们需要的信息呢?
我们直接用 curl 工具通过之前的登录获得的 token
,构建一个该 url 的 GET 语句:
curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken
返回的 html 页面袋面如下(去掉无关信息后):
window.g_useSSR = true;
window.g_initialProps = {
"res": {
"code": 0,
"message": "",
"data": {
"UserNickName": "17********8",
"UserID": 1840800539,
"ShareName": "95女鬼枪剑士.7z",
"HasPwd": false,
"Expiration": "2099-12-12T08:00:00+08:00",
"CreateAt": "2024-09-23T20:22:31+08:00",
"Expired": false,
"ShareKey": "fBcNTd-T4MUh",
"HeadImage": "https:\u002F\u002Fstatics.123pan.com\u002Fstatic-by-custom\u002Fdefault_avatar.png",
"IsVip": true,
"DisplayStatus": 1,
"isPayShare": 0,
"isReward": 1,
"payAmount": 0,
"resourceDesc": "",
"isChange": false,
"payStatus": 0,
"IsSVip": false,
"BackendImgUrl": "",
"DomainStatus": 0,
"HTTPSStatus": 0,
"SharePageDomain": "",
"IsShowAdvertisement": true,
"domainList": ["www.123pan.com", "www.123pan.cn", "www.123865.com", "www.123684.com", "www.123pan.cn"],
"shareBackground": [{
"type": 1,
"viewImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F提取码页面.png",
"viewUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
"payImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F付费资源页面.png",
"payUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
"fileImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F文件列表t.png",
"fileUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember"
}, {
"type": 2,
"viewImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F提取码页面.png",
"viewUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
"payImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F付费资源页面.png",
"payUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember",
"fileImg": "https:\u002F\u002Fapp.123pan.com\u002Fmanager\u002Fshare_background\u002F文件列表t.png",
"fileUrl": "https:\u002F\u002Fwww.123pan.com\u002Fmember"
}, {
"type": 3,
"viewImg": "",
"viewUrl": "",
"payImg": "",
"payUrl": "",
"fileImg": "",
"fileUrl": ""
}, {
"type": 4,
"viewImg": "",
"viewUrl": "",
"payImg": "",
"payUrl": "",
"fileImg": "",
"fileUrl": ""
}],
"staticDomain": "statics.123957.com",
"sharePayDomain": "www.123pan.cn"
}
},
"reslist": {
"code": 0,
"message": "",
"data": {
"Next": "18446744073709551615",
"Len": 1,
"IsFirst": true,
"Expired": false,
"InfoList": [{
"FileId": 8171949,
"FileName": "95女鬼枪剑士.7z",
"Type": 0,
"Size": 19650979289,
"ContentType": "0",
"S3KeyFlag": "1840800539-0",
"CreateAt": "2024-09-21T20:37:19+08:00",
"UpdateAt": "2024-09-23T20:22:31+08:00",
"Etag": "deb837b0b0d3f092787000e76fb9da25",
"DownloadUrl": "",
"Status": 5,
"ParentFileId": 8171631,
"Category": 10,
"PunishFlag": 0,
"StorageNode": "m54",
"PreviewType": 0,
"checked": true
}]
}
},
"publicPath": "https:\u002F\u002Fwww.123684.com\u002Fb\u002Fapi\u002F"
};
看到我们需要找的 5 个构造 POST 请求的 负载信息
赫然在列,当然也可以直接在浏览器中按 F12,找到这段返回的 html 代码。
那现在获取链接的完整流程已经明了,如下:
- 首先我们通过构造一个用于登录的 POST 请求进行登录,会返回一个
token
。 - 我们用这个
token
和https://www.123684.com/s/fBcNTd-T4MUh?
网址构造一个 GET 请求,里面会返回 5 个和特定资源相关的负载信息
。 - 利用上面返回的 5 个
负载信息
和https://www.123684.com/b/api/share/download/info?3654564465=1727318211-6994024-1490120705
网址(经测试,后面的查询字符串可去除)构造一个 POST 请求,返回DownloadURL
。 - 对这个
DownloadURL
发起一个 GET 请求,会返回一段 html 代码,而这段 html 代码会对DownloadURL
中的查询字符串params
进行两次解码,得到一个新的 url。 - 我们继续对这个新的 url 发起一个 GET 请求,返回状态代码为
210
,并得到最终的资源下载地址。
为了验证我们推断出的流程有没有错误,我们接下来使用 curl 工具对整个流程进行测试,得到最终的下载地址。
三、通过 curl 验证整个流程
step 1 进行登录
首先我们构造一个 POST 登录请求:
curl -X POST https://login.123pan.com/api/user/sign_in -d "passport=myName&password=myPwd&remember=true"
会返回一个 token
:
step 2 得到用于获取下载链接的 POST 请求参数
我们用这个 token
和 https://www.123684.com/s/fBcNTd-T4MUh?
网址构造一个 GET 请求:
curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken
返回 5 个和特定资源相关的 负载信息
,分别是 Etag
、FileID
、S3keyFlag
、ShareKey
和 Size
,用于构造构造获取下载链接的 POST 请求:
step 3 构造 POST 请求获取下载链接
首先我们通过得到的 负载信息
构造一个 POST 请求:
curl -X POST https://www.123684.com/b/api/share/download/info -d "Etag=deb837b0b0d3f092787000e76fb9da25&FileID=8171949&S3KeyFlag=1840800539-0&ShareKey=fBcNTd-T4MUh&Size=19650979289" -H "app-version:3" -H "platform:web" -H "authorization:Bearer myToken"
可以看到我们这次的 POST 请求额外多指定了三个请求头,分别是 app-version
、platform
和 authorization
,经测试至少需要指定这三个请求头,前面两个分别表示请求的版本和平台,第三个传递的是登陆后返回的 token
,提交请求后得到 DownloadURL
:
step 4 对下载链接的请求字符串进行解码
对这个 DownloadURL
构造一个 GET 请求:
curl -X GET myDownloadURL
返回了我们熟悉的那段 html 代码:
因此实际上我们不需要发起这个请求,直接将 DownloadURL
里面的查询字符串 params
进行 Base64 解码即可:
step 5 通过解码后的链接得到最终下载链接
我们继续对这个解码后的 url 构造一个 GET 请求:
curl -X GET "myDecodeURL"
当然,由于我们的 url 里面有 & 特殊字符,所有我们需要把 url 用双引号引起来,或者在每个 & 字符前面加一个 ^ 符号也行,终于得到了我们最终的下载链接:
得到最终的下载链接后把其中的转义字符 \
去掉即可。
四、获取带提取码的下载链接
获取带提取码的下载链接步骤基本一致,唯一不同点在 step 2 得到用于获取下载链接的 POST 请求参数
步骤有所差别。
通过之前类似的方法进行分析,找到如下的 GET 请求:
在分析获取无提取码资源下载链接时,获取 Etag
、FileID
、S3keyFlag
、ShareKey
和 Size
这五个请求参数是构造一个 GET 请求来实现的:
curl -X GET https://www.123684.com/s/fBcNTd-T4MUh?token=myToken
而带提取码的文件在获取这五个参数的时候也采用的是 GET 方法,对 https://www.123684.com/b/api/share/get?
网址发起请求,并且附带了 10 个参数(2972272565=1728980514-657908-1876151490 参数经测试可以忽略),分别是 limit
、next
、orderBy
、orderDirection
、shareKey
、SharePwd
、ParentFileId
、Page
、event
、operateType
,得到的结果如下:
因此,通过构造如下命令即可得到 step 2
中的五个参数:
curl -X GET "https://www.123684.com/b/api/share/get?limit=100&next=0&orderBy=file_name&orderDirection=asc&shareKey=fBcNTd-wXMUh&SharePwd=S3kU&ParentFileId=0&Page=1&event=homeListFile&operateType=1"
返回的结果如下:
五、获取无提取码的文件夹下载链接
如果分享的不是单个文件,而是整个文件夹,那么我们应该如何处理呢?我们对下载文件夹的资源链接进行分析:
我们看到,在按照前面的方法获取 Etag
、FileID
、S3keyFlag
、ShareKey
和 Size
这五个参数后,浏览器对 https://www.123pan.com/b/api/file/batch_download_share_info
网址发起了 POST 请求,并携带了参数 ShareKey
和 fileIdList
:
然后得到了合并文件的下载链接:
解析该下载链接得到最终的下载链接,我们可以看到该链接下载的是一个 .rar
后缀的文件,是将需要下载的文件夹压缩成了一个 .rar
文件,并给出对应的下载链接: