SharePoint 外接程序的授权代码 OAuth 流

注意

本文要求你熟悉创建使用低信任授权的 SharePoint 加载项以及 OAuth 背后的概念和原理。 有关 OAuth 的详细信息,请参阅 OAuth.netWeb 授权协议 (oauth)

重要

作为 Azure Active Directory (Azure AD) 的一项服务,Azure 访问控制 (ACS) 将于 2018 年 11 月 7 日停用。 此停用不会影响使用 https://accounts.accesscontrol.windows.net 主机名(不受此停用影响)的 SharePoint 加载项模型。 有关详细信息,请参阅停用 Azure 访问控制对 SharePoint 加载项的影响

在某些情况下,加载项可实时请求访问 SharePoint 资源的权限;即,加载项可请求在运行时(而不是安装加载项时)动态访问 SharePoint 资源的权限。 此类型的加载项不必从 SharePoint 上启动,甚至不必在 SharePoint 上安装。 例如,它可以是从任何网站启动的本机设备加载项,或者是从 Office 应用程序启动、需要实时访问 SharePoint 上的资源的 Office 加载项。

注意

运行此类外接程序的用户必须对外接程序要访问的资源拥有管理权限。 例如,当外接程序仅请求对网站的读取权限时,如果用户拥有对该网站的读取权限,但没有管理权限,则不能运行该外接程序。

若要能够调入 SharePoint,必须首先通过卖家面板或 AppRegNew.aspx 页注册此类型的加载项。 有关通过卖家面板或 AppRegNew.aspx 注册加载项的详细信息,请参阅注册 SharePoint 加载项

注册加载项后,它会成为安全主体,并且具有与用户和组相同的标识。 此标识称为加载项主体。 与用户和组一样,加载项主体具有特定权限。 有关加载项主体的详细信息,请参阅注册 SharePoint 加载项

注册加载项时,你将收到加载项主体的客户端 ID、客户端密码、加载项域和重定向 URI。 此信息可注册到授权服务器 Microsoft Azure 访问控制服务 (ACS) 中。

实时请求权限的加载项的授权代码 OAuth 流

本节总结了实时请求权限的 SharePoint 加载项的 OAuth 身份验证和授权流。 该流称为授权代码流。 相关过程介绍不从 SharePoint 中启动的加载项如何访问 SharePoint 中的资源。

注意

该流涉及加载项、SharePoint、授权服务器(即 ACS)和最终用户之间在运行时的一系列交互。 因此,该流要求 SharePoint Online 或 SharePoint 场连接到 Internet,以便可与 ACS 通信。 未连接到 Internet 的 SharePoint 场必须使用高信任授权系统

必须有一个 Web 应用程序或服务与 SharePoint 分开托管。 即使加载项是设备加载项,具有可在 ACS中注册的 Web 应用程序或服务 URL,即使 Web 组件不用于其他任何目的。

为简单起见,本文假定加载项为名为 Contoso.com 的 Web 应用程序。 该应用程序使用 SharePoint 客户端对象模型 (CSOM) 或 SharePoint REST API 调用 SharePoint。 当应用程序先尝试访问 SharePoint 时,SharePoint 会从 ACS 请求授权代码,前者可向 Contoso.com 应用程序发送授权代码。 然后,应用程序使用授权代码从 ACS 请求访问令牌。 收到访问令牌后,Contoso.com 应用程序会将其包含在它向 SharePoint 发出的所有请求中。

流的详细示例

假定 Contoso 可提供联机照片打印服务。 某用户希望打印一些照片。 该用户希望允许 Contoso 照片打印服务访问用户保存在 SharePoint Online 网站 fabrikam.sharepoint.com 上的一组照片库,并打印其中的照片。

OAuth 概述

照片打印应用程序已注册,因此具有客户端 ID、客户端密码和重定向 URI。 Contoso 在注册加载项时提供的重定向 URI 是 https://contoso.com/RedirectAccept.aspx。 客户端 ID 和客户端密码信息存储在照片打印应用程序的 web.config 文件中。 下面举例说明如何将客户端 ID 和客户端密码输入到 web.config 文件中。

