Как получить значение дайджеста запроса из приложения, размещенного у провайдера?

Я разрабатываю приложение, размещенное у поставщика SharePoint 2013, с использованием javascript REST Api. Чтобы выполнить операции создания (POST) или обновления (MERGE) для элементов sharepoint, мне нужно установить заголовок «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-адреса на что-то поддельное, но в основном мы говорим прокси-серверу использовать хост-сеть, имеющую цель, и получить конечную точку /_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 с пустым телом.

Как передать значение дайджеста запроса из службы /_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." Я предполагаю, что значение дайджеста запроса содержит некоторую информацию заголовка реферера, которая указывает, что оно было запрошено сервером; однако будущие запросы, сделанные с ним, будут из браузера, и это несоответствие может быть приемлемой причиной того, что он недействителен.

Еще несколько заметок о попытке добавить контроллер webAPI. Я создал свой код на основе этого примера: http://code.msdn.microsoft.com/SharePoint-2013-Perform-335d925b, но преобразовал его для использования более нового HttpClient. Я перегрузил метод Page_Load, сохранил contextTokenString в переменной, к которой мог получить доступ контроллер WebAPI, а затем проанализировал/использовал его при запросе contextinfo.

Кто-нибудь знает, является ли это правильным диагнозом этой ошибки? Есть ли что-то, закодированное в значении дайджеста запроса, что не позволяет его получить, как я предложил?

Я также открыл связанный вопрос на форумах MSDN, так как отчаянно пытаюсь найти ответ: http://social.msdn.microsoft.com/Forums/sharepoint/en-US/f601fddd-3747-4152-b2d1-4e89f0a771c4/question-about-limitation-of-providerhosted-apps-возможноли-делать-остальные-вызовы-с-javascript?forum=sharepointdevelopmentprevious

Мне очень трудно поверить, что это может быть ограничением приложений, размещенных у провайдера, но, учитывая все проведенное мной тестирование, я начинаю сомневаться в жизнеспособности приложений, размещенных у провайдера, когда вы хотите писать на javascript.

Прошу о помощи!


person Matt Mazzola    schedule 03.03.2014    source источник


Ответы (4)


Я понимаю, что вы уже ответили на свой вопрос в контексте приложения, размещенного у поставщика, но для таких разработчиков, как я, которым нужен доступ к REST API с языка, не основанного на платформе .NET (и которые не могут написать свой проект как веб-приложение) Я хотел бы немного расширить тему. Недавно мне поручили написать приложение для iPad, которое требовало этой функциональности, и в итоге я перепроектировал следующее:

Шаг 1 - Аутентификация

Не буду подробно рассказывать об этом, так как в Интернете есть множество примеров, которые демонстрируют более распространенные методы. Библиотеки Microsoft.SharePoint.Client в основном используют проверку подлинности на основе утверждений при работе с SharePoint Online, при этом токен запрашивается через конечную точку, расположенную по адресу: https://login.microsoftonline.com/RST2.srf

Шаг 2. Получение дайджеста запроса (глупый подход)

Если вам лень, вы всегда можете взять свои аутентифицированные файлы cookie, сделать запрос GET на домашнюю страницу целевой сети и использовать регулярное выражение, например:

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

чтобы очистить HTML от ответа. Оттуда нужно будет просто извлечь атрибут value для вашего дайджеста.

Шаг 2. Получение дайджеста запроса (подход SOAP)

Библиотеки CSOM в настоящее время используют конечную точку SOAP при получении дайджеста запроса, используемого для вызовов API. Вы можете сделать то же самое, отправив запрос SOAP к веб-службе $(SPWebUrl)/_vti_bin/sites.asmx, подобный следующему:

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.

Шаг 2. Получение дайджеста запроса (подход REST)

Последний подход, о котором я знаю, использует запрос OData, сделанный к конечной точке $(SPWebUrl)/_api/contextinfo:

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.

Шаг 2. Получение дайджеста запроса (подход CSOM)

Если вы используете 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.

Шаг ??? - Обработка ошибок

Несколько примеров ответов, когда наши запросы терпят неудачу:

