可以使用 SharePoint 客户端对象模型 (CSOM) 在 SharePoint 中检索、更新和管理数据。 SharePoint 按以下几种形式提供此 CSOM:
- .NET Framework 可再发行程序集
- .NET Standard 可再发行程序集
- JavaScript 库 (JSOM)
- REST/OData 终结点
在本文中,我们将重点介绍 .NET Framework 版本和 .NET Standard 可再发行版本之间的区别。 这两个版本在许多方面完全相同,并且如果你一直在使用 .NET Framework 版本编写代码,那么在使用 .NET Standard 版本时,该代码和你学到的一切在很大程度上仍将是相关的。
.NET Framework 版本和 .NET Standard 版本之间的主要区别
下表概括了两个版本之间的区别,并提供了有关如何处理这些区别的指南。
CSOM 功能 | .NET Framework 版本 | .NET Standard 版本 | 准则 |
---|---|---|---|
.NET 可支持性 | .NET Framework 4.5+ | .NET Framework 4.6.1+、.NET Core 2.0+、Mono 5.4+ (.NET docs) | 建议针对所有 SharePoint Online CSOM 开发使用 CSOM for .NET Standard 版本 |
跨平台 | 否 | 是(可用于任何支持 .NET Standard 的平台) | 对于跨平台,必须使用 CSOM for .NET Standard |
本地 SharePoint 支持 | 是 | 否 | CSOM .NET Framework 版本仍然完全受支持并且持续更新,因此请使用它们进行本地 SharePoint 开发 |
支持旧式身份验证流程(使用 SharePointOnlineCredentials 类的所谓基于 cookie 的身份验证) |
是 | 否 | 请参阅对 CSOM for .NET Standard 使用新式身份验证一章。 建议使用 Azure AD 应用程序配置 SharePoint Online 的身份验证 |
SaveBinaryDirect / OpenBinaryDirect API(基于 webdav) |
是 | 否 | 在 CSOM 中使用常规文件 API,因为不建议使用 BinaryDirect API,即使在使用 .NET Framework 版本时也不例外 |
Microsoft.SharePoint.Client.Utilities.HttpUtility 类 |
是 | 否 | 切换到 .NET 中的类似类,如 System.Web.HttpUtility |
Microsoft.SharePoint.Client.EventReceivers 命名空间 |
是 | 否 | 切换到新式事件概念,如 Web 挂钩。 |
注意
.NET 标准版本的 CSOM 程序集包含在自版本 16.1.20211.12000 以来名为 Microsoft.SharePointOnline.CSOM 的现有 NuGet 包中。 以下示例需要此版本或更高版本才能在 .Net core/标准目标项目中工作。
对 CSOM for .NET Standard 使用新式身份验证
使用通过 SharePointOnlineCredentials
类实施的基于用户/密码的身份验证是使用 CSOM for .NET Framework 的开发人员的常用方法。 在 CSOM for .NET Standard 中,这种方法不再可行,由使用 CSOM for .NET Standard 的开发人员来获取 OAuth 访问令牌并在调用 SharePoint Online 时使用该令牌。 建议通过设置 Azure AD 应用程序来为 SharePoint Online 获取访问令牌。 对于 CSOM for .NET Standard,唯一重要的是获取有效的访问令牌,可以通过使用资源所有者密码凭据流、使用设备登录信息、使用基于证书的身份验证等等来实现。
在本章中,我们将使用 OAuth 资源所有者密码凭据流生成一个 OAuth 访问令牌,然后,CSOM 将其用于模仿 SharePointOnlineCredentials
类的行为,以对 SharePoint Online 进行身份验证请求。
在 Azure AD 中配置应用程序
以下步骤将帮助你在 Azure Active Directory 中创建和配置应用程序:
- 通过 https://aad.portal.azure.com 转到 Azure AD 门户
- 选择“Azure Active Directory”,并在左侧导航应用注册上
- 选择“新注册”
- 输入应用程序的名称,然后选择“注册”
- 转到“API 权限”以授予应用程序权限,选择“添加权限”,选择“SharePoint”、“委派权限”,然后选择例如 AllSites.Manage。
- 选择“授予管理员许可”同意应用程序的请求权限
- 选择左侧导航栏中的“身份验证”
- 将“允许公共客户端流”从“否”更改为“是”。
- 选择“概述”并将应用程序 ID 复制到剪贴板(稍后需要使用它)
从Azure AD 获取访问令牌,并在基于 CSOM for .NET Standard 的应用程序中使用该令牌
使用 CSOM for .NET Standard 时,开发人员有责任获取 SharePoint Online 的访问令牌,并确保将其插入到对 SharePoint Online 的每次调用中。 实现此操作的常见代码模式如下所示:
public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)
{
context.ExecutingWebRequest += (sender, e) =>
{
// Get an access token using your preferred approach
string accessToken = MyCodeToGetAnAccessToken(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password);
// Insert the access token in the request
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
}
通过 GetContext
方法获取的 ClientContext
可以像其他任何 ClientContext
一样使用,并且可以与所有现有代码一起使用。 下面的代码段显示帮助程序类和使用该帮主程序类的控制台应用,重用这些类将能够轻松实施 SharePointOnlineCredentials
类的等效操作。
注意
PnP 网站核心库具有类似的 AuthenticationManager 类,支持更多基于 Azure AD 的身份验证流。
控制台应用示例
public static async Task Main(string[] args)
{
Uri site = new Uri("https://contoso.sharepoint.com/sites/siteA");
string user = "joe.doe@contoso.onmicrosoft.com";
SecureString password = GetSecureString($"Password for {user}");
// Note: The PnP Sites Core AuthenticationManager class also supports this
using (var authenticationManager = new AuthenticationManager())
using (var context = authenticationManager.GetContext(site, user, password))
{
context.Load(context.Web, p => p.Title);
await context.ExecuteQueryAsync();
Console.WriteLine($"Title: {context.Web.Title}");
}
}
AuthenticationManager 示例类
注意
使用在 Azure AD 中注册的应用程序的应用程序 ID 更新 defaultAADAppId
注意
如果将 CSOM for .NET Standard 与 Azure Functions v3 一起使用,则可能会遇到与 System.IdentityModel.Tokens.Jwt 相关的运行时错误。 可通过遵循此解决方法解决此问题。
using Microsoft.SharePoint.Client;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Security;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace CSOMDemo
{
public class AuthenticationManager: IDisposable
{
private static readonly HttpClient httpClient = new HttpClient();
private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
private const string defaultAADAppId = "986002f6-c3f6-43ab-913e-78cca185c392";
// Token cache handling
private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);
private AutoResetEvent tokenResetEvent = null;
private readonly ConcurrentDictionary<string, string> tokenCache = new ConcurrentDictionary<string, string>();
private bool disposedValue;
internal class TokenWaitInfo
{
public RegisteredWaitHandle Handle = null;
}
public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)
{
var context = new ClientContext(web);
context.ExecutingWebRequest += (sender, e) =>
{
string accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password).GetAwaiter().GetResult();
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
return context;
}
public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)
{
string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);
if (accessTokenFromCache == null)
{
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
try
{
// No async methods are allowed in a lock section
string accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword).ConfigureAwait(false);
Console.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");
AddTokenToCache(resourceUri, tokenCache, accessToken);
// Register a thread to invalidate the access token once's it's expired
tokenResetEvent = new AutoResetEvent(false);
TokenWaitInfo wi = new TokenWaitInfo();
wi.Handle = ThreadPool.RegisterWaitForSingleObject(
tokenResetEvent,
async (state, timedOut) =>
{
if (!timedOut)
{
TokenWaitInfo internalWaitToken = (TokenWaitInfo)state;
if (internalWaitToken.Handle != null)
{
internalWaitToken.Handle.Unregister(null);
}
}
else
{
try
{
// Take a lock to ensure no other threads are updating the SharePoint Access token at this time
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
RemoveTokenFromCache(resourceUri, tokenCache);
Console.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");
}
catch (Exception ex)
{
Console.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");
RemoveTokenFromCache(resourceUri, tokenCache);
}
finally
{
semaphoreSlimTokens.Release();
}
}
},
wi,
(uint)CalculateThreadSleep(accessToken).TotalMilliseconds,
true
);
return accessToken;
}
finally
{
semaphoreSlimTokens.Release();
}
}
else
{
Console.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and user {userPrincipalName}");
return accessTokenFromCache;
}
}
private async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)
{
string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";
var clientId = defaultAADAppId;
var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";
using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))
{
var result = await httpClient.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>
{
return response.Result.Content.ReadAsStringAsync().Result;
}).ConfigureAwait(false);
var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);
var token = tokenResult.GetProperty("access_token").GetString();
return token;
}
}
private static string TokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
{
if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))
{
return accessToken;
}
return null;
}
private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)
{
if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))
{
tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);
}
else
{
tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);
}
}
private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
{
tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);
}
private static TimeSpan CalculateThreadSleep(string accessToken)
{
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);
var lease = GetAccessTokenLease(token.ValidTo);
lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);
return lease;
}
private static TimeSpan GetAccessTokenLease(DateTime expiresOn)
{
DateTime now = DateTime.UtcNow;
DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);
TimeSpan lease = expires - now;
return lease;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (tokenResetEvent != null)
{
tokenResetEvent.Set();
tokenResetEvent.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}