[{"content":"AI Learn Claude Code learn.shareai.run 林清华的博客技术博客 lintsinghua.github.io 图解 Agent\u0026#43;RAG\u0026#43;LLM 大模型面试题小林面试笔记是专注 AI Agent 开发的免费面试学习网站，提供 Agent、RAG、LLM 大模型、Claude Code 源码与实战等高频面试题，全部配手绘图解、通俗易懂，助你跑赢 AI Agent 开发面试。 xiaolinnote.com 公考 SaDuck - 公考知识库 saduck.top 后端 主页推荐阅读 ：类 Claude Code 的 Agent CLI，从 ReAct 到 Tool Calling、MCP、多模态、Skill、Memory、RAG、Multi-Agent、Chrome DevTools，适合想把 Agent 底层原理吃透的同学。 ：PaiFlow 是类 Dify、Coze、n8n 的企业级 AI Agent 工作流编排平台... javabetter.cn JavaGuide（Java 面试 \u0026amp; 后端通用知识体系）JavaGuide 是一份面向 Java 后端开发者和面试准备人群的学习指南，系统覆盖 Java 基础、集合、并发、JVM、MySQL、Redis、分布式、高并发、高可用、系统设计、消息队列、计算机基础和 AI 应用开发等核心知识，适合校招社招复习、查缺补漏和规划学习路线。 javaguide.cn 小林coding - 从图解计算机到后端面试全攻略图解计算机网络、操作系统、MySQL、Redis，覆盖后端技术面试，让天下没有难懂的八股文！ www.xiaolincoding.com 算法 《代码随想录》网站介绍代码随想录是一个免费开源的算法学习平台，由程序员Carl耗时5年精心打磨而成，提供科学刷题顺序、全套图文和视频题解、面试八股文大全、项目资源、训练营以及简历制作工具。涵盖从零基础到进阶的学习路径，包括算法、C\u0026#43;\u0026#43;、Java、大模型等专项训练，帮助用户高效提升编程技能和面试竞争力，旨在少走弯路，分享给更多需要的人。 programmercarl.com labuladong 的算法笔记 - 算法教程、AI 编程、编程语言系统学习平台系统化编程学习平台，涵盖 400\u0026#43; 算法教程、AI 编程实战、编程语言入门，配套可视化面板和刷题系统 labuladong.online 工具 ElegantResume Builder在线简历生成器 builder.elegantresume.pro ","permalink":"https://devx0.com/links/","summary":"\u003ch2 id=\"ai\"\u003eAI\u003c/h2\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://learn.shareai.run/zh/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://learn.shareai.run/favicon.ico?favicon.0b3bf435.ico\" alt=\"learn.shareai.run favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003eLearn Claude Code\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003elearn.shareai.run\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://lintsinghua.github.io/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://lintsinghua.github.io/favicon.ico\" alt=\"lintsinghua.github.io favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003e林清华的博客\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e技术博客\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003elintsinghua.github.io\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://xiaolinnote.com/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://xiaolinnote.com/favicon.ico\" alt=\"xiaolinnote.com favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003e图解 Agent\u0026#43;RAG\u0026#43;LLM 大模型面试题\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e小林面试笔记是专注 AI Agent 开发的免费面试学习网站，提供 Agent、RAG、LLM 大模型、Claude Code 源码与实战等高频面试题，全部配手绘图解、通俗易懂，助你跑赢 AI Agent 开发面试。\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003exiaolinnote.com\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003ch2 id=\"公考\"\u003e公考\u003c/h2\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://saduck.top/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://saduck.top/favicon64.ico\" alt=\"saduck.top favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003eSaDuck - 公考知识库\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003esaduck.top\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003ch2 id=\"后端\"\u003e后端\u003c/h2\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://javabetter.cn/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://cdn.paicoding.com/tobebetterjavaer/images/favicon.ico\" alt=\"javabetter.cn favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003e主页\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e推荐阅读 ：类 Claude Code 的 Agent CLI，从 ReAct 到 Tool Calling、MCP、多模态、Skill、Memory、RAG、Multi-Agent、Chrome DevTools，适合想把 Agent 底层原理吃透的同学。 ：PaiFlow 是类 Dify、Coze、n8n 的企业级 AI Agent 工作流编排平台...\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003ejavabetter.cn\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://javaguide.cn/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://javaguide.cn/favicon.ico\" alt=\"javaguide.cn favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003eJavaGuide（Java 面试 \u0026amp; 后端通用知识体系）\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003eJavaGuide 是一份面向 Java 后端开发者和面试准备人群的学习指南，系统覆盖 Java 基础、集合、并发、JVM、MySQL、Redis、分布式、高并发、高可用、系统设计、消息队列、计算机基础和 AI 应用开发等核心知识，适合校招社招复习、查缺补漏和规划学习路线。\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003ejavaguide.cn\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://www.xiaolincoding.com/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://cdn.xiaolincoding.com/icon.webp\" alt=\"www.xiaolincoding.com favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003e小林coding - 从图解计算机到后端面试全攻略\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e图解计算机网络、操作系统、MySQL、Redis，覆盖后端技术面试，让天下没有难懂的八股文！\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003ewww.xiaolincoding.com\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003ch2 id=\"算法\"\u003e算法\u003c/h2\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://programmercarl.com/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://file1.kamacoder.com/i/web/20250421212153.png\" alt=\"programmercarl.com favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003e《代码随想录》网站介绍\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e代码随想录是一个免费开源的算法学习平台，由程序员Carl耗时5年精心打磨而成，提供科学刷题顺序、全套图文和视频题解、面试八股文大全、项目资源、训练营以及简历制作工具。涵盖从零基础到进阶的学习路径，包括算法、C\u0026#43;\u0026#43;、Java、大模型等专项训练，帮助用户高效提升编程技能和面试竞争力，旨在少走弯路，分享给更多需要的人。\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003eprogrammercarl.com\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://labuladong.online/zh/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://labuladong.online/favicon.ico\" alt=\"labuladong.online favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003elabuladong 的算法笔记 - 算法教程、AI 编程、编程语言系统学习平台\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e系统化编程学习平台，涵盖 400\u0026#43; 算法教程、AI 编程实战、编程语言入门，配套可视化面板和刷题系统\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003elabuladong.online\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003ch2 id=\"工具\"\u003e工具\u003c/h2\u003e\n\u003cdiv class=\"link-card\"\u003e\n  \u003ca href=\"https://builder.elegantresume.pro/\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cimg class=\"link-card-favicon\" src=\"https://builder.elegantresume.pro/favicon.ico\" alt=\"builder.elegantresume.pro favicon\" width=\"56\" height=\"56\" loading=\"lazy\"\u003e\n    \u003cdiv class=\"link-card-text\"\u003e\n      \u003cdiv class=\"link-card-title\"\u003eElegantResume Builder\u003c/div\u003e\u003cdiv class=\"link-card-desc\"\u003e在线简历生成器\u003c/div\u003e\u003cdiv class=\"link-card-domain\"\u003e\n        \u003csvg class=\"link-card-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/\u003e\u003cpolyline points=\"15 3 21 3 21 9\"/\u003e\u003cline x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/\u003e\u003c/svg\u003e\n        \u003cspan\u003ebuilder.elegantresume.pro\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/a\u003e\n\u003c/div\u003e","title":"网站收录"},{"content":" 本文由 AI 辅助总结自 TuneForge 项目实践。\n问题 写一个音乐播放器项目 TuneForge，音频存在 115 网盘。想让浏览器 \u0026lt;audio src=\u0026quot;...\u0026quot;\u0026gt; 直接播网盘里的 FLAC，数据不经过服务器中转。\n理想的数据流是这样的：\n服务端只负责\u0026quot;告诉浏览器去哪里拿\u0026quot;，实际音频数据从 CDN 直通浏览器——零服务器带宽。\n但现实是，网盘的 CDN 有防盗链。\n防盗链机制 115 网盘 CDN URL 长这样：\nhttps://cdnfhnfile.115.com/xxx.flac?t=1234567890\u0026amp;f=3\u0026amp;k=abc123... 其中 f 参数控制校验强度：\nf 值 校验要求 f=0 无限制 f=1 请求时 User-Agent 必须与获取 CDN URL 时一致 f=2 需要 Referer 匹配来源站点 f=3 UA + Cookie 都必须与获取 CDN URL 时一致 旧的 webapi.115.com/files/download 接口返回的 CDN URL 永远是 f=3。这意味着：\n服务端用带 Cookie 的请求拿到了 CDN URL 浏览器拿到 URL 后自己去请求 CDN CDN 校验 Cookie——浏览器不会跨域携带 .115.com 的 cookie 到 115cdn.net 结果：403 Forbidden 用 f=1 就行 f=1 只校验 User-Agent。浏览器请求 CDN 时带的 UA 和我们获取 CDN URL 时传的 UA 一致就行——这个我们完全可以控制。\n所以问题的核心变成了：怎么拿到 f=1 的 CDN URL？\n寻找 f=1 接口 翻阅开源社区找到两个关键线索：\np115strmhelper：一个 MoviePilot 插件，用 proapi.115.com/android/2.0/ufile/download 拿到 f=1 的 CDN URL p115rsacipher：115 客户端 Python 库，实现了 proapi 需要的 RSA 加密协议 proapi 接口不是随便调的——请求体需要 RSA-1024 加密，响应也需要解密。\n整体架构 最终的 302 直链流程：\n关键点：\n透传 UA：服务端用浏览器的 UA 去获取 CDN URL，而不是自己的默认 UA。这样浏览器后续请求 CDN 时 UA 自然匹配 no-referrer：防止浏览器携带 Referer 头，避免触发 f=2 级别校验 服务端不中转数据：sendRedirect 之后服务端就退场了，音频流在浏览器和 CDN 之间直传 RSA 加密协议逆向 这是整个方案的技术核心。proapi 的加密/解密流程完全用纯 Kotlin 实现，零外部依赖。\n常量 从 p115rsacipher 的 Python 源码中提取以下常量：\n// RSA-1024 公钥 val RSA_N = BigInteger(\u0026#34;8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1c...\u0026#34;, 16) val RSA_E = BigInteger(\u0026#34;10001\u0026#34;, 16) // 用于 XOR 的固定密钥 val RSA_KEY = byteArrayOf(0x8d, 0xa5, 0xa5, 0x8d) // 4 字节 val G_KEY_L = byteArrayOf(0x78, 0x06, 0xad, 0x4c, 0x33, 0x86, 0x5d, 0x18, 0x4c, 0x01, 0x3f, 0x46) // 12 字节 val G_KTS = byteArrayOf(0xf0, 0xe5, 0x69, 0xae, ...) // 128 字节，用于派生解密 key 加密流程（客户端 → 服务端） 对应代码：\nfun rsaEncrypt115(payload: String): String { val data = payload.toByteArray(Charsets.UTF_8) // ② + ③: XOR with RSA_KEY then reverse val tmp = xorWithKey(data, RSA_KEY) tmp.reverse() // ④: XOR body with G_KEY_L val body = xorWithKey(tmp, G_KEY_L) // ⑤: prepend 16 zero bytes val xorData = ByteArray(16 + body.size) System.arraycopy(body, 0, xorData, 16, body.size) // ⑥ + ⑦: RSA encrypt and Base64 return Base64.getEncoder().encodeToString(rsaEncrypt(xorData)) } RSA 加密分块（每块最多 117 字节），PKCS#1 v1.5 填充为 128 字节：\nfun rsaEncrypt(data: ByteArray): ByteArray { val result = ByteArrayOutputStream() var offset = 0 while (offset \u0026lt; data.size) { val chunkSize = minOf(117, data.size - offset) // PKCS#1 v1.5: 0x00 || 0x02[padLen] || 0x00 || message val padLen = 126 - chunkSize val padded = ByteArray(128) padded[0] = 0 for (i in 0 until padLen) padded[1 + i] = 2 padded[1 + padLen] = 0 System.arraycopy(data, offset, padded, 2 + padLen, chunkSize) val intVal = BigInteger(1, padded) val encrypted = intVal.modPow(RSA_E, RSA_N) // Right-align into 128 bytes result.write(fixed128(encrypted.toByteArray())) offset += chunkSize } return result.toByteArray() } 解密流程（服务端响应 → 明文） 对应代码：\nfun rsaDecrypt115(cipherText: String): String { val cipherData = Base64.getDecoder().decode(cipherText) // RSA decrypt, strip sign byte val decrypted = rsaDecrypt(cipherData) // Skip PKCS#1 padding var pos = 0 while (pos \u0026lt; decrypted.size \u0026amp;\u0026amp; decrypted[pos] == 0x02.toByte()) pos++ if (pos \u0026lt; decrypted.size \u0026amp;\u0026amp; decrypted[pos] == 0x00.toByte()) pos++ // Extract randKey and derive keyL val randKey = decrypted.copyOfRange(pos, pos + 16) pos += 16 val keyL = genKey(randKey, 12) // Reverse encryption steps val body = decrypted.copyOfRange(pos, decrypted.size) val tmp = xorWithKey(body, keyL) tmp.reverse() return String(xorWithKey(tmp, RSA_KEY), Charsets.UTF_8) } fun genKey(randKey: ByteArray, skLen: Int): ByteArray { return ByteArray(skLen) { i -\u0026gt; val idx = i * skLen val lenPos = skLen * (skLen - 1) - i * skLen val x = ((randKey[i].toInt() and 0xff) + (G_KTS[idx].toInt() and 0xff)) and 0xff (G_KTS[lenPos].toInt() xor x).toByte() } } RSA 解密注意：只剥离 BigInt sign byte 这是踩过的坑之一。Java/Kotlin 的 BigInteger.toByteArray() 会在正数前加 0x00 sign byte。RSA 解密后得到的字节是 [0x00][0x02]*padding[0x00][randKey:16][body]。\nPython 的做法是只剥离第一个 0x00（sign byte），保留 PKCS#1 填充：\n# Python: 只找第一个 0x00 b = to_bytes(p, exact_len) data += memoryview(b)[b.index(0)+1:] 我一开始错误地剥离了全部 PKCS#1 padding（0x00 0x02..0x02 0x00），导致 randKey 读到了 0x02 开头的字节，genKey 派生出的 keyL 完全错误，解密结果乱码。\n// 正确：只剥离 BigInt sign byte fun rsaDecrypt(data: ByteArray): ByteArray { val decrypted = BigInteger(1, chunk).modPow(RSA_E, RSA_N) val decBytes = decrypted.toByteArray() val idx = decBytes.indexOf(0) val stripped = if (idx \u0026gt;= 0) decBytes.copyOfRange(idx + 1, decBytes.size) else decBytes // stripped = [0x02]*[0x00][randKey:16][body] — PKCS#1 padding still there return stripped } 然后在 rsaDecrypt115() 中再单独处理 PKCS#1 padding 的跳过。\nXOR 的坑：chunk-aligned 分段逻辑 这是最隐蔽的 bug。Python p115rsacipher.xor() 的分段方式很特别：\ndef xor(src, key): secret = bytearray() i = len(src) \u0026amp; 0b11 # 残留字节数 = len % 4 if i: secret += bytes_xor(src[:i], key[:i]) # 残留部分用 key 前 i 字节 for i, j, s in acc_step(i, len(src), len(key)): secret += bytes_xor(src[i:j], key[:s]) # 剩余按 key 长度分组，每组从 key[0] 开始 return secret 以 22 字节 payload {\u0026quot;pick_code\u0026quot;:\u0026quot;test123\u0026quot;} 和 12 字节 key 为例：\n每轮分组的 key 索引都从 0 开始，不是连续递增的。我最初用 i % keyLen 循环，导致密钥偏移累积，整个加密结果错误。\n正确实现：\nfun xorWithKey(src: ByteArray, key: ByteArray): ByteArray { val result = ByteArray(src.size) val remainder = src.size and 3 // len \u0026amp; 0b11 var pos = 0 // 残留部分 for (i in 0 until remainder) { result[pos] = (src[pos].toInt() xor key[i].toInt()).toByte() pos++ } // 剩余按 key 长度分组，每组从 key[0] 开始 while (pos \u0026lt; src.size) { for (i in key.indices) { if (pos \u0026gt;= src.size) break result[pos] = (src[pos].toInt() xor key[i].toInt()).toByte() pos++ } } return result } randKey 的坑：必须全零 加密时 payload 前面要 pad 16 字节的 randKey。Python 源码用的固定全零：\nxor_text = bytearray(16) # 全零, 不是随机! xor_text += xor(tmp, G_key_l) 我最初用了 SecureRandom().nextBytes(randKey)，结果是随机的。服务端 RSA 解密后拿到 [randKey][body]，用 randKey 调 genKey 派生 keyL 解密 body。全零 randKey 时服务端有硬编码路径直接上 G_KEY_L，但随机 randKey 会导致 keyL 派生不一致，解密出乱码 JSON。\n修正——直接用全零：\nval xorData = ByteArray(16 + body.size) // Java 的 ByteArray 默认全零 System.arraycopy(body, 0, xorData, 16, body.size) ProAPI 调用 private fun downloadUrlF1(pickCode: String, cookie: String, ua: String): String? { val payload = \u0026#34;\u0026#34;\u0026#34;{\u0026#34;pick_code\u0026#34;:\u0026#34;$pickCode\u0026#34;}\u0026#34;\u0026#34;\u0026#34; val encrypted = rsaEncrypt115(payload) // URL encode, 注意空格转 %20 而非 + val formEncoded = URLEncoder.encode(encrypted, \u0026#34;UTF-8\u0026#34;).replace(\u0026#34;+\u0026#34;, \u0026#34;%20\u0026#34;) val body = \u0026#34;data=$formEncoded\u0026#34;.toRequestBody( \u0026#34;application/x-www-form-urlencoded\u0026#34;.toMediaType() ) val request = Request.Builder() .url(\u0026#34;http://proapi.115.com/android/2.0/ufile/download\u0026#34;) .header(\u0026#34;User-Agent\u0026#34;, ua) // 透传浏览器的 UA .post(body) .build() val response = client.newCall(request).execute() // ... 解密响应，提取 CDN URL } 两个注意点：\n端点是 android/2.0/ufile/download，不是 app/chrome/downurl——后者仅支持 aid=1 的 pickcode 且字段名不同（pickcode vs pick_code） User-Agent 透传浏览器的——这是整个方案成立的前提。获取 CDN URL 时用什么 UA，浏览器请求 CDN 时也得用什么 UA OkHttp 配置 private val client = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .cookieJar(cookieJar) .followRedirects(false) // 关键! .build() followRedirects(false) 很重要。服务端调用 proapi 拿到 CDN URL 后，需要的是这个 URL 字符串本身，然后返回给浏览器。如果在服务端自动跟随了 CDN 的重定向，那就变成服务端代理下载了。\n自定义 CookieJar 负责把 115 登录的 auth cookie（UID/CID/SEID/KID）注入到所有 115 相关域名的请求中，同时保留 WAF 返回的 acw_tc token。\nCDN URL 缓存 CDN URL 有有效期（约 50 分钟），同一首歌没必要每次播放都重新加密请求：\nprivate data class CachedUrl(val url: String, val expireAt: Long) private val downloadUrlCache = ConcurrentHashMap\u0026lt;String, CachedUrl\u0026gt;() fun downloadUrl(pickCode: String, cookie: String, userAgent: String): String? { val cacheKey = \u0026#34;$pickCode:$userAgent\u0026#34; // 命中缓存直接返回 getCachedDownloadUrl(cacheKey)?.let { return it } // 优先 proapi (f=1) val f1Url = downloadUrlF1(pickCode, cookie, userAgent) if (f1Url != null) { downloadUrlCache[cacheKey] = CachedUrl(f1Url, System.currentTimeMillis() + 50 * 60 * 1000L) return f1Url } // 回退 webapi (f=3, 调试用) // ... } 注意缓存 key 包含了 userAgent。不同浏览器的 UA 不同，对应拿到的 CDN URL 也不同（f=1 的 URL 绑定了 UA）。\nController 层 @GetMapping(\u0026#34;/play/{id}\u0026#34;) fun play(@PathVariable id: String, request: HttpServletRequest, response: HttpServletResponse) { val row = musicRepository.findById(id) ?: throw NoSuchElementException(\u0026#34;Track not found\u0026#34;) val pickCode = row.fileName.substringAfter(\u0026#34;:\u0026#34;) // 1. 透传浏览器 UA val browserUa = request.getHeader(\u0026#34;User-Agent\u0026#34;) ?: \u0026#34;Mozilla/5.0\u0026#34; // 2. 获取 f=1 CDN URL（优先走缓存） val downloadUrl = p115Client.downloadUrl(pickCode, cookie, browserUa) ?: throw NoSuchElementException(\u0026#34;Download URL not available\u0026#34;) // 3. 设置 no-referrer 策略 response.setHeader(\u0026#34;Referrer-Policy\u0026#34;, \u0026#34;no-referrer\u0026#34;) // 4. 302 重定向——服务端退场 response.sendRedirect(downloadUrl) } 三步走完，服务端的活就干完了。剩下的音频流传输是浏览器和 CDN 之间的事。\n为什么要 302 直链而不是服务端代理？ 方案 服务器带宽 延迟 服务端代理（拿流→转发） 每字节都走服务器 多一跳 302 直链 零 低 一首 FLAC 动辄 30-50MB，同时几个人播放下服务器的带宽就吃紧了。302 方案让数据从 CDN 直通浏览器，服务端只负责那几百字节的 HTTP 响应头和 RSA 加解密——CPU 消耗几乎可以忽略不计。\n总结 整个方案的核心思想是防盗链降级：通过逆向网盘的 proapi 加密协议，把 CDN URL 的防盗链等级从需要 Cookie 的 f=3 降到只需 UA 的 f=1，然后透传浏览器 UA 拿到 URL，302 丢给浏览器自己去拉数据。\n涉及的坑：\nXOR chunk-aligned 分段逻辑：残留字节单独处理，剩余按 key 长度分组且每组从 key[0] 重新开始 RSA 解密只剥离 sign byte：不要动 PKCS#1 padding，那是后面步骤的输入 randKey 必须全零：不能用随机值，否则服务端 keyL 派生不一致 端点和字段名：android/2.0/ufile/download + pick_code，不是 app/chrome/downurl + pickcode 完整实现见 TuneForge - P115Client.kt。\n","permalink":"https://devx0.com/posts/115-cdn-302-redirect-play/","summary":"逆向 115 网盘 proapi RSA 加密协议，CDN 防盗链 f=3 降至 f=1，浏览器 302 直链零带宽播放。XOR/randKey/RSA sign byte 等坑点实录。","title":"115 CDN 防盗链逆向与 302 直链播放方案"},{"content":" 本文转载自 博客园 charygao1990，仅作学习备忘之用。\n有时因为网络原因，比如公司 NAT，或其它啥的，需要使用代理。Docker 的代理配置，略显复杂，因为有三种场景。但基本原理都是一致的，都是利用 Linux 的 http_proxy 等环境变量。\nDockerd 代理 在执行 docker pull 时，是由守护进程 dockerd 来执行。因此，代理需要配在 dockerd 的环境中。而这个环境，则是受 systemd 所管控，因此实际是 systemd 的配置。\nsudo mkdir -p /etc/systemd/system/docker.service.d sudo touch /etc/systemd/system/docker.service.d/proxy.conf 在这个 proxy.conf 文件（可以是任意 *.conf 的形式）中，添加以下内容：\n[Service] Environment=\u0026#34;HTTP_PROXY=http://proxy.example.com:8080/\u0026#34; Environment=\u0026#34;HTTPS_PROXY=http://proxy.example.com:8080/\u0026#34; Environment=\u0026#34;NO_PROXY=localhost,127.0.0.1,.example.com\u0026#34; 其中 http://proxy.example.com:8080 要换成可用的免密代理。通常使用 cntlm 在本机自建免密代理，去对接公司的代理。\n重启生效：\nsudo systemctl daemon-reload sudo systemctl restart docker 检查是否生效：\nsudo systemctl show --property=Environment docker docker info | grep Proxy 注意： 通过 daemon.json 方式配置的优先级会高于通过 systemd 配置（Docker Engine 23.0+ 支持）。\nContainer 代理 在容器运行阶段，如果需要代理上网，则需要配置 ~/.docker/config.json。以下配置只在 Docker 17.07 及以上版本生效。\n{ \u0026#34;proxies\u0026#34;: { \u0026#34;default\u0026#34;: { \u0026#34;httpProxy\u0026#34;: \u0026#34;http://proxy.example.com:8080\u0026#34;, \u0026#34;httpsProxy\u0026#34;: \u0026#34;http://proxy.example.com:8080\u0026#34;, \u0026#34;noProxy\u0026#34;: \u0026#34;localhost,127.0.0.1,.example.com\u0026#34; } } } 这个是用户级的配置，修改后立即生效，但只针对以后启动的 Container，对已经启动的 Container 无效。\n此外，容器的网络代理，也可以直接在其运行时通过 -e 注入：\ndocker run --env HTTP_PROXY=\u0026#34;http://proxy.example.com:8080\u0026#34; \u0026lt;some-image\u0026gt; 两种方法分别适合不同场景：\nconfig.json 非常方便，适合个人开发环境 -e 注入更显式，适合 CI/CD 自动构建环境或上线环境 警告： 无论是 docker run 还是 docker build，默认是网络隔绝的。如果代理使用的是 localhost:3128 这类，会无效。仅限本地的代理必须加上 --network host 才能正常使用。\nDocker Build 代理 虽然 docker build 的本质也是启动一个容器，但是环境会略有不同，用户级 config.json 配置无效。在构建时，需要通过 --build-arg 注入：\ndocker build . \\ --build-arg \u0026#34;HTTP_PROXY=http://proxy.example.com:8080/\u0026#34; \\ --build-arg \u0026#34;HTTPS_PROXY=http://proxy.example.com:8080/\u0026#34; \\ --build-arg \u0026#34;NO_PROXY=localhost,127.0.0.1,.example.com\u0026#34; \\ -t your/image:tag 重要： 不要在 Dockerfile 中使用 ENV 指令配置代理！这会把代理服务器打包进镜像，可能造成安全隐患，也可能导致通过此镜像创建的容器访问不到私有代理服务器。\n总结 场景 配置方式 生效时机 docker pull systemd 或 daemon.json 重载 systemd + 重启 dockerd docker run ~/.docker/config.json 或 -e 注入 下次启动容器生效 docker build --build-arg 注入 执行时立即生效 关键是理解每种场景的代理作用范围——是从 systemd 层面、用户配置层面，还是运行参数层面。\n","permalink":"https://devx0.com/posts/docker-proxy-guide/","summary":"转载一篇关于 Docker 三种场景下代理配置的详细教程：Dockerd 代理、Container 代理、Docker Build 代理。","title":"转载｜如何优雅的给 Docker 配置网络代理"}]