如何从提供程序托管应用程序获取请求摘要值?

23
我正在使用JavaScript REST API开发SharePoint 2013提供程序托管应用程序。为了在SharePoint项目上执行创建(POST)或更新(MERGE)操作,我需要在请求中设置“X-RequestDigest”标头。
在SharePoint托管的应用程序中,我能够使用http://contoso.sharepoint.com/SharePointHostedApp/_api/contextinfo服务检索请求摘要值;然而,在提供程序托管的应用程序中获取该值时遇到了问题。
提供程序托管应用程序的第一个区别是现在我们需要进行跨域请求,因为我们不在SharePoint站点中运行,而是在托管在不同服务器上的不同域中。明确一下:代替
$.ajax({
    url: appWebUrl + '/_api/contextinfo',
    method: "POST",
    headers: { "Accept": "application/json; odata=verbose" }
})

我认为我们需要使用SP.RequestExecutor来执行跨域请求。 当我构建请求时,它看起来像下面这样(我已经更改了实际的URL以使用虚假的内容,但基本上我们告诉代理使用主机Web作为目标并获取/_api/contextinfo端点):

https://contoso-6f921c6addc19f.sharepoint.com/ProviderHostedApp/_api/SP.AppContextSite(@target)/contextinfo?@target=%27https://contoso.sharepoint.com%27

然而,我收到了这个错误:Cannot find resource for the request contextinfo. 意味着终结点不存在。
我确保使用了POST方法并正确使用了application/json;odata=verbose头和一个空的body。 如何从/_api/contextinfo服务中获取请求摘要值以供提供程序托管应用程序使用? 基于我所研究的:
  • 我们不能使用$('#__REQUESTDIGEST').val(),因为它不可用于提供程序托管应用。
  • 由于我在SharePoint之外运行,因此需要使用某种跨域请求。
  • 我尝试将跨域请求的目标设置为hostWebUrl和appWebUrl,但两者都会出现相同的错误。
必须有一些方法可以获取此值,否则当使用JavaScript时,我们将仅限于读操作。 有其他人使用javascript解决了这个问题吗? 从技术上讲,我可以尝试使用服务器上的CSOM实现所需的服务,并使用WebAPI或WCF公开它们,但这似乎是不合理的。 更新: 我尝试添加了一个WebAPI控制器,公开了一个检索请求摘要值的服务。这确实检索到了请求摘要值;但是,在尝试在以后的调用中使用它时,我收到错误消息:"The security validation for this page is invalid and might be corrupted. Please use your web browser's Back button to try your operation again." 我猜测请求摘要值中包含一些referer头信息,指示它是由服务器请求的;但是,使用它进行的未来请求来自浏览器,这种不匹配可能是其无效的可接受原因。
关于尝试添加WebAPI控制器的其他说明。 我基于此示例编写了代码:http://code.msdn.microsoft.com/SharePoint-2013-Perform-335d925b,但将其转换为使用较新的HttpClient。 我重载了Page_Load方法,将contextTokenString存储在可以被WebAPI控制器访问的变量中,然后在请求上下文信息时解析/使用它。

请问这个错误的诊断是否正确?请求摘要值中是否有编码的内容会阻止它像我建议的那样被检索出来?

我还在MSDN论坛上开了一个相关的问题,因为我迫切需要找到答案: http://social.msdn.microsoft.com/Forums/sharepoint/en-US/f601fddd-3747-4152-b2d1-4e89f0a771c4/question-about-limitation-of-providerhosted-apps-is-it-possible-to-make-rest-calls-with-javascript?forum=sharepointdevelopmentprevious

我很难相信这可能是提供程序托管应用程序的限制,但考虑到我所做的所有测试,我开始怀疑当你想用JavaScript编写时,提供程序托管应用程序的可行性。

请求帮助!

4个回答

31

我知道你已经在提供程序托管的应用程序的上下文中回答了自己的问题,但是对于像我这样需要从未基于.NET框架的语言访问REST API(且无法将其项目编写为Web应用程序)的开发人员,我想更详细地介绍一下这个主题。最近,我被要求编写一个需要此功能的iPad应用程序,并最终进行了以下反向工程:

第1步 - 认证