Следующий ответ исходит от запроса REST к конечной точке $(SPWebUrl)/_api/contextinfo. (файлы 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."
        }
    }
}
person Charles Grunwald    schedule 03.09.2014
comment
Спасибо за решение. Я знаю, что есть также способ сделать вызов REST для получения дайджеста запроса (site.sharepoint.com/ _api/contextinfo), передав файлы cookie rtFa и FedAuth, но я продолжаю получать ответ 403. Вы пробовали вызов REST? - person Laurent Rivard; 07.08.2015
comment
Ага. Когда я его использую, я отправляю пустой объект JSON (например: {}), но я думаю, что вы можете отправить его с пустым телом. Если это не сработает для вас, дайте мне знать, и я отредактирую свой пост с дополнительной информацией. - person Charles Grunwald; 08.08.2015
comment
Я все еще получаю 403, когда публикую сообщение с пустым телом. - person Laurent Rivard; 10.08.2015
comment
Извините, я не обратил достаточно внимания, когда читал ваш комментарий, поэтому я полностью пропустил немного о коде ответа 403. Есть несколько разных причин для ответа 403, поэтому мне нужно больше информации, чтобы дать вам точный ответ. Действительные запросы OData, как правило, завершатся ошибкой с подробной информацией об ошибке в теле ответа, поэтому проверьте и посмотрите, есть ли в теле ответа 403 что-нибудь полезное. Тем временем я обновил свой первоначальный ответ подходом REST и несколькими примерами неудачных ответов для сравнения. - person Charles Grunwald; 12.08.2015
comment
Есть ли пример с С#? Я использую CSOM. - person Rana; 28.01.2016
comment
CSOM имеет для этого встроенную функциональность в классе Microsoft.SharePoint.Client.ClientContext. Я добавил некоторую информацию в свой ответ под заголовком: Шаг 2. Получение дайджеста запроса (подход CSOM) - person Charles Grunwald; 30.01.2016
comment
Это помогло... Он доступен в JSOM в SP.ClientRuntimeContext.$K и, по-видимому, обрабатывает кеширование, хотя я не уверен, что это правильный способ доступа к нему... 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); }); - person bdimag; 23.03.2016
comment
Похоже, что SP.ClientRuntimeContext обрабатывает большую часть запросов дайджеста в executeQueryAsync. Поскольку код JSOM в основном генерируется, я бы с осторожностью полагался на существование внутренних имен членов. Я смотрю на копию SP.Runtime.js, которую я скачал из SharePoint Online несколько месяцев назад, что наводит меня на мысль, что имена участников могут меняться при обновлениях. (он использует $Q вместо $K и $2c_0 вместо $26_0) Тем не менее, вы можете разрешить имя члена во время выполнения и использовать именованный API для всего остального. См.: gist.github.com/juntalis/4ed013b5b2b299b36f5f - person Charles Grunwald; 29.03.2016
comment
Отличный ответ Чарльз Грюнвальд. Спасибо - person SP Developer; 10.10.2016
comment
Я могу получить новое значение дайджеста из _api/contextinfo, однако я могу использовать его только в течение окна истечения срока действия предыдущего дайджеста запроса. Допустим, я подключаюсь к сайту SharePoint и получаю дайджест запроса A, который действителен в течение 30 минут. Через 15 минут я обновляю дайджест запроса и получаю еще один дайджест B, затем я должен использовать B в течение 15 минут, иначе я получу ошибку проверки безопасности. Это почему? - person David Sagang; 10.07.2020
comment
Извините, @DavidSagang. Я не работал с SharePoint с 2016 года, поэтому не совсем разбираюсь в текущем поведении. - person Charles Grunwald; 18.07.2020

Хорошо, я сделал новое приложение, размещенное у провайдера, чтобы повторно проверить проблему.

Посмотреть репозиторий можно здесь:

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 был установлен на URL-адрес appWeb, но на самом деле при отправке запроса в appWeb с помощью RequestExecutor вам не нужно этого делать. Это просто appweburl + "/_api/contextinfo". Только при выполнении запросов к ресурсам, существующим на hostWeb, вам нужно использовать AppContextSite и устанавливать @target.

Вы можете увидеть полный код в связанном решении для получения более подробной информации. Я добавил скриншот решения. введите здесь описание изображения

person Matt Mazzola    schedule 29.03.2014

Вы должны помнить, что на уровне разрешений существует проверка, которая отключает все службы под _api.

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

Вы включаете это обеспечение

настройки сайта->разрешения сайта->уровень разрешений->чтение->

Возможности клиента интеграции Использовать удаленный интерфейс

Я нашел решение в https://letrasandnumeros.com/2017/02/28/unauthorizedaccessexception-sharepoint-_api/

person Lucaseto    schedule 28.02.2017

RequestExecutor фактически заботится о RequestDigest для вас. Вы не должны получить его.

Если по какой-то причине вы все еще хотите получить значение RequestDigest, попробуйте выполнить вызов, не меняя контекст сайта.

person Gab Royer    schedule 25.03.2014
comment
Если я не ошибаюсь, вызов appWeb или hostWeb по-прежнему является междоменным, поэтому это невозможно. домен приложения: providerhosted.azurewebsites.com веб-домен приложения: contoso-6f921c6addc19f.sharepoint.com веб-домен хоста: contoso.sharepoint.com - person Matt Mazzola; 26.03.2014
comment
Вы правы, вам все равно понадобится исполнитель запроса, чтобы сделать этот вызов. Я удалил URL-адрес, так как это сбивало с толку. Для этого вам все равно понадобится RequestExecutor. - person Gab Royer; 26.03.2014