<configuration>
  <appSettings>
    <add key="ClientId" value="c78d058c-7f82-44ca-a077-fba855e14d38 "/>
    <add key="ClientSecret" value="SbALAKghPXTjbBiLQZP+GnbmN+vrgeCMMvptbgk7T6w= "/>
  </appSettings>
</configuration>

授权代码流步骤

以下是授权代码流中的步骤。

提示

这些步骤引用 TokenHelper.cs 文件中的方法。 此托管代码没有经过编译,因此没有相关的引用主题。 然而,文件本身添加了全面注释,带有每个类、成员参数和返回值的说明。 请考虑在阅读这些步骤时打开一个副本作为参考。

步骤 1:客户端打开应用程序,然后将其定向到 SharePoint 网站以获取数据。

三段 OAuth 流 - 步骤 1

用户浏览到 Contoso 照片打印网站,其中 UI 指示用户可以打印保存在任何 SharePoint Online 网站上的照片。

在此示例中,URL 为 https://contoso.com/print/home.aspx

照片打印加载项请求用户输入照片集合的 URL。 用户输入一个指向 SharePoint Online 网站的 URL:https://fabrikam.sharepoint.com/

步骤 2:加载项重定向到 SharePoint 网站授权 URL

三段 OAuth 流 - 步骤 2

当用户选择此按钮获取照片时,Contoso 照片打印加载项将浏览器重定向到 https://fabrikam.sharepoint.com/此重定向为 HTTP 302 重定向响应。

如果你使用的是 Microsoft .NET,那么 Response.Redirect 是从代码中进行重定向的几种方法之一。 通过在项目中使用 TokenHelper.cs 文件,代码可以调用重载的 GetAuthorizationUrl 方法(使用带有三个参数的重载)。 此方法为你构造 OAuthAuthorize.aspx 重定向 URL。 或者,代码可以手动构造 URL。

例如,如果选择调用 GetAuthorizationUrl 方法来构造 OAuthAuthorize.aspx 重定向 URL,可使用项目中的 TokenHelper.cs,代码如下所示:

Response.Redirect(
  TokenHelper.GetAuthorizationUrl(
    sharePointSiteUrl.ToString(),
    "Web.Read List.Write",
    "https://contoso.com/RedirectAccept.aspx"
  )
);

如果查看 TokenHelper.csGetAuthorizationUrl 方法的三个参数重载,会发现第二个参数是权限范围参数,它是加载项请求的速记格式的空格分隔权限列表。 有关权限范围的详细信息,请参阅权限范围别名和 OAuthAuthorize.aspx 页面的使用方法

第三个参数必须与加载项注册时使用的重定向 URI 相同。 有关注册的详细信息,请参阅注册 SharePoint 加载项。返回的字符串是包含查询字符串参数的 URL。 如果你愿意,可以手动构造 OAuthAuthorize.aspx 重定向 URL。 例如,在本例中,Contoso 照片打印加载项将用户重定向到的 URL 为(为可读性添加的换行符):

https://fabrikam.sharepoint.com/_layouts/15/OAuthAuthorize.aspx?
    client_id=client_GUID
    &scope=app_permissions_list
    &response_type=code
    &redirect_uri=redirect_uri

如示例所示,Contoso 照片打印加载项将 OAuth 客户端 ID 和重定向 URI 发送到 Fabrikam 网站作为查询字符串参数。 以下是具有示例查询字符串值的 GET 请求示例。 实际目标 URL 为单行形式。

GET /_layouts/15/OAuthAuthorize.aspx?client_id=c78d058c-7f82-44ca-a077-fba855e14d38&scope=list.read&response_type=code&redirect_uri=https%3A%2F%2Fcontoso%2Ecom%2Fredirectaccept.aspx HTTP/1.1
Host: fabrikam.sharepoint.com

如果你需要单独的许可弹出对话框,可以将查询参数 IsDlg=1 添加到 URL 构造中,如下所示:/oauthauthorize.aspx?IsDlg=1&client_id=c78d058c-7f82-44ca-a077-fba855e14d38&scope=list.read&response_type=code&redirect_uri=https%3A%2F%2Fcontoso%2Ecom%2Fredirectaccept.aspx

