从基于致远互联(Seeyon)封装的SuwellLightRead技术的PDF预览页面提取原始PDF文件的方法原理与实践

文章目录

实践实例

源码:

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>&nbsp;(<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>&nbsp;(<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()触发后端请求,前端通过fetchXHR/seeyon/rest/skResource/sk/file/info接口发送请求,携带seed(随机种子)、tko=SuwellLightReadHeart(组件标识)、tolen(动态鉴权令牌)等核心参数,以及permissionJson权限配置(原生配置中download:0明确禁用下载权限)。后端校验会话合法性、参数有效性后,返回文件预览所需的元信息与资源地址,而非原始文件流。
  • 阅读器渲染层:前端通过内置阅读器(集成PDF解析内核)加载后端返回的预览资源,将文件流转换为可视化页面,全程屏蔽原始文件的直接访问链路,仅提供只读预览能力。
原始PDF文件提取的核心技术逻辑

提取方法的核心是利用架构设计中的"前端功能限制与后端鉴权松耦合"特性,通过请求拦截、参数解析、会话复用三步实现原始文件获取:

  • 请求拦截与核心响应捕获 :通过重写window.fetchXMLHttpRequest.prototype.open方法,监听前端与后端的交互请求,筛选包含"url"关键字的响应数据(此类响应中隐含中间链路fileInfoUrl)。拦截过程中克隆响应流,避免影响原生预览功能,同时记录响应的原始内容、状态码等关键信息,无需二次请求即可获取核心数据。
  • 中间链路解析与鉴权参数复用 :从捕获的响应中解析fileInfoUrl,该URL是后端为阅读器提供的资源访问中间入口,包含fileIdseedtolen等完整鉴权参数。由于后端仅校验参数合法性与会话有效性,未强制绑定请求发起方(前端阅读器或自定义脚本),因此可直接复用该URL进行后续请求。
  • 会话权限复用与下载地址获取 :向fileInfoUrl发送请求时,通过credentials: 'include'参数携带当前用户的合法会话Cookie,复用预览页面的访问权限。后端校验通过后,返回包含download_url的完整文件信息,该地址指向/seeyon/rest/office/file/oa/download接口,携带动态鉴权参数,直接访问即可获取原始PDF文件流,实现提取目的。
关键技术依赖与约束条件

提取操作的可行性依赖特定技术环境与权限边界,核心约束如下:

  • 技术依赖 :依赖前端JavaScript的原生API重写能力,需确保_officeSDK实例已初始化(页面加载完成后执行脚本);依赖后端对会话Cookie的信任机制,未额外校验请求的来源上下文(如Referer、请求头特征)。
  • 权限边界约束 :提取操作必须基于合法用户会话,需先登录系统并拥有目标PDF的预览权限,无有效会话时,fileInfoUrldownload_url的请求均会被后端拒绝。
  • 动态参数约束seedtolen为一次性动态鉴权参数,由后端实时生成并与当前会话绑定,过期或伪造参数会导致鉴权失败,需从当前页面的实时请求中拦截获取。
  • 接口兼容性约束 :提取逻辑依赖/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 请求响应中直接提取(推荐,优先发生)
  1. 打开浏览器开发者工具(F12),切换至 Network(网络) 面板,勾选 Preserve log

  2. 刷新预览页面,在请求列表中查找 XHR/Fetch 类型的请求,其 URL 为:

    复制代码
    http://*/seeyon/rest/skResource/VIEW/url
  3. 点击该请求,查看 Response 内容,定位字段:

    json 复制代码
    {
      "data": {
        "fileInfoUrl": "http://*:80/seeyon/rest/skResource/sk/file/info?..."
      }
    }
  4. 复制 fileInfoUrl 的完整值,即为目标 fileInfo 接口地址。

方式 B:从阅读器页面 URL 参数中解码提取(备选,稍后发生)
  1. 在 Network 面板中查找类型为 Document 的请求,其 URL 形如:

    复制代码
    http://*:81/web-reader/reader?file=...
  2. 复制整个 URL,提取 file= 后的参数值;

  3. 对该值执行 URL 解码 (例如在浏览器控制台运行):

    js 复制代码
    decodeURIComponent("http%3A%2F%2F172...")
    // 如果存在问题,可以尝试2次解码
    // decodeURIComponent(decodeURIComponent("http%3A%2F%2F172..."))
  4. 解码结果即为完整的 fileInfo 接口地址。

建议优先使用方式 A,因其发生在页面渲染前,响应结构清晰,且无需处理嵌套编码。

后置处理步骤:从 fileInfo 接口获取并下载原始 PDF

一旦获得有效的 fileInfo 接口地址,后续操作完全一致:

  1. 访问 fileInfo 接口

    在新浏览器标签页中粘贴并打开该 URL。由于当前会话 Cookie 有效,服务器将返回 JSON 响应。

  2. 提取 download_url

    在响应体中定位如下字段:

    json 复制代码
    {
      "file": {
        "download_url": "http://*:80/seeyon/rest/office/file/oa/download?_=...&fileId=...&seed=...&tolen=...&m=s"
      }
    }

    download_url 即为原始 PDF 文件的直接下载链接。

  3. 下载文件

    • download_url 在新标签页中打开;
    • 浏览器将自动触发 PDF 文件下载;
    • 若页面空白或报错,请检查:
      • 当前会话是否仍有效(重新登录可刷新 token);
      • URL 是否完整复制(尤其注意 &? 的完整性);
注意事项
  • 时效性fileInfoUrldownload_url 中的 seedtolen 参数通常仅在几分钟内有效,建议在页面加载完成后立即操作。
  • 会话绑定:所有请求必须在同一浏览器会话下完成,跨设备或清除 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预览页面执行即可完成下载地址提取,具体使用流程如下:

前置条件
  1. 环境要求:使用Chrome、Firefox、Edge等现代浏览器(IE浏览器不兼容ES6语法,无法运行);
  2. 会话有效性 :已登录目标系统,且能正常打开PDF预览页面(页面可显示PDF内容,证明_officeSDK实例已初始化);
  3. 权限基础:拥有该PDF的预览权限(无预览权限时脚本无法捕获有效请求)。
核心操作步骤
  1. 打开浏览器开发者工具控制台

    1. 进入PDF预览页面,确保页面已完全加载(PDF内容正常显示);
    2. 按下快捷键 F12Ctrl+Shift+I(Mac系统为 Cmd+Opt+I)打开开发者工具;
    3. 在开发者工具面板中,切换至 Console(控制台) 标签页,清空原有日志(可选,点击面板左上角清空按钮)。
  2. 复制并执行脚本

    1. 全选本脚本的所有代码内容,使用 Ctrl+C(Mac为 Cmd+C)复制;
    2. 将复制的脚本粘贴到Console面板的输入框中,按下回车键执行脚本;
    3. 脚本启动后,控制台会输出 ✅ 已启动请求监听(直接记录响应内容),执行openFile... 日志,表明脚本开始运行。
  3. 等待脚本自动完成解析

    1. 脚本内置5秒延迟(确保请求完全捕获),期间控制台会依次输出以下关键日志:
      • 📤 捕获到第一个含"url"的fetch/XHR请求:确认请求拦截成功;
      • ✅ 已记录第一个匹配请求的响应内容:确认响应数据已保存;
      • 紫色/橙色/绿色分段日志:分别展示请求响应详情、fileInfoUrl解析结果、download_url提取结果;
    2. 脚本执行完成后,会弹出提示框 🎉 操作成功!,表明下载地址已提取完成。
下载地址使用方式
  1. 使用页面下载按钮(推荐)

    脚本执行成功后,PDF预览页面右上角会生成固定悬浮按钮 📥 打开下载地址,点击该按钮会在新标签页打开原始PDF的下载地址,浏览器自动触发PDF下载或直接渲染原始文件。

  2. 手动复制下载地址(备用)

    1. 仅推荐当按钮未正常生成时,从Console控制台提取下载地址。
    2. 在控制台日志中找到红色加粗的 🎉 最终下载地址: 行,复制其后的URL字符串;
    3. 打开新标签页,粘贴该URL并回车,即可访问原始PDF下载地址。
注意事项

本文分享的方法与脚本,仅适用于个人学习、研究或获取有权限的文档。在使用过程中,需严格遵守相关法律法规和网站的用户协议,不得用于非法获取受版权保护的文档,避免侵权风险。

相关推荐
开开心心_Every18 分钟前
支持自定义名单的实用随机抽签工具
运维·服务器·pdf·电脑·excel·启发式算法·宽度优先
shuaiqinke21 小时前
【分享】Master PDF Editor v5.9.98便携版 多功能PDF编辑工具
智能手机·pdf
jianwuhuang821 天前
Kimi怎么导出pdf
人工智能·chatgpt·pdf·deepseek·ai导出鸭
daanpdf1 天前
四六级翻译《中国文化概况》双语批注版pdf百度网盘
pdf
daanpdf1 天前
古籍原文周易(易经)全文完整版PDF
pdf
daanpdf1 天前
大学英语四级试卷历年真题及答案PDF电子版百度网盘
pdf
hikktn1 天前
Excel模板智能转PDF:零硬编码的通用打印解决方案
windows·pdf
m0_502724951 天前
vue3生成pdf
前端·javascript·vue.js·pdf
驯龙高手_追风2 天前
Adobe Acrobat PDF阅读器设置默认滚动翻页
adobe·pdf·adobe acrobat reader·adobe reader
优化控制仿真模型2 天前
【26年社工】初级社会工作者历年真题及答案PDF电子版(2010-2025年)
经验分享·pdf