You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React集成DocuSign Focus View加载无限循环及CSP错误求助

DocuSign Focus View嵌入React应用时CSP错误导致无限加载

问题背景

在React应用中集成DocuSign Focus View,采用Node.js的DocuSign SDK通过Express服务器发起API请求。Postman可正常获取URL,直接访问链接能打开表单,但在React应用中嵌入该URL时,Focus View加载图标陷入无限循环,控制台出现Content Security Policy(CSP)错误。已尝试服务器端禁用CSP、在index.html添加meta标签,参考DocuSign React示例应用也无进展。

React组件代码

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';

function DocumentSignUI({ integrationKey, docUrl }) {
  useEffect(() => {
    const loadDocuSign = async () => {
      try {
        const docusign = await window.DocuSign.loadDocuSign(integrationKey);
        const url = `${docUrl}&output=embed`;

        const signing = docusign.signing({
          url,
          displayFormat: 'focused',
        });

        signing.mount('#agreement');
      } catch (ex) {
        console.error(ex);
        // Handle error
      }
    };

    console.log('DOCUSIGN RENDER');
    loadDocuSign();
  }, []);

  return (
    <div className="docusign-agreement" id="agreement" />
  );
}

DocumentSignUI.propTypes = {
  integrationKey: PropTypes.string.isRequired,
  docUrl: PropTypes.string.isRequired,
};

export default DocumentSignUI;

API请求代码

router.get("/sign-document", processFile, async (req, res) => {
  try {
    var userData = "";

    await checkToken(req);


    var apiClient = new docusign.ApiClient({
      basePath: basePath,
      oAuthBasePath: oAuthBasePath + "/oauth/token",
    });

    apiClient.addDefaultHeader(
      "Authorization",
      "Basic KEY"
    );

    userData = await apiClient.getUserInfo(req.session.access_token);

    var baseUri = userData.accounts[0].baseUri;
    var accountDomain = baseUri.split("/v2");
    apiClient.setBasePath(accountDomain[0] + "/restapi");

    const envelopeArgs = {
      signerEmail: "EMAIL",
      signerName: "NAME",
      signerClientId: DOCUSIGN_JWT_CLIENT_ID,
      docFile: req.file,
      status: "created",
    };

    const envelope = makeEnvelope(envelopeArgs);


    const envelopesApi = await getEnvelopes(req);



    const userEnvelopeResult = await envelopesApi.createEnvelope(
      userData.accounts[0].accountId,
      { envelopeDefinition: envelope }
    );

    const recepientRequest = {
      dsReturnUrl: "https://localhost:5173/success",
      signerEmail: "EMAIL",
      signerName: "NAME",
      signerClientId: DOCUSIGN_JWT_CLIENT_ID,
    };


    const viewRequest = await makeRecipientViewRequest(recepientRequest);


    const viewRequestResult = await envelopesApi.createRecipientView(
      userData.accounts[0].accountId,
      userEnvelopeResult.envelopeId,
      {
        recipientViewRequest: viewRequest,
        frameAncestors: [
          "http://localhost",
          "https://apps-d.docusign.com",
        ],
        messageOrigins: ["https://apps-d.docusign.com"],
        signerEmail: "EMAIL",
        signerName: "NAME"
      }
    );


    res.status(200).json({
      envelopeId: userEnvelopeResult.envelopeId,
      redirectUrl: viewRequestResult.url,
    });
  } catch (error) {
    console.error("Error processing request:", error);
    res.status(500).json({
      success: false,
      message: "An error occurred while processing the request",
    });
  }
});

辅助函数代码

const checkToken = async (req) => {
  try {
    var apiClient = new docusign.ApiClient({
      basePath: basePath,
      oAuthBasePath: oAuthBasePath + "/oauth/token",
    });

    apiClient.addDefaultHeader(
      "Authorization",
      "Basic OTAyY2IxNmYtYjBmZi00MzY2LWIyYzQtZDBmNWM3MTk1NTc0OjhlMzJjNzI0LTI1NjMtNDM0MC1iMTk5LWQxYTEwMTBhODM5YQ=="
    );

    var oauthLoginUrl = await apiClient.getJWTUri(
      DOCUSIGN_JWT_CLIENT_ID,
      "http://localhost:5173/",
      oAuthBasePath
    );
    var scopes = [oAuth.Scope.IMPERSONATION, oAuth.Scope.SIGNATURE];

    if (req.session.access_token && Date.now() < req.session.expires_at) {
      console.log("re-using access token ", req.session.access_token);
    } else {
      console.log("creating new access token");
      jWTResponse = await apiClient.requestJWTUserToken(
        DOCUSIGN_JWT_CLIENT_ID,
        DOCUSIGN_IMPERSONATED_USER_GUID,
        scopes,
        DOCUSIGN_RSA_KEY,
        36000
      );

      console.log(jWTResponse.body.access_token);

      req.session.access_token = jWTResponse.body.access_token;

      req.session.expires_at = 
        Date.now() + (jWTResponse.body.expires_in - 60) * 1000;
    }
  } catch (error) {
    console.error("Error while checking token:", error);
  }
};