三段 OAuth 流 - 步骤 3

如果用户尚未登录到 Fabrikam SharePoint Online 网站,将提示用户登录。 如果用户已登录,则 SharePoint 将呈现 HTML 许可页。 许可页会提示用户授予(或拒绝)Contoso 照片打印加载项请求的权限。 本例中,用户将授予加载项读取 Fabrikam 上的用户图片库的权限。

步骤 4:SharePoint 从 ACS 请求短生存期的授权代码

三段 OAuth 流 - 步骤 4

Fabrikam SharePoint Online 网站要求 ACS 创建短期(约 5 分钟)授权代码,该代码对此用户和外接程序组合具有唯一性。 ACS 将授权代码发送到 Fabrikam 网站。

步骤 5:SharePoint Online 网站重定向到应用程序的注册重定向 URI,将授权代码传递给加载项

三段 OAuth 流 - 步骤 5

Fabrikam SharePoint Online 网站通过 HTTP 302 响应将浏览器重新重定向回 Contoso。 为此重定向构造的 URL 使用注册照片打印加载项时指定的重定向 URI。 它还包括作为查询字符串的授权代码。

重定向 URL 的结构如下:https://contoso.com/RedirectAccept.aspx?code=[authcode]

步骤 6:加载项使用授权代码从 ACS 请求访问令牌,ACS 检验请求、使授权代码失效,然后将访问和刷新令牌发送到加载项。

三段 OAuth 流 - 步骤 6

Contoso 从查询参数检索授权代码,然后将其与客户端 ID 和客户端机密一起包含在向 ACS 发出的访问令牌请求中。

如果使用的是托管的代码和 SharePoint CSOM,则 TokenHelper.cs 文件(即向 ACS 发出请求的方法)为 GetClientContextWithAuthorizationCode。 在这种情况下,代码类似于以下 (其中 authCode 是授权代码已分配给) 的变量:

TokenHelper.GetClientContextWithAuthorizationCode(
  "https://fabrikam.sharepoint.com/",
  "00000003-0000-0ff1-ce00-000000000000",
  authCode,
  "1ee82b34-7c1b-471b-b27e-ff272accd564",
  new Uri(Request.Url.GetLeftPart(UriPartial.Path))
);

如果查看 TokenHelper.cs 文件,则 GetClientContextWithAuthorizationCode 方法的第二个参数是 targetPrincipalName。 此值始终是正在访问 SharePoint 的加载项中的常量 00000003-0000-0ff1-ce00-000000000000。 如果从 GetClientContextWithAuthorizationCode 跟踪调用层次结构,它会从 web.config 文件获取客户端 ID 和密码。

ACS 接收 Contoso 请求并验证客户端 ID、客户端密码、重定向 URI 和授权代码。 如果所有信息都有效,ACS 会使授权码无效(只能使用一次),并创建刷新令牌和访问令牌,并将这两种令牌返回到 Contoso。 Contoso 应用程序可以缓存此访问令牌,以便在以后的请求中重复使用。 默认情况下,访问令牌有效期限约为 12 小时。

每每个访问令牌特定于在原始授权请求中指定的用户帐户,并且仅授予对相应请求中指定的服务的访问权限。 加载项应该安全地存储访问令牌。 Contoso 应用程序还可缓存刷新令牌。 默认情况下,刷新令牌有效期限为 6 个月。 访问令牌过期后,可从 ACS 使用刷新令牌兑换新的访问令牌。

有关令牌的详细信息,请参阅在提供托管的低信任 SharePoint 加载项中处理令牌

步骤 7:现在加载项可以使用访问令牌从向用户显示的 SharePoint 网站请求数据。

三段 OAuth 流 - 步骤 7

Contoso 包括向 SharePoint 发出 REST API 调用或 CSOM 请求的访问令牌,在 HTTP Authorization 标头中传递 OAuth 访问令牌。 SharePoint 返回 Contoso 请求的信息。

有关如何发出该请求的详细信息,请参阅在提供程序托管的低信任 SharePoint 加载项中处理安全令牌

权限范围别名和 OAuthAuthorize.aspx 页面