不会实际涵盖此内容,因为有很多在线示例展示了更常见的方法。当使用SharePoint Online时,Microsoft.SharePoint.Client库似乎大多使用基于声明的身份验证,其中通过在https://login.microsoftonline.com/RST2.srf找到的端点请求令牌。

第2步 - 获取请求摘要(愚笨的方法)

如果你感到懒惰,可以始终获取已验证的cookie,对目标Web的主页进行GET请求,并使用类似于以下正则表达式的内容:

/(<input (?:[^>]*?)name="?__REQUESTDIGEST"?(?:[^>]*?)\/>)/i

从响应中抓取HTML。从那里,只需提取摘要的value属性即可。

第2步 - 获取请求摘要(SOAP方法)

CSOM库当前在获取其API调用所使用的请求摘要时使用SOAP端点。您可以通过向类似于以下内容的$(SPWebUrl)/_vti_bin/sites.asmx Web服务发出SOAP请求来执行相同的操作:

POST $(SPWebUrl)/_vti_bin/sites.asmx HTTP/1.1
Content-Type: text/xml
SOAPAction: http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation
X-RequestForceAuthentication: true
Host: $(SPSiteHostname)
Expect: 100-continue
Accept-Encoding: gzip, deflate
Cookie: $(Authenticated Cookies - Either "FedAuth=...; rtFa=..." or "SPOIDCRL=...")
Content-Length: $(Whatever)

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/" />
    </soap:Body>
</soap:Envelope>

执行成功时,响应体将类似于:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <soap:Body>
        <GetUpdatedFormDigestInformationResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
            <GetUpdatedFormDigestInformationResult>
                <DigestValue>0x1122334455 ... FF,27 Jul 2015 03:06:54 -0000</DigestValue>
                <TimeoutSeconds>1800</TimeoutSeconds>
                <WebFullUrl>$(SPWebUrl)</WebFullUrl>
                <LibraryVersion>16.0.3208.1222</LibraryVersion>
                <SupportedSchemaVersions>14.0.0.0,15.0.0.0</SupportedSchemaVersions>
            </GetUpdatedFormDigestInformationResult>
        </GetUpdatedFormDigestInformationResponse>
    </soap:Body>
</soap:Envelope>

此时,您只需从DigestValue块中提取请求摘要。

第二步 - 获取请求摘要(REST方法)

我知道的最后一种方法是使用向$(SPWebUrl)/_api/contextinfo端点发出的OData请求:

POST $(SPWebUrl)/_api/contextinfo HTTP/1.1
Host: $(SPSiteHostname)
DataServiceVersion: 3.0
Accept: application/json; odata=nometadata
Content-Type: application/json; odata=verbose
Cookie: $(Authenticated Cookies)
Content-Length: 2

{}

执行成功后,响应主体将如下所示:

{
    "FormDigestTimeoutSeconds" : 1800,
    "FormDigestValue" : "0x1122334455 ... FF,27 Jul 2015 03:06:54 -0000",
    "LibraryVersion" : "16.0.4230.1217",
    "SiteFullUrl" : "$(SPSiteUrl)",
    "SupportedSchemaVersions" : ["14.0.0.0", "15.0.0.0"],
    "WebFullUrl" : "$(SPWebUrl)"
}

请求摘要可以从FormDigestValue属性中提取。

如果您正在使用CSOM,那么您已经内置了处理此问题的功能。(可能是JSOM,除非它使用__REQUESTDIGEST输入)Microsoft.SharePoint.Client.ClientContext在内部使用SOAP方法来管理其请求摘要,并通过其GetFormDigestDirect方法公开此功能。

ClientContext clientContext = new ClientContext(webUrl);
// ...
FormDigestInfo formDigest = clientContext.GetFormDigestDirect();

// X-RequestDigest header value
string headerValue = formDigest.DigestValue;

// Digest expiration
DateTime expirationDate = formDigest.Expiration;

使用说明:虽然 ClientContext 会维护和重复使用缓存的表单摘要,但此方法不会使您访问该缓存值。相反,每次调用此方法都会请求一个全新的表单摘要,因此您需要设置自己的缓存机制,以便在多个请求中重复使用未过期的摘要。

步骤2 - 获取请求摘要(JSOM方法)