const getEnvelopes = async (req) => {
  let dsApiClient = new docusign.ApiClient();
  dsApiClient.setBasePath(basePath);
  dsApiClient.addDefaultHeader(
    "Authorization",
    "Bearer " + req.session.access_token
  );
  return new docusign.EnvelopesApi(dsApiClient);
};

const makeEnvelope = (args) => {
  let docPdfBytes;
  // const filePath = path.resolve(__dirname, args.docFile);
  // docPdfBytes = fs.readFileSync(filePath);

  console.log(args.docFile);

  let env = new docusign.EnvelopeDefinition();
  env.emailSubject = "Please sign this document";

  let doc1 = new docusign.Document();
  let doc1b64 = Buffer.from(args.docFile.buffer).toString("base64");
  doc1.documentBase64 = doc1b64;
  doc1.name = "Lorem Ipsum";
  doc1.fileExtension = "pdf";
  doc1.documentId = "3";

  env.documents = [doc1];

  let signer1 = docusign.Signer.constructFromObject({
    email: args.signerEmail,
    name: args.signerName,
    clientUserId: args.signerClientId,
    recipientId: 1,
  });

  let signHere1 = docusign.SignHere.constructFromObject({
    anchorString: "/sn1/",
    anchorYOffset: "10",
    anchorUnits: "pixels",
    anchorXOffset: "20",
  });
  let signer1Tabs = docusign.Tabs.constructFromObject({
    signHereTabs: [signHere1],
  });
  signer1.tabs = signer1Tabs;

  let recipients = docusign.Recipients.constructFromObject({
    signers: [signer1],
  });
  env.recipients = recipients;
  env.status = "sent";

  return env;
};

const makeRecipientViewRequest = (args) => {
  let viewRequest = new docusign.RecipientViewRequest();
  viewRequest.returnUrl = args.dsReturnUrl;

  viewRequest.authenticationMethod = "none";

  viewRequest.email = args.signerEmail;
  viewRequest.userName = args.signerName;
  viewRequest.clientUserId = args.signerClientId;

  viewRequest.frameAncestors = [
    "http://localhost:4000/api",
    "https://apps-d.docusign.com",
  ];
  viewRequest.messageOrigins = ["https://apps-d.docusign.com"];

  return viewRequest;
};

解决步骤

1. 修正frameAncestors配置

当前frameAncestors设置不完整,需包含React应用完整域名(含端口),并覆盖DocuSign所有必要子域:

  • 在makeRecipientViewRequest函数中,更新frameAncestors:
    viewRequest.frameAncestors = [
      "https://localhost:5173", // 替换为你的React应用实际地址
      "https://*.docusign.com",
      "https://*.docusign.net"
    ];
    
  • 删除createRecipientView调用中外层重复的frameAncestors参数,避免配置冲突。

2. 配置React应用的CSP规则

在React的public/index.html中添加正确的CSP meta标签:

<meta http-equiv="Content-Security-Policy" content="
  frame-src https://*.docusign.com https://*.docusign.net;
  script-src https://*.docusign.com 'unsafe-inline' 'unsafe-eval';
  style-src https://*.docusign.com 'unsafe-inline';
  img-src https://*.docusign.com data:;
  connect-src https://*.docusign.com https://*.docusign.net
">

3. 修复React组件的依赖与清理逻辑

更新useEffect的依赖数组,并添加实例清理逻辑,避免内存泄漏:

useEffect(() => {
  let signingInstance;
  const loadDocuSign = async () => {
    try {
      const docusign = await window.DocuSign.loadDocuSign(integrationKey);
      const url = `${docUrl}&output=embed`;
      signingInstance = docusign.signing({ url, displayFormat: 'focused' });
      signingInstance.mount('#agreement');
    } catch (ex) {
      console.error(ex);
    }
  };

  loadDocuSign();

  // 组件卸载时清理实例
  return () => {
    if (signingInstance) {
      signingInstance.unmount();
    }
  };
}, [integrationKey, docUrl]); // 添加依赖,确保参数变化时重新加载

4. 验证信封状态与参数一致性

确保makeEnvelope中env.status = "sent",且signerClientId在创建信封和收件人视图时完全一致。

内容的提问来源于stack exchange,提问作者ubi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 07:27:08