本节要求你熟悉 SharePoint 中的加载项权限一文。 表 1 显示了与该文章中相同的加载项权限请求范围 URI,除了它具有一个额外的列(“范围别名”)以外,FullControl 权限在“可用权限”列中不可用,因为实时请求访问 SharePoint 资源的权限的加载项不能请求 FullControl 权限。

范围别名列中列出的值是范围 URI 列中其对应部分的简写形式。 别名只能由实时请求访问 SharePoint 资源的权限的加载项使用。 (范围 URI 值用于从 SharePoint 启动的加载项的外接程序清单中。这些加载项在加载项安装过程中请求权限。)

范围别名仅在使用 OAuthAuthorize.aspx 重定向页的上下文中使用。 如前一节中 OAuth 流的步骤 2 所示,如果加载项使用托管代码,则当调用项目中 TokenHelper.csGetAuthorizationUrl 方法时,将使用别名。 下面是另一个示例:

Response.Redirect(TokenHelper.GetAuthorizationUrl(
    sharePointSiteUrl.ToString(),
    "Web.Read List.Write ",
    "https://contoso.com/RedirectAccept.aspx ")
);

scope 参数值 Web.Read List.Write 是你将如何使用范围别名请求权限的示例。 scope 参数是权限范围和权限请求的以空格分隔的集合。

如果未使用托管代码,则在重定向 URL 的范围字段中使用范围别名。 例如:

https://fabrikam.sharepoint.com/_layout/15/OAuthAuthorize.aspx?client_id=c78d058c-7f82-44ca-a077-fba855e14d38&scope=list.write&response_type=code&redirect_uri=https%3A%2F%2Fcontoso%2Ecom%2Fredirectaccept.aspx

注意

有关范围的说明,请参阅 SharePoint 中的加载项权限

表 1. SharePoint 加载项权限请求范围 URI 及其对应的别名

范围 URI 范围别名 可用权限
https://sharepoint/content/sitecollection Site 读取、写入、管理
https://sharepoint/content/sitecollection/web Web 读取、写入、管理
https://sharepoint/content/sitecollection/web/list 列表 读取、写入、管理
https://sharepoint/content/tenant AllSites 读取、写入、管理
https://sharepoint/bcs/connection 无(当前不支持) 阅读
https://sharepoint/search 搜索 QueryAsUserIgnoreAppPrincipal
https://sharepoint/projectserver ProjectAdmin 管理
https://sharepoint/projectserver/projects 项目 读取、写入
https://sharepoint/projectserver/projects/project 项目 读取、写入
https://sharepoint/projectserver/enterpriseresources ProjectResources 读取、写入
https://sharepoint/projectserver/statusing ProjectStatusing SubmitStatus
https://sharepoint/projectserver/reporting ProjectReporting 阅读
https://sharepoint/projectserver/workflow ProjectWorkflow 提升
https://sharepoint/social/tenant AllProfiles 读取、写入、管理
https://sharepoint/social/core 社交 读取、写入、管理
https://sharepoint/social/microfeed Microfeed 读取、写入、管理
https://sharepoint/taxonomy TermStore 读取、写入

重定向 URI 和示例重定向页面

实时请求权限的加载项使用的重定向 URI 是 SharePoint 在获得许可后将浏览器重定向到的 URI(包含授权代码作为请求参数)。 OAuth 流的步骤 2 提供了一个示例,其中 URI 在对 GetAuthorizationUrl 方法的调用中进行硬编码。 或者,ASP.NET 加载项也可以在 web.config 文件中存储重定向 URI,如本示例中所示:

<configuration>
  <appSettings>
    <add key="RedirectUri" value="https://contoso.com/RedirectAccept.aspx" />
  </appSettings>
<configuration>

可以通过调用 WebConfigurationManager.AppSettings.Get("RedirectUri") 检索该值。

位于 RedirectUri 的终结点从查询参数获取授权代码并使用授权代码来获取访问令牌,然后可以使用访问令牌来访问 SharePoint。 终结点通常为同一个页面或控制器方法,或者最初尝试访问 SharePoint 的 Web 方法。 但是,也可以是仅接收授权代码的页面或方法,然后将授权代码重定向到另一个页面或方法。 特殊页面或方法可以传递或缓存授权令牌。 (有效期约为 5 分钟。)或者,终结点也可以使用授权令牌获取其缓存的访问令牌。