如果您正在使用JSOM API并且没有访问__REQUESTDIGEST输入值,则可以使用以下扩展名访问ClientContext的缓存摘要。(感谢 bdimag 指出缓存

步骤3 - 获取新的请求摘要

假设您在 TimeoutSeconds 过去之前使用了请求摘要,那么可以通过以下方式进行有效的REST请求:

POST $(SPWebUrl)/_api/web/lists/getByTitle('MyList')/getchanges HTTP/1.1
Host: $(SPSiteHostname)
DataServiceVersion: 3.0
Accept: application/json; odata=nometadata
Content-Type: application/json; odata=verbose
X-RequestDigest: $(Request Digest)
Cookie: $(Authenticated Cookies)
Content-Length: 140

{
    "query" : {
        "__metadata" : {
            "type" : "SP.ChangeQuery"
        },
        "Add" : "True",
        "Item" : "True",
        "Update" : "True"
    }
}

应该会产生成功的响应。如果你检查响应的头部,你会发现类似这样的东西:

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: application/json;odata=fullmetadata;streaming=true;charset=utf-8
...
X-RequestDigest: 0xAABBCC...00,03 Sep 2014 18:09:34 -0000
...
提取X-RequestDigest响应头将允许您在随后的调用中使用它。(我猜超时是从您的新响应时间开始+原始摘要请求中的$(TimeoutSeconds),但我还没有确认)
不幸的是,X-RequestDigest标头仅由实际需要请求摘要的REST请求返回。对于不需要请求摘要的请求,例如:$(SPWebUrl)/_api/web/lists/getByTitle('MyList')/items,您将不会收到该标头。如果您发现自己需要在原始计时器超时后获取新的摘要,则需要向$(SPWebUrl)/_vti_bin/sites.asmx Web服务发起另一个请求。
步骤??? - 处理错误
我们请求失败时的几个示例响应:
下面的响应来自向$(SPWebUrl)/_api/contextinfo端点发出的REST请求。(未指定身份验证Cookie)
HTTP/1.1 403 Forbidden
Cache-Control: private, max-age=0
Content-Type: application/json;odata=nometadata;charset=utf-8
...
Server: Microsoft-IIS/8.5
X-SharePointHealthScore: 0
X-Forms_Based_Auth_Required: $(SPRootSiteUrl)/_forms/default.aspx?ReturnUrl=/_layouts/15/error.aspx&Source=%2f_vti_bin%2fclient.svc%2fcontextinfo
X-Forms_Based_Auth_Return_Url: $(SPRootSiteUrl)/_layouts/15/error.aspx
X-MSDAVEXT_Error: 917656; Access+denied.+Before+opening+files+in+this+location%2c+you+must+first+browse+to+the+web+site+and+select+the+option+to+login+automatically.
DATASERVICEVERSION: 3.0
X-AspNet-Version: 4.0.30319
X-IDCRL_AUTH_PARAMS_V1: IDCRL Type="BPOSIDCRL", EndPoint="$(SiteRelativeUrl)/_vti_bin/idcrl.svc/", RootDomain="sharepoint.com", Policy="MBI"
...
Date: Wed, 12 Aug 2015 02:27:35 GMT
Content-Length: 201

{
    "odata.error" : {
        "code" : "-2147024891, System.UnauthorizedAccessException",
        "message" : {
            "lang" : "en-US",
            "value" : "Access denied. You do not have permission to perform this action or access this resource."
        }
    }
}

接下来是一个来自于使用过期请求摘要的REST请求的响应(请注意响应中指定的X-RequestDigest标头。不确定是否可用,但值得一试):

HTTP/1.1 403 FORBIDDEN
Cache-Control: private, max-age=0
Content-Type: application/json;odata=fullmetadata;charset=utf-8
...
Server: Microsoft-IIS/8.5
Set-Cookie: rtFa=$(RtfaAuthCookie)
Set-Cookie: FedAuth=$(FedAuth)
X-SharePointHealthScore: 0
X-RequestDigest: 0x19EFFF80617AB2E48B0A9FF0ABA1440B5301E7445F3859177771BF6A39C7E4A74643108D862505A2C99350B0EDB871EF3DDE960BB68060601268818027F04956,12 Aug 2015 02:39:22 -0000
DATASERVICEVERSION: 3.0
X-AspNet-Version: 4.0.30319
...
Date: Wed, 12 Aug 2015 02:39:22 GMT
Content-Length: 253

{
    "odata.error" : {
        "code" : "-2130575251, Microsoft.SharePoint.SPException",
        "message" : {
            "lang" : "en-US",
            "value" : "The security validation for this page is invalid and might be corrupted. Please use your web browser's Back button to try your operation again."
        }
    }
}

1
感谢您提供的解决方案。我知道还有另一种方法可以通过REST调用来获取请求摘要(https://site.sharepoint.com/_api/contextinfo),方法是通过传递rtFa和FedAuth cookies来实现,但是我一直收到403响应。您尝试过REST调用吗? - Laurent Rivard
是的。当我使用它时,我会POST一个空的JSON对象(例如:{}),但我认为你也可以POST一个空的主体。如果这对你不起作用,请告诉我,我会在我的帖子中编辑更多信息。 - Charles Grunwald
当我使用空主体进行发布时,仍然会收到403错误。 - Laurent Rivard
抱歉,我在阅读您的评论时没有足够的注意力,所以完全忽略了关于403响应代码的部分。 403响应有几个不同的原因,因此我需要更多信息才能给出准确的响应。有效的OData请求通常会在响应正文中失败并提供错误详细信息,因此请检查并查看您的403响应正文是否有任何有用的信息。同时,我更新了我的原始回复,提供了REST方法和一些示例失败响应以供比较。 - Charles Grunwald
1
这有帮助... 它在SP.ClientRuntimeContext.$K中可用于JSOM,并似乎处理缓存 - 虽然我不确定访问它的合适方式是什么... 如果已经有上下文或运行空查询:console.log(SP.ClientRuntimeContext.$K[SP.ClientContext.get_current().$26_0()].$c_0); var context = new SP.ClientContext("/sites/yourCollection"); context.executeQueryAsync(function() { console.log(SP.ClientRuntimeContext.$K[context.$26_0()].$c_0); }); - bdimag
显示剩余6条评论

2
你必须记住,在权限级别中存在一个检查,禁用所有_api下的服务。

_api/web/lists _api/search/query?querytext=’SharePoint’ _api/SP.UserProfiles.PeopleManager

您需要启用的功能是确保:

网站设置 -> 站点权限 -> 权限级别 -> 读取 ->

集成客户端功能 使用远程接口

我在以下链接中找到了解决方案: https://letrasandnumeros.com/2017/02/28/unauthorizedaccessexception-sharepoint-_api/


2

好的,我创建了一个全新的提供程序托管应用程序来重新测试这个问题。

您可以在此处查看存储库:

https://github.com/mattmazzola/providerhosted_01

在比较这个新应用程序和旧应用程序之后,我意识到我误解了SP.RequestExecutor期望如何构造URL的方式。我认为需要使用SP.AppContextSite()端点。

我错误地构建了对appWeb的请求,类似于以下URL:

https://contoso-6f921c6addc19f.sharepoint.com/ProviderHostedApp/_api/SP.AppContextSite(@target)/contextinfo?@target=%27https%3A%2F%2Fcontoso-6f921c6addc19f.sharepoint.com%2FProviderHostedApp%27

正如您所看到的那样,@target被设置为appWeb的URL,但实际上当使用RequestExecutor请求appWeb时,您不需要这样做。它只是appweburl + "/_api/contextinfo"。只有在请求存在于hostWeb上的资源时,您才需要使用AppContextSite并设置@target。

您可以在链接解决方案中查看完整的代码以获取更多详细信息。我已经添加了一个解决方案的截图。 enter image description here


0

RequestExecutor 实际上会为您处理 RequestDigest。您不必自己获取它。

如果出于某种原因,您仍然想要获取 RequestDigest 值,请尝试在不更改上下文站点的情况下进行调用。


除非我误解了,否则调用appWeb或hostWeb仍然是跨域的,因此不可能实现。app域:providerhosted.azurewebsites.com appWeb域:https://contoso-6f921c6addc19f.sharepoint.com hostWeb域:https://contoso.sharepoint.com - Matt Mazzola
你是正确的,你仍然需要请求执行器来进行这个调用。我删除了URL,因为它很令人困惑。确实,你仍然需要RequestExecutor。 - Gab Royer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接