文章目录
实践实例
源码:
html
<!DOCTYPE html>
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<script src="/seeyon/rest/global/v1/env" type="text/javascript"></script>
<link rel="stylesheet" theme-data type="text/css" href="/seeyon/common/all-min.css?V=V9_0SP1_250518_2028260">
<!--[if lt IE 9]>
<link rel="stylesheet" theme-data href="/seeyon/portal/management/css/iconfont.css?V=V9_0SP1_250518_2028260"/>
<![endif]-->
<link rel="icon" type="image/x-icon" href="/seeyon/common/images/A8N/favicon.ico?V=V9_0SP1_250518_2028260" />
<!--[if (!IE)|(gte IE 9)]><!-->
<!-- <link rel="stylesheet" type="text/css" href="/seeyon/skin/dist/common/special-notIE8.css?V=V9_0SP1_250518_2028260"> -->
<!--<![endif]-->
<script type="text/javascript">;var _ctpSkinUrl='/skin/dist/components/components_theme_default.css';</script>
<link rel="stylesheet" href="/seeyon/skin/dist/min/skinComponents-defaultTheme-min.css?V=V9_0SP1_250518_2028260">
<link rel="stylesheet" href="/seeyon/skin/xicon/xicon-min.css?V=V9_0SP1_250518_2028260">
<link rel="stylesheet" type="text/css" href="/seeyon/common/css/ddPeoCardSelect2Animate.css?V=V9_0SP1_250518_2028260" />
<!--[if lt IE 9]>
<link rel="stylesheet" theme-data type="text/css" href="/seeyon/common/ctpUi/dist/css/ctpUi4IE8.css?V=V9_0SP1_250518_2028260">
<![endif]-->
<!--[if (!IE)|(gte IE 9)]><!-->
<link rel="stylesheet" theme-data type="text/css" href="/seeyon/common/ctpUi/dist/css/ctpUi.css?V=V9_0SP1_250518_2028260">
<!--<![endif]-->
<script type="text/javascript">
var isSecretLevelEnable = false;
var CSRFTOKEN = 'null';
var APPID = '4095';
var _ctxPath = '/seeyon', _ctxServer = 'http://*:80/seeyon';
var _staticPath = _ctxPath;
var _staticSuffix = '?V=V9_0SP1_250518_2028260';
var _locale = 'zh_CN',_isDevelop = false,_sessionid = '',_isModalDialog = false;
var _editionI18nSuffix = '';
var _resourceCode = "";
var seeyonProductId="14";
</script>
<script type="text/javascript">
var setFlag=false;
if(typeof Set!="undefined"){
var SysSet=Set;
setFlag=true;
}
</script>
<script type="text/javascript" charset="UTF-8" src="/seeyon/common/js/polyfill.min.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/common/all-min.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/common/ctpUi/dist/js/ctpUi.min.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/common/js/ui/calendar/calendar-language.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/main.do?method=headerjs&login=*"></script>
<script type="text/javascript">
var addinMenus = new Array();
$.ctx._currentPathId = 'bulData_bulView';
$.ctx._pageSize = 20;
$.ctx._emailShow = true;
$.ctx.fillmaps = null;
$.releaseOnunload();
</script>
<script type="text/javascript" src="/seeyon/ajaxStub.js?v=*"></script>
<html>
<head>
<meta charset="utf-8">
<title>*</title>
<link rel="stylesheet" type="text/css" href="/seeyon/skin/dist/modules/bulletin.css?V=V9_0SP1_250518_2028260" theme-data/>
<script type="text/javascript" charset="UTF-8">if (window.skinStyleHandler) {window.skinStyleHandler({variables: window.themeCssVariables || {}});}</script>
<script type="text/javascript" src="/seeyon/common/waterMark/js/waterMark.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/apps_res/bulletin/js/common.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/apps_res/bulletin/js/index.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" charset="UTF-8" src="/seeyon/common/office/js/baseOffice.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/apps_res/bulletin/js/bulView.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/apps_res/doc/js/docFavorite.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/common/ofd/js/baseOfd.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" charset="UTF-8" src="/seeyon/apps_res/uc/rongcloud/chat.js?V=V9_0SP1_250518_2028260"></script>
<script>
<!--
var theToShowAttachments = new ArrayList();
var downloadURL = "/seeyon/fileUpload.do";
//-->
</script>
<div style="display:none;">
<iframe name="downloadFileFrame" id="downloadFileFrame" frameborder="0" width="0" height="0"></iframe>
</div>
<script type="text/javascript">
var _isOfficeTrans = true;
var isGuest =false;
var bulId = "*";
var isEnableWPSOnlineView = false;
var isEnableDcsOnlineView = false;
//dengwei OA-229275 龙芯-点聚阅读器-查看pdf正文未展示,另存为报错 start
var ext5 = "";
//dengwei OA-229275 龙芯-点聚阅读器-查看pdf正文未展示,另存为报错 end
//dengwei OA-229889 windows+wps:公告,,word转ofd的正文,点击打印,报js错误 start
var cebType = "";
//dengwei OA-229889 windows+wps:公告,,word转ofd的正文,点击打印,报js错误 end
var bulStyle = parseInt("2");
var dataFormat_Type = "Pdf";
var bul_ext4 = false;
var _spaceType = '';
var _spaceId = '';
var wi = "";
var waterMarkBase64 = "";
var editType = "4,0";
var officecanPrint = "false";
var officecanSaveLocal = "false";
window.top.pageName = 'bul';
var useWebOffice=false;
// 互联互通打印依赖参数
var printParamsObj = {
dataFormat:'Pdf',
isChangedPdf:'true',
isForm:false,
content:'*',
createDate:'2025-01-13 17:54',
userId:'',
ext:'false',
title:'*'
};
function initOfficeSucessCallback(res) {
if (typeof (res) != "undefined" && res != null){
if (res.code === 1){
$.alert(res.msg);
}
}
}
window.onload = function () {
if (typeof (OfficeAPI) != "undefined") {
OfficeAPI.setOptions({
fileName: "*",
editType: editType,
officecanPrint: officecanPrint,
officecanSaveLocal: officecanSaveLocal
});
}
}
//屏蔽鼠标右键
window.document.oncontextmenu = function () {
return false;
}
//禁止用户选择并复制数据
window.document.onselectstart = function () {
return false;
}
//返回首页(集团和单位空间下的处于发布状态下的公告)
function toIndex() {
if (('2' == '2' || '2' == '3') && '30' == '30' && 'myCollect' != 'pigeonhole') {
var url = '/seeyon/bulData.do?method=bulIndex';
window.location.href = url + CsrfGuard.getUrlSurffix();
}
}
</script>
<style>
#content {border: none;}
#content:hover {border: none;}
.set_color_gold {color: #fab715;}
table#col-contentTable tr td span.browse_class span[data-role], table#col-contentTable tr td span.browse_class span.xdRichTextBox {
min-height: 23px !important;;
white-space: pre-wrap !important;;
height: auto !important;;
}
table#col-contentTable tr td table.xdLayout tr td span.browse_class span[data-role], table#col-contentTable tr td table.xdLayout tr td span.browse_class span.xdRichTextBox {
/* width: 95% !important; */
}
#htmlContentDiv ul,
#htmlContentDiv ol{
font-size:16px;
}
</style>
<script type="text/javascript">
//表单签章相关,hw.js中需要用到
var hwVer = '6,7,0,514';
var _ctxPath = '/seeyon', _ctxServer = 'http://*:80/seeyon';
var htmOcxUserName = $.ctx.CurrentUser.name;
</script>
<SCRIPT language=javascript for=SignatureControl event=EventOnSign(DocumentId,SignSn,KeySn,Extparam,EventId,Ext1)>
//作用:重新获取签章位置
if (EventId = 4) {
CalculatePosition();
SignatureControl.EventResult = true;
}
</SCRIPT>
<style>html, body, div, img {
-webkit-user-drag: none;
user-drag: none;
}</style>
</head>
<body>
<input type="hidden" id="subject" name="subject" value="*">
<div id="2">
<div class="container bulentin" id="container1">
<div class="container_top">
<div class="bulentinLOG">
<span class="index_Logo" title="返回首页" onclick="toIndex();" style="cursor: pointer;">
<img src="/seeyon/skin/dist/images/cultural/bulletin/notice_log1.png">
<span class="bulentinLOG_Text">公告</span>
</span>
</div>
</div>
<div class="content_container" id="content_container1">
<div class="container_auto" id="page_height1">
<div class="all_content" style="" id = "page_height2">
<div class="container_discuss detail moduleTWO" id="page_height" style=";">
<div class="discuss_left" style=";">
<div class="left_list">
<div class="mainText_head" id="head_height">
</div>
<div class="mainText_body">
<div>
<div id="contentTD" style="width:100%;height:768px;padding-bottom: 6px;text-align: left;" valign="top" colspan="6">
<div id="mainbodyDiv" style="height:100%">
<div style='display:none'>
<input type='hidden' name='bodyType' id='bodyType' value='Pdf'>
<input type="hidden" name="bodyCreateDate" value="2025-01-13 17:54:20">
</div>
<input id="contentNameId" type="hidden" name="contentName" value="">
<script type="text/javascript">var currUserName = "*"</script>
<script>
userId='*';
officeOcxUploadMaxSize='51200';
realSize="";
fileType="pdf";
createDate="2025-01-13 17:54:20";
sessionId="*";
var ofdReader="null";
</script>
<table align="center" border="0" height="100%" cellspacing="0" cellpadding="0" class="body-detail-office">
<tr>
<td height="100%">
<div id="officeFrameDiv" style="height:100%;display:none">
<iframe src="" name="officeEditorFrame" id="officeEditorFrame" frameborder="0" width="100%" height="100%"></iframe>
</div>
<script type="text/javascript" src="/seeyon/common/js/polyfill.min.js?V=*"></script>
<script type="text/javascript" src="/seeyon/common/office/js/officeSDK.js?V=*"></script>
<script>
var webRoot="http://*:80/seeyon";
var fileId="*";
var createDate="2025-01-13 17:54:20";
var category = "";
var editType="0,0";
var ocxVer="6,0,2,312";
var originalFileId = "";
var needReadFile = "true";
var currUserName="*";
var lastUpdateTime="*";
var officeOcxUploadMax=51200;
var pdfVer="9,0,0,1666";
var officeFileRealSize="";
var hasAdvanceOcx=true;
var isRetainedTraces="false";
var userId="*";
var isLoadOfficeImmediate="true";
var sessionId="*";
var ofdReader="null";
var officecanPrint="false";
var officecanSaveLocal="false";
var saveLocalFileName="*";
v3x.loadLanguage("/common/office/js/i18n");
</script>
</td>
</tr>
</table>
<script>
var officeParams = {
webRoot: "http://*:80/seeyon",
fileId: "*",
fileType: "pdf",
createDate: "2025-01-13 17:54:20",
editType: "0,0",
originalFileId: "" ,
needReadFile: true,
currUserName: "*",
lastUpdateTime: "*",
officeOcxUploadMaxSize: 51200,
canPrint: "false",
officecanPrint: "false",
officecanSaveLocal: "false",
loadOfficeImmediate:"true",
fileName:"*"
};
officeParams.canPrint = "false";
officeParams.officecanPrint = "false";
officeParams.officecanSaveLocal = "false";
officeParams.canDownload = "false";
officeParams.isNeedCheckSignDownload = "true";
officeParams.fileName = '*';
var _officeSDK = new OfficeSDK(officeParams);
var editType = officeParams.editType;
var mode = 'VIEW';
if (editType === '1,0' || editType === '2,1') {
mode = 'EDIT';
}
_officeSDK.init({
webRoot: officeParams.webRoot,
domId: 'officeFrameDiv',
mode: mode,
fileType: 'pdf'
}, function (err, result) {
if (err != null) {
if ($ && $.alert) {
$.alert('当前配置,不支持操作pdf类型文件');
} else {
alert('当前配置,不支持操作pdf类型文件');
}
return;
} else {
var targetDom = document.querySelector("#officeFrameDiv");
targetDom.style.display = "block";
targetDom.style.overflow = "hidden";
_officeSDK.openFile({},);
}
});
</script>
</div>
<script type="text/javascript">
// pdf 控制参数
officecanPrint = "false";
officecanSaveLocal = "false";
editType = "0,0";
// office 控制参数
if(typeof(OfficeAPI) != "undefined"){
OfficeAPI.setOptions({
officecanPrint : "false",
officecanSaveLocal : "false",
editType : editType
});
}
</script>
<script type="text/javascript">
// office 控制不显示花脸,保存本地时强制清除痕迹
if(typeof(OfficeAPI) != "undefined"){
OfficeAPI.setOptions({
showHl : "false",
isRemoveTrace : "true"
});
}
</script>
<style>
.contentText p{
font-size:16px;
line-height: 1.6;
word-break: break-all !important;
word-wrap: break-word !important;
}
.contentText ul,.contentText ol {
padding-left: 40px;
}
.contentText ul li {
list-style: disc;
font-size: 16px;
word-break: break-word !important;
word-wrap: break-word !important;
}
.contentText ol li {
list-style: decimal;
font-size: 16px;
word-break: break-word !important;
word-wrap: break-word !important;
}
.mainText_body table{
width:100%;
}
</style>
</div>
</div>
</div>
<div class="mainText_foot" id="foot_height" style="width:100%">
<div class="padding30" style="display: none;padding: 5px 0px 5px 0px" id="attachmentTRAttFile">
<div class="atts-label" >
<span class="left label_num">
<em class="icon16 file_attachment_16"></em> (<span id="attachmentNumberDivAttFile"></span>)
</span>
<div id="attFileDomain" isGrid="true" class="comp" comp="type:'fileupload',attachmentTrId:'AttFile',canFavourite:false,applicationCategory:'8',canDeleteOriginalAtts:false" attsdata=''></div>
</div>
</div>
<div class="padding30" style="display: none; padding: 5px 0px 5px 0px" id="attachment2TRDoc1">
<div class="atts-label">
<span class="left label_num">
<em class="icon16 relation_file_16"></em> (<span id="attachment2NumberDivDoc1"></span>)
</span>
<div id="assDocDomain" isCrid="true" class="comp" comp="type:'assdoc',attachmentTrId:'Doc1',applicationCategory:'8',modids:8,canDeleteOriginalAtts:false" attsdata=''></div>
</div>
</div>
</div>
</div>
</div>
<div class="module_Two">
</div>
<div class="attrNotice overflow" id="foot2_height">
<div class="mainText_head_msg" style="height:100%">
<div class="attrMsg"><span>属性信息 :</span>
</div>
<div class="left Wopacity mainText_head_msgSpan ">
<span class="pubMsg">
<em class="pubMsg_bar">公告标题 :</em>
<span class="pubMsg_span" title="*">*</span>
</span>
<span class="pubMsg">
<em class="pubMsg_bar">发布范围 :</em>
<span class="pubMsg_span" title="*">*</span>
</span>
<span class="pubMsg">
<em class="pubMsg_bar">发布时间 :</em>
<span class="pubMsg_span">2025-01-13 08:42</span>
</span>
<span class="pubMsg">
<em class="pubMsg_bar">发布部门 :</em>
<span class="pubMsg_span" title="*">*</span>
</span>
<span class="pubMsg">
<em class="pubMsg_bar">阅读量 :</em>
<span class="pubMsg_span">*</span>
</span>
</div>
</div>
<div class="setBtn" id="noprint">
<span class="">
<span class="head_title_collect hand">
<span id="article_fav_more" onclick="javaScript:cancelFavorite_bul('*')">
<em id = "article_fav" class="ico16 stored_16 " ></em>
<span class="Wopacity collect" >取消收藏</span>
</span>
</span>
<span class="head_title_print hand" onclick="showReadList1('*','查看阅读信息' ,'false' )">
<em class="icon16 discuss_click_current_16"></em>
<span class="Wopacity">查看阅读信息</span>
</span>
<span class="hand" id="viewOriginalContentA" onclick="popupContentWin('*','Pdf','2025-01-13 17:54:20.573','','false','*')">
<span class="syIcon sy-call_template set_color_gold" style=""></span>
<span class="Wopacity">查看原文档</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="to_top" id="back_to_top">
<span class="scroll_bg">
<em class="to_top_24" title="返回顶部"></em>
</span>
</div>
</div>
</body>
<script type="text/javascript">
//页面大小改变的时候移动ISignatureHTML签章对象,让其到达正确的位置
</script>
</html>
技术原理
基于致远互联(Seeyon)V9.0SP1版本封装的SuwellLightRead技术,其PDF预览页面采用"前端SDK管控-后端鉴权分发-阅读器渲染"的三层架构设计,核心目标是在保障文件安全的前提下提供预览能力。原始PDF文件的提取方法,本质是通过解析该架构的接口交互逻辑,复用合法会话权限,绕过前端功能限制,直接获取后端分发的原始文件下载链路,具体技术原理如下:
SuwellLightRead技术的PDF预览原生架构
SuwellLightRead作为轻量级PDF阅读组件,其预览流程依赖前端与后端的紧密协作,核心交互链路具有明确的分层特征:
- 前端初始化与配置层 :页面加载时,通过
OfficeSDK实例化核心配置,传入webRoot(系统根路径)、fileId(文件唯一标识)、fileType=pdf等必要参数,同时设置功能权限开关(officecanPrint: "false"、officecanSaveLocal: "false"),禁用下载、打印等敏感操作。初始化后通过_officeSDK.init()绑定DOM容器,指定预览模式为VIEW,完成前端渲染准备。 - 后端鉴权与资源分发层 :调用
_officeSDK.openFile()触发后端请求,前端通过fetch或XHR向/seeyon/rest/skResource/sk/file/info接口发送请求,携带seed(随机种子)、tko=SuwellLightReadHeart(组件标识)、tolen(动态鉴权令牌)等核心参数,以及permissionJson权限配置(原生配置中download:0明确禁用下载权限)。后端校验会话合法性、参数有效性后,返回文件预览所需的元信息与资源地址,而非原始文件流。 - 阅读器渲染层:前端通过内置阅读器(集成PDF解析内核)加载后端返回的预览资源,将文件流转换为可视化页面,全程屏蔽原始文件的直接访问链路,仅提供只读预览能力。
原始PDF文件提取的核心技术逻辑
提取方法的核心是利用架构设计中的"前端功能限制与后端鉴权松耦合"特性,通过请求拦截、参数解析、会话复用三步实现原始文件获取:
- 请求拦截与核心响应捕获 :通过重写
window.fetch与XMLHttpRequest.prototype.open方法,监听前端与后端的交互请求,筛选包含"url"关键字的响应数据(此类响应中隐含中间链路fileInfoUrl)。拦截过程中克隆响应流,避免影响原生预览功能,同时记录响应的原始内容、状态码等关键信息,无需二次请求即可获取核心数据。 - 中间链路解析与鉴权参数复用 :从捕获的响应中解析
fileInfoUrl,该URL是后端为阅读器提供的资源访问中间入口,包含fileId、seed、tolen等完整鉴权参数。由于后端仅校验参数合法性与会话有效性,未强制绑定请求发起方(前端阅读器或自定义脚本),因此可直接复用该URL进行后续请求。 - 会话权限复用与下载地址获取 :向
fileInfoUrl发送请求时,通过credentials: 'include'参数携带当前用户的合法会话Cookie,复用预览页面的访问权限。后端校验通过后,返回包含download_url的完整文件信息,该地址指向/seeyon/rest/office/file/oa/download接口,携带动态鉴权参数,直接访问即可获取原始PDF文件流,实现提取目的。
关键技术依赖与约束条件
提取操作的可行性依赖特定技术环境与权限边界,核心约束如下:
- 技术依赖 :依赖前端JavaScript的原生API重写能力,需确保
_officeSDK实例已初始化(页面加载完成后执行脚本);依赖后端对会话Cookie的信任机制,未额外校验请求的来源上下文(如Referer、请求头特征)。 - 权限边界约束 :提取操作必须基于合法用户会话,需先登录系统并拥有目标PDF的预览权限,无有效会话时,
fileInfoUrl与download_url的请求均会被后端拒绝。 - 动态参数约束 :
seed与tolen为一次性动态鉴权参数,由后端实时生成并与当前会话绑定,过期或伪造参数会导致鉴权失败,需从当前页面的实时请求中拦截获取。 - 接口兼容性约束 :提取逻辑依赖
/seeyon/rest/skResource/sk/file/info与/seeyon/rest/office/file/oa/download接口的固定路径与响应格式,接口路径或参数结构变更需要更新对应目标定位特征。
通用方法:手动提取原始PDF地址的步骤
在 SuwellLightRead 技术封装的 PDF 预览页面中,尽管前端界面明确禁用下载功能(如 permissionJson 中 "download":0),但原始 PDF 文件的访问地址仍可通过浏览器开发者工具手动提取。该过程无需执行任何脚本,仅依赖对网络请求的观察与分析,适用于所有已通过身份认证、具备合法会话的用户。
由于实际加载流程中,系统先发起 /VIEW/url 的 XHR 请求获取阅读器配置,再加载 /web-reader/reader 页面,因此以下方法按真实请求顺序组织,并将共用逻辑抽象为前置与后置步骤,以提升操作效率和清晰度。
前提条件
- 用户已成功登录致远 OA 系统;
- 已打开目标 PDF 文档的预览页面(通常为公告、公文或附件详情页);
- 浏览器支持开发者工具(如 Chrome DevTools、Edge DevTools 等);
- 预览页面使用的是 SuwellLightRead 阅读器(URL 中包含
/web-reader/reader或调用_officeSDK.openFile)。
前置步骤:捕获 fileInfo 接口地址
无论采用哪种路径,最终目标都是获取形如以下格式的 fileInfo 接口 URL:
http://*:80/seeyon/rest/skResource/sk/file/info?v=...&fileId=...&seed=...&tolen=...&m=s
该接口是后续提取下载地址的关键入口。可通过以下任一方式获得:
方式 A:从 XHR 请求响应中直接提取(推荐,优先发生)
-
打开浏览器开发者工具(F12),切换至 Network(网络) 面板,勾选 Preserve log;
-
刷新预览页面,在请求列表中查找 XHR/Fetch 类型的请求,其 URL 为:
http://*/seeyon/rest/skResource/VIEW/url -
点击该请求,查看 Response 内容,定位字段:
json{ "data": { "fileInfoUrl": "http://*:80/seeyon/rest/skResource/sk/file/info?..." } } -
复制
fileInfoUrl的完整值,即为目标 fileInfo 接口地址。
方式 B:从阅读器页面 URL 参数中解码提取(备选,稍后发生)
-
在 Network 面板中查找类型为 Document 的请求,其 URL 形如:
http://*:81/web-reader/reader?file=... -
复制整个 URL,提取
file=后的参数值; -
对该值执行 URL 解码 (例如在浏览器控制台运行):
jsdecodeURIComponent("http%3A%2F%2F172...") // 如果存在问题,可以尝试2次解码 // decodeURIComponent(decodeURIComponent("http%3A%2F%2F172...")) -
解码结果即为完整的 fileInfo 接口地址。
✅ 建议优先使用方式 A,因其发生在页面渲染前,响应结构清晰,且无需处理嵌套编码。
后置处理步骤:从 fileInfo 接口获取并下载原始 PDF
一旦获得有效的 fileInfo 接口地址,后续操作完全一致:
-
访问 fileInfo 接口
在新浏览器标签页中粘贴并打开该 URL。由于当前会话 Cookie 有效,服务器将返回 JSON 响应。
-
提取 download_url
在响应体中定位如下字段:
json{ "file": { "download_url": "http://*:80/seeyon/rest/office/file/oa/download?_=...&fileId=...&seed=...&tolen=...&m=s" } }此
download_url即为原始 PDF 文件的直接下载链接。 -
下载文件
- 将
download_url在新标签页中打开; - 浏览器将自动触发 PDF 文件下载;
- 若页面空白或报错,请检查:
- 当前会话是否仍有效(重新登录可刷新 token);
- URL 是否完整复制(尤其注意
&和?的完整性);
- 将
注意事项
- 时效性 :
fileInfoUrl和download_url中的seed与tolen参数通常仅在几分钟内有效,建议在页面加载完成后立即操作。 - 会话绑定:所有请求必须在同一浏览器会话下完成,跨设备或清除 Cookie 将导致 403 错误。
- 权限限制:该方法仅适用于用户本身拥有"阅读"权限的文档,无法绕过后端权限校验。
- 双重编码 :若使用方式 B,务必进行
decodeURIComponent,否则参数解析错误将导致 400 响应。
自动化提取脚本实战
提取PDF脚本
js
// 核心:捕获含"url"的请求响应 → 直接解析fileInfoUrl → 请求fileInfoUrl → 生成新页面打开的下载按钮
(async function() {
// ========== 全局变量初始化 ==========
let firstMatchedResponse = null; // 直接记录第一个含"url"请求的响应内容
let fileInfoUrl = ''; // 解析出的fileInfoUrl
let downloadUrl = ''; // 最终下载地址
const FILTER_KEY = 'url'; // 仅匹配含"url"的请求
let isFirstMatched = false; // 标记是否已捕获第一个匹配请求
// ========== 第一步:监听请求并直接记录响应内容 ==========
// 1. 拦截fetch请求(捕获响应内容)
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
const fetchUrl = typeof url === 'string' ? url : url.href;
// 仅处理含"url"的请求,且只记录第一个
if (fetchUrl.includes(FILTER_KEY) && !isFirstMatched) {
console.log(`📤 捕获到第一个含"${FILTER_KEY}"的fetch请求:`, fetchUrl);
try {
// 执行原生fetch并获取响应
const response = await originalFetch.apply(this, arguments);
// 克隆响应(避免原响应被消费)
const responseClone = response.clone();
// 获取原始响应文本(兼容非JSON格式)
const rawText = await responseClone.text();
// 记录第一个匹配请求的完整响应信息
firstMatchedResponse = {
url: fetchUrl,
status: response.status,
statusText: response.statusText,
rawResponse: rawText, // 直接记录响应内容,无需二次请求
isJson: false
};
// 尝试解析JSON(容错)
try {
firstMatchedResponse.jsonResponse = JSON.parse(rawText);
firstMatchedResponse.isJson = true;
} catch (e) {
firstMatchedResponse.jsonResponse = null;
firstMatchedResponse.parseError = '响应不是合法JSON格式';
}
isFirstMatched = true; // 标记已捕获第一个
console.log('%c✅ 已记录第一个匹配请求的响应内容', 'font-size:14px; color:purple; font-weight:bold;');
return response;
} catch (err) {
console.error(`❌ 第一个含"${FILTER_KEY}"的fetch请求失败:`, err);
firstMatchedResponse = { url: fetchUrl, error: err.message };
isFirstMatched = true;
}
}
// 非匹配请求/已捕获第一个,直接执行原生fetch
return originalFetch.apply(this, arguments);
};
// 2. 拦截XHR请求(捕获响应内容,兼容老式请求)
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
// 仅处理含"url"的请求,且只记录第一个
if (url.includes(FILTER_KEY) && !isFirstMatched) {
console.log(`📤 捕获到第一个含"${FILTER_KEY}"的XHR请求:`, url);
// 监听XHR加载完成事件,记录响应
this.addEventListener('load', () => {
if (isFirstMatched) return; // 已记录第一个则跳过
// 记录XHR响应内容
firstMatchedResponse = {
url: url,
status: this.status,
statusText: this.statusText,
rawResponse: this.responseText, // 直接记录响应内容
isJson: false
};
// 尝试解析JSON
try {
firstMatchedResponse.jsonResponse = JSON.parse(this.responseText);
firstMatchedResponse.isJson = true;
} catch (e) {
firstMatchedResponse.jsonResponse = null;
firstMatchedResponse.parseError = '响应不是合法JSON格式';
}
isFirstMatched = true;
console.log('%c✅ 已记录第一个匹配请求的响应内容', 'font-size:14px; color:purple; font-weight:bold;');
});
// 监听XHR错误
this.addEventListener('error', () => {
if (isFirstMatched) return;
firstMatchedResponse = { url: url, error: 'XHR请求失败' };
isFirstMatched = true;
});
}
return originalXhrOpen.apply(this, arguments);
};
// ========== 第二步:执行openFile触发请求 ==========
console.log('✅ 已启动请求监听(直接记录响应内容),执行openFile...');
if (!window._officeSDK) {
alert('❌ 错误:未找到_officeSDK实例,请确保OfficeSDK已初始化!');
// 还原原生方法
window.fetch = originalFetch;
XMLHttpRequest.prototype.open = originalXhrOpen;
return;
}
try {
await window._officeSDK.openFile({},);
console.log('✅ _officeSDK.openFile 执行完成');
} catch (err) {
alert(`❌ openFile执行失败:${err.message}`);
window.fetch = originalFetch;
XMLHttpRequest.prototype.open = originalXhrOpen;
return;
}
// ========== 第三步:解析fileInfoUrl(直接用记录的响应,无需二次请求) ==========
setTimeout(async () => {
// 还原原生方法
window.fetch = originalFetch;
XMLHttpRequest.prototype.open = originalXhrOpen;
// 1. 检查是否捕获到第一个匹配请求的响应
if (!firstMatchedResponse) {
alert('❌ 未捕获到任何包含"url"的请求响应!');
return;
}
// 打印第一个匹配请求的响应详情(供核实)
console.log('%c==================== 第一个匹配请求的响应详情 ====================', 'font-size:16px; color:purple; font-weight:bold;');
console.log('请求URL:', firstMatchedResponse.url);
console.log('响应状态码:', firstMatchedResponse.status);
console.log('响应原始内容:', firstMatchedResponse.rawResponse);
console.log('是否为JSON格式:', firstMatchedResponse.isJson);
if (firstMatchedResponse.parseError) {
console.log('JSON解析错误:', firstMatchedResponse.parseError);
}
console.log('%c================================================================', 'font-size:16px; color:purple; font-weight:bold;');
// 2. 直接从响应内容解析fileInfoUrl(无需二次请求)
if (!firstMatchedResponse.isJson || !firstMatchedResponse.jsonResponse) {
alert('❌ 第一个匹配请求的响应不是JSON格式,无法解析fileInfoUrl!');
return;
}
// 提取fileInfoUrl并打印(供核实)
fileInfoUrl = firstMatchedResponse.jsonResponse.data?.fileInfoUrl || '';
console.log('%c==================== 解析出的fileInfoUrl ====================', 'font-size:16px; color:orange; font-weight:bold;');
console.log('fileInfoUrl字段内容:', fileInfoUrl || '未找到');
console.log('%c============================================================', 'font-size:16px; color:orange; font-weight:bold;');
if (!fileInfoUrl) {
alert('❌ 从响应中未解析到fileInfoUrl字段!');
return;
}
// ========== 第四步:请求fileInfoUrl(携带Cookie)并打印详情 ==========
try {
console.log('📡 开始请求fileInfoUrl(携带本地Cookie)...');
// 请求fileInfoUrl(携带Cookie)
const fileInfoResponse = await fetch(fileInfoUrl, {
credentials: 'include', // 关键:携带Cookie
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// 打印fileInfoUrl请求的完整信息
console.log('%c==================== fileInfoUrl请求详情 ====================', 'font-size:16px; color:green; font-weight:bold;');
console.log('请求的完整URL:', fileInfoUrl);
// 解析URL参数
try {
const urlObj = new URL(fileInfoUrl);
const params = Object.fromEntries(urlObj.searchParams.entries());
console.log('URL参数列表:', params);
console.log('参数数量:', Object.keys(params).length);
} catch (e) {
console.log('URL解析失败(非合法URL):', e.message);
}
console.log('响应状态码:', fileInfoResponse.status);
console.log('响应状态文本:', fileInfoResponse.statusText);
// 打印原始返回内容(兼容非JSON格式)
const fileInfoRawText = await fileInfoResponse.text();
console.log('响应原始内容:', fileInfoRawText);
// 尝试解析JSON(容错)
let fileInfoResult = null;
try {
fileInfoResult = JSON.parse(fileInfoRawText);
console.log('响应JSON解析结果:', fileInfoResult);
} catch (e) {
console.log('JSON解析错误:', e.message);
}
console.log('%c================================================================', 'font-size:16px; color:green; font-weight:bold;');
// 3. 提取download_url
if (fileInfoResult && fileInfoResult.file?.download_url) {
downloadUrl = fileInfoResult.file.download_url;
console.log('%c🎉 最终下载地址:', 'font-size:16px; color:red; font-weight:bold;', downloadUrl);
} else {
alert('❌ 从fileInfoUrl响应中未解析到download_url!');
return;
}
// ========== 第五步:生成下载按钮(核心修复:改为a标签,新页面打开) ==========
// 移除页面已存在的同名按钮(避免重复)
const oldBtn = document.querySelector('#pdfDownloadBtn');
if (oldBtn) oldBtn.remove();
// 创建a标签(模拟按钮样式,本质是跳转链接)
const downloadLink = document.createElement('a');
downloadLink.id = 'pdfDownloadBtn';
downloadLink.innerText = '📥 打开下载地址'; // 文案改为更贴合的描述
downloadLink.href = downloadUrl; // 设置跳转地址
downloadLink.target = '_blank'; // 强制在新页面打开
downloadLink.rel = 'noopener noreferrer'; // 安全优化
// 按钮样式(和之前一致,保持醒目)
downloadLink.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: #4444ff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
z-index: 9999;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: background 0.3s;
text-decoration: none; // 去掉a标签默认下划线
display: inline-block; // 适配padding样式
`;
// 悬浮效果
downloadLink.onmouseover = () => {
downloadLink.style.background = '#6666ff';
};
downloadLink.onmouseout = () => {
downloadLink.style.background = '#4444ff';
};
// 添加到页面
document.body.appendChild(downloadLink);
// ========== 第六步:人性化弹窗提示 ==========
alert(`🎉 操作成功!
1. 已获取到下载地址(控制台可查看);
2. 页面右上角【打开下载地址】按钮可在新页面打开地址;
3. 若新页面未弹出,请关闭浏览器弹窗拦截后重试;`)
} catch (reqErr) {
alert(`❌ 请求fileInfoUrl失败:${reqErr.message}`);
console.error('%c❌ 请求错误详情:', 'font-size:14px; color:red; font-weight:bold;', reqErr);
}
}, 5000); // 5秒延迟确保请求完成并记录响应
})();
脚本使用方法
该脚本为通用化的PDF下载地址自动提取工具,基于浏览器前端环境运行,无需额外部署依赖,仅需在目标PDF预览页面执行即可完成下载地址提取,具体使用流程如下:
前置条件
- 环境要求:使用Chrome、Firefox、Edge等现代浏览器(IE浏览器不兼容ES6语法,无法运行);
- 会话有效性 :已登录目标系统,且能正常打开PDF预览页面(页面可显示PDF内容,证明
_officeSDK实例已初始化); - 权限基础:拥有该PDF的预览权限(无预览权限时脚本无法捕获有效请求)。
核心操作步骤
-
打开浏览器开发者工具控制台
- 进入PDF预览页面,确保页面已完全加载(PDF内容正常显示);
- 按下快捷键
F12或Ctrl+Shift+I(Mac系统为Cmd+Opt+I)打开开发者工具; - 在开发者工具面板中,切换至 Console(控制台) 标签页,清空原有日志(可选,点击面板左上角清空按钮)。
-
复制并执行脚本
- 全选本脚本的所有代码内容,使用
Ctrl+C(Mac为Cmd+C)复制; - 将复制的脚本粘贴到Console面板的输入框中,按下回车键执行脚本;
- 脚本启动后,控制台会输出
✅ 已启动请求监听(直接记录响应内容),执行openFile...日志,表明脚本开始运行。
- 全选本脚本的所有代码内容,使用
-
等待脚本自动完成解析
- 脚本内置5秒延迟(确保请求完全捕获),期间控制台会依次输出以下关键日志:
📤 捕获到第一个含"url"的fetch/XHR请求:确认请求拦截成功;✅ 已记录第一个匹配请求的响应内容:确认响应数据已保存;- 紫色/橙色/绿色分段日志:分别展示请求响应详情、fileInfoUrl解析结果、download_url提取结果;
- 脚本执行完成后,会弹出提示框
🎉 操作成功!,表明下载地址已提取完成。
- 脚本内置5秒延迟(确保请求完全捕获),期间控制台会依次输出以下关键日志:
下载地址使用方式
-
使用页面下载按钮(推荐)
脚本执行成功后,PDF预览页面右上角会生成固定悬浮按钮
📥 打开下载地址,点击该按钮会在新标签页打开原始PDF的下载地址,浏览器自动触发PDF下载或直接渲染原始文件。 -
手动复制下载地址(备用)
- 仅推荐当按钮未正常生成时,从Console控制台提取下载地址。
- 在控制台日志中找到红色加粗的
🎉 最终下载地址:行,复制其后的URL字符串; - 打开新标签页,粘贴该URL并回车,即可访问原始PDF下载地址。
注意事项
本文分享的方法与脚本,仅适用于个人学习、研究或获取有权限的文档。在使用过程中,需严格遵守相关法律法规和网站的用户协议,不得用于非法获取受版权保护的文档,避免侵权风险。