重要

AppRegNew.aspx 页面中创建应用时,RedirectUri 必须是所列的相同终结点。

以下是 ASP.NET 应用程序中此类页面后台的代码示例。 关于此代码,请注意以下几点:

  • 它使用 Visual Studio 的 Office 开发人员工具生成的 TokenHelper.cs 文件。
  • 该代码假定有一个“代码”查询参数,其中包含一个授权代码。 这是安全的,因为页面仅由 SharePoint 调用,并且只有在它传递授权代码时才调用。
  • 它使用 CSOM 客户端上下文对象访问 SharePoint,但它可能也在服务器上缓存该对象,并重定向到另一个页面。
  • GetClientContextWithAuthorizationCode 方法使用授权代码来获取访问代码。 然后它创建 SharePoint 客户端上下文对象,并修改对象针对 ExecutingWebRequest 事件的处理程序,以便处理程序在对 SharePoint 的所有请求中包含访问令牌。 访问令牌实际上缓存在对象内部。
  • GetClientContextWithAuthorizationCode 方法在 rUrl 参数中将重定向 URL 发送回 ACS,但是 ACS 将其作为一种标识,以防授权代码被窃取。 ACS 不会使用它再次重定向,因此该代码不会无限制地循环以重定向到它本身。
  • 代码不对过期访问令牌的处理做出规定。 创建客户端上下文对象后,它会始终使用相同的访问令牌。 它不使用刷新令牌。 对于仅用于会话持续时间短于访问令牌有效期的加载项,这是有效策略。

有关使用刷新令牌获取新访问令牌的更复杂示例,请参阅下一节。

public partial class RedirectAccept : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    string authCode = Request.QueryString["code"];
    Uri rUri = new Uri("https://contoso.com/RedirectAccept.aspx");

    using (ClientContext context = TokenHelper.GetClientContextWithAuthorizationCode(
        "https://fabrikam.sharepoint.com/",
        "00000003-0000-0ff1-ce00-000000000000",
        authCode,
        "1ee82b34-7c1b-471b-b27e-ff272accd564".
        rUri))
    {
      context.Load(context.Web);
      context.ExecuteQuery();

      Response.Write("<p>" + context.Web.Title + "</p>");
    }
  }
}

可访问 SharePoint 的页面后台的示例代码

下面是 Default.aspx 页面的后台代码。 该页面假定方案中默认页面是加载项的启动页面,并且也是加载项的注册重定向 URL。 关于此代码,请注意以下几点:

  • Page_Load 方法首先检查查询字符串中是否有授权代码。 如果 SharePoint 将浏览器重定向到页面,则有一个授权代码。 如果有,代码会使用它获取新的刷新令牌,刷新令牌缓存在跨会话的持久性缓存中。

  • 然后该方法在缓存中检查刷新令牌。

    • 如果不存在,它会告知 SharePoint 所需的权限(Web 范围的写入权限)并请求 SharePoint 提供授权代码,从而获取一个刷新令牌。 系统会提示用户授予权限,如果授予了该权限,SharePoint 将从 ACS 获取授权代码,并在重定向到同一页面时将其作为查询参数发送回来。
    • 如果存在缓存的刷新令牌,该方法将使用它直接从 ACS 获取访问令牌。 如本文前一节末尾的示例所示,访问令牌用于创建 SharePoint 客户端上下文对象。 使用缓存刷新令牌直接从 ACS 获取访问令牌 可以避免在会话启动时向 SharePoint 发出额外的网络调用,因此在刷新令牌缓存有效期内重新运行加载项的用户将能更快地完成启动。
  • 如前一节末尾的示例所示,该代码未对过期访问令牌的处理做出任何规定。 创建客户端上下文对象后,它会始终使用相同的访问令牌。 防止过期访问令牌的一种方法是,除了刷新令牌以外,还缓存访问令牌。 可以修改下面的代码,使其仅在缓存中的所有访问令牌均已过期时调用 GetAccessToken 方法。

    但是,尽管将刷新令牌缓存在客户端上(例如,在 Cookie 中)可接受,但出于安全考虑,访问令牌应仅位于服务器端缓存中。 刷新令牌会进行加密,且只能由 ACS 解密。 但是访问令牌仅进行编码(使用 Base 64 编码),且在中间人攻击中可被轻松解码。

  • 此代码中引用的 TokenCache 类在本节后面进行定义。

Default.aspx 页面的后台代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.Samples;
using Microsoft.SharePoint.Client;

namespace DynamicAppPermissionRequest
{
  public partial class Default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      Uri sharePointSiteUrl = new Uri("https://fabrikam.sharepoint.com/print/");

      if (Request.QueryString["code"] != null)
      {
        TokenCache.UpdateCacheWithCode(Request, Response, sharePointSiteUrl);
      }

      if (!TokenCache.IsTokenInCache(Request.Cookies))
      {
        Response.Redirect(TokenHelper.GetAuthorizationUrl(sharePointSiteUrl.ToString(), "Web.Write"));
      }
      else
      {
        string refreshToken = TokenCache.GetCachedRefreshToken(Request.Cookies);
        string accessToken =
        TokenHelper.GetAccessToken(
                    refreshToken,
                    "00000003-0000-0ff1-ce00-000000000000",
                    sharePointSiteUrl.Authority,
                    TokenHelper.GetRealmFromTargetUrl(sharePointSiteUrl)).AccessToken;

        using (ClientContext context =
                TokenHelper.GetClientContextWithAccessToken(sharePointSiteUrl.ToString(),
                                                            accessToken))
        {
          context.Load(context.Web);
          context.ExecuteQuery();

          Response.Write("<p>" + context.Web.Title + "</p>");
        }
      }
    }
  }
}

下面是前一个示例代码调用的令牌缓存模块的代码示例。 它将 Cookie 用作缓存。 提供其他缓存选项。 有关详细信息,请参阅在提供程序托管的低信任 SharePoint 加载项中处理安全令牌

令牌缓存模块的代码示例

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.SharePoint.Samples;

namespace DynamicAppPermissionRequest
{
  public static class TokenCache
  {
    private const string REFRESH_TOKEN_COOKIE_NAME = "RefreshToken";

    public static void UpdateCacheWithCode(HttpRequest request,
                                            HttpResponse response,
                                            Uri targetUri)
    {
      string refreshToken =
          TokenHelper.GetAccessToken(
              request.QueryString["code"],
              "00000003-0000-0ff1-ce00-000000000000",
              targetUri.Authority,
              TokenHelper.GetRealmFromTargetUrl(targetUri),
              new Uri(request.Url.GetLeftPart(UriPartial.Path))
          ).RefreshToken;
      SetRefreshTokenCookie(response.Cookies, refreshToken);
      SetRefreshTokenCookie(request.Cookies, refreshToken);
    }

    internal static string GetCachedRefreshToken(HttpCookieCollection requestCookies)
    {
      return GetRefreshTokenFromCookie(requestCookies);
    }

    internal static bool IsTokenInCache(HttpCookieCollection requestCookies)
    {
      return requestCookies[REFRESH_TOKEN_COOKIE_NAME] != null;
    }

    private static string GetRefreshTokenFromCookie(HttpCookieCollection cookies)
    {
      if (cookies[REFRESH_TOKEN_COOKIE_NAME] != null)
      {
        return cookies[REFRESH_TOKEN_COOKIE_NAME].Value;
      }
      else
      {
        return null;
      }
    }

    private static void SetRefreshTokenCookie(HttpCookieCollection cookies, string refreshToken)
    {
      if (cookies[REFRESH_TOKEN_COOKIE_NAME] != null)
      {
        cookies[REFRESH_TOKEN_COOKIE_NAME].Value = refreshToken;
      }
      else
      {
        HttpCookie cookie = new HttpCookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken);
        cookie.Expires = DateTime.Now.AddDays(30);
        cookies.Add(cookie);
      }
    }
  }
}

另请参阅