下载 SharePoint Office 365 的版本文件



我尝试使用 c# 下载以前版本的 SharePoint 文件。我用这篇文章作为参考。该链接是带有镶边的工作文件。现在,当我尝试在 c# 上使用 URL 逐部分下载文件时,它给了我远程服务器返回错误:(401( 未经授权。 错误。

我什至提供了使用该函数标头的访问令牌。

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
WebHeaderCollection header = new WebHeaderCollection();
request.Headers.Add(System.Net.HttpRequestHeader.Authorization, $"Bearer {token}");
request.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");

这里 URI 类似于地址:http://yoursite/yoursubsite/_vti_history/512/Documents/Book1.xlsx

如何使用 c# 下载以前的版本文件?

这是我的测试代码供您参考。

var login = "user@xxx.onmicrosoft.com";
var password = "Password";
var securePassword = new SecureString();
foreach (char c in password)
{
securePassword.AppendChar(c);
}
SharePointOnlineCredentials onlineCredentials = new SharePointOnlineCredentials(login, securePassword);
string webUrl = "https://xxx.sharepoint.com/sites/lee";
string requestUrl = "https://xxx.sharepoint.com/sites/lee/_vti_history/512/MyDoc2/testdata.xlsx";
Uri uri = new Uri(requestUrl);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
request.Method = "GET";
request.Credentials = onlineCredentials;
request.Headers[HttpRequestHeader.Cookie] = onlineCredentials.GetAuthenticationCookie(new Uri(webUrl), true);  // SPO requires cookie authentication
request.Headers["X-FORMS_BASED_AUTH_ACCEPTED"] = "f";  // disable interactive forms-based auth            
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream stream = response.GetResponseStream();

下面是使用客户端 ID 和客户端密钥从 SharePoint 网站下载文件的完整示例。

遇到的主要问题:

  1. 获取实际接受的访问令牌。获取访问令牌非常容易,但是尝试使用它来下载文件会导致带有消息invalid_client的401。

这些是不起作用的身份验证URL。尝试x的值既是租户 ID(例如 guid(又是租户域(例如 company.com(。将返回访问令牌,但返回的资源始终用于Microsoft图形 (00000003-0000-0000-c000-000000000000(。

String authUrl = "https://login.microsoftonline.com/" + x + "/oauth2/token";
String authUrl = "https://login.microsoftonline.com/" + x + "/oauth2/v2.0/token";
String authUrl = "https://login.windows.net/" + x + "/oauth2/token";
String authUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";

实际有效的 authUrl 是:

String authUrl = "https://accounts.accesscontrol.windows.net/" + tenantId + "/tokens/OAuth/2";
  1. 第二个问题是使用直接URL下载文件, 例如https://tenant.sharepoint.com/Shared Documents/Data.xlsx

显然直接网址不起作用。理论上,内部服务器重定向从请求标头中去除访问令牌。因此,必须使用以下任一方法下载文件:

a(https://tenant.sharepoint.com/_api/web/getfilebyserverrelativeurl('/Shared Documents/FileName.xlsx')/$value

最后的/$value说下载实际的二进制数据。如果省略,则下载一个XML文件,其中包含有关该文件的属性(created datemodified datelength等(。

b(https://tenant.sharepoint.com/_layouts/15/download.aspx?SourceUrl=/Shared Documents/FileName.xlsx

注意:如果使用选项 a(,则"文件名"中的任何单引号都必须替换为连续两个单引号。可以在此处阅读有关转义问题的更多信息:https://sharepoint.stackexchange.com/questions/154590/getfilebyserverrelativeurl-fails-when-the-filename-contains-a-quote

  1. 忘记设置:System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

这会导致出现类似连接被强制关闭的消息。

  1. 确保-DisableCustomAppAuthenticationfalse。这可以通过PowerShell进行设置。

    PS> install-module -name "PnP.PowerShell"

    PS> Connect-PnPOnline -Url "https://tenant.sharepoint.com"

    PS> Set-SPOTenant -DisableCustomAppAuthentication $false

注意:我看到很多设置了标题的代码,例如:

req.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");
req.Headers.Add("Accept", "application/json;odata=verbose");
req.Headers.Add("cache-control", "no-cache");
req.Headers.Add("Use-Agent", "Other");

这些标头都不是 ClientId/ClientSecret 身份验证所必需的。但是,使用旧版SharePointOnlineCredentials时需要"X-FORMS_BASED_AUTH_ACCEPTED"

有用的链接:

  • https://www.sharepointdiary.com/2019/03/connect-pnponline-with-appid-and-appsecret.html
  • https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs

下面的代码提供了几种不同的方法来获取访问令牌和下载文件。

using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web;
namespace SharePointDemo {
public class Program {
const String SharePointPrincipal = "00000003-0000-0ff1-ce00-000000000000";
public static void Main(String[] args) {
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; // required.
// Replace these 6 values:
Uri url = new Uri("https://tenant.sharepoint.com"); // with or without an ending '/' both work
String clientId = "... guid ...";
String clientSecret = "... generated using the SharePoint AppRegNew.aspx ..."; // see useful links above
String tenantId = "... guid ..."; // aka realm. Can be found in the SharePoint admin site settings, or by using GetTenantId(url);
String fileRelativeUrl = "/Shared Documents/Data.xlsx";
String filename = @"C:tempData.xlsx"; // local file name

String access_token_key = "access_token"; // variable name in headers to look for in authUrl's response.
Uri fileUrl = new Uri(url.ToString() + "_layouts/15/download.aspx?SourceUrl=" + fileRelativeUrl);
//Uri fileUrl = new Uri(url.ToString() + "_api/web/getfilebyserverrelativeurl('" + fileRelativeUrl.Replace("'", "''") + "')/$value"); // also works
String clientIdPrincipal = clientId + "@" + tenantId;
String resource = SharePointPrincipal + "/" + url.Host + "@" + tenantId;
// Note: a 'scope' parameter is not required for this authUrl, but 'resource' is required.
String authUrl = "https://accounts.accesscontrol.windows.net/" + tenantId + "/tokens/OAuth/2";
String content = "grant_type=client_credentials&client_id=<username>&client_secret=<password>&resource=<resource>";
content = content.Replace("<username>", clientIdPrincipal);
content = content.Replace("<password>", clientSecret);
content = content.Replace("<resource>", resource);
AuthResult result = GetAuthResult(authUrl, content, access_token_key);
String accessToken = result.access_token;
DownloadFile1(fileUrl, accessToken, filename); // pick whichever DownloadFile method floats your boat
}
private static AuthResult GetAuthResult(String authUrl, String content, String access_token_key = "access_token", int timeoutSeconds = 10) {
HttpContent data = new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded");
using (data) {
using (HttpClient c = new HttpClient()) {
c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
using (HttpResponseMessage res = c.PostAsync(authUrl, data).Result) {
HttpStatusCode code = res.StatusCode;
String message = res.Content.ReadAsStringAsync().Result;
if (code == HttpStatusCode.OK)
return ParseMessage(message, access_token_key);
throw new Exception("Auth failed. Status code: " + code + " Message: " + message);
}
}
}
}
// alternative way using HttpWebRequest
private static AuthResult GetAuthResult2(String authUrl, String content, String access_token_key = "access_token", int timeoutSeconds = 10) {
HttpWebRequest req = WebRequest.CreateHttp(authUrl);
req.AuthenticationLevel = System.Net.Security.AuthenticationLevel.None;
req.ContentLength = content.Length;
req.ContentType = "application/x-www-form-urlencoded";
req.Method = "POST";
req.Timeout = timeoutSeconds * 1000;
using (StreamWriter sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII)) {
sw.Write(content);
sw.Close();
}
using (WebResponse res = req.GetResponse()) {
using (StreamReader sr = new StreamReader(res.GetResponseStream(), Encoding.ASCII)) {
String message = sr.ReadToEnd();
return ParseMessage(message, access_token_key);
}
}
}
// this could also be done using a Json library
private static AuthResult ParseMessage(String message, String access_token_key) {
Hashtable ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
char[] trimChars = new [] { '{', '"', '}' };
String[] arr = message.Split(',');
for (int i = 0; i < arr.Length; i++) {
String t = arr[i];
int x = t.IndexOf(':');
if (x < 0)
continue;
String varName = t.Substring(0, x).Trim(trimChars);
String value = t.Substring(x + 1).Trim(trimChars);
ht[varName] = value;
}
String accessToken = (String) ht[access_token_key];
if (accessToken == null)
throw new Exception(String.Format("Could not find '{0}' in response message: ", access_token_key) + message);
AuthResult result = new AuthResult();
result.access_token = accessToken;
result.resource = (String) ht["resource"];
result.token_type = (String) ht["token_type"];
int val = 0;
if (int.TryParse((String) ht["expires_in"], out val)) result.expires_in = val;
if (int.TryParse((String) ht["expires_on"], out val))
result.expires_on = val;
else
result.expires_on = (int) (DateTime.UtcNow.AddSeconds(result.expires_in) - AuthResult.EPOCH).TotalSeconds;
if (int.TryParse((String) ht["not_before"], out val)) result.not_before = val;
return result;
}
private class AuthResult {
public static readonly DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public DateTime NotBefore { get { return EPOCH.AddSeconds(not_before).ToLocalTime(); } }
public DateTime ExpiresOn { get { return EPOCH.AddSeconds(expires_on).ToLocalTime(); } }
public int not_before { get; set; }
public int expires_on { get; set; }
public String resource { get; set; }
///<summary>Indicates the token type value. The only type that the Microsoft identity platform supports is bearer.</summary>
public String token_type { get; set; }
///<summary>The amount of time that an access token is valid (in seconds).</summary>
public int expires_in { get; set; }
///<summary>The access token generated by the authentication server.</summary>
public String access_token { get; set; }
}
public static void DownloadFile1(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
using (var c = new HttpClient()) { // requires reference to System.Net.Http
c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
var req = new HttpRequestMessage();
req.Headers.Add("Authorization", "Bearer " + accessToken);
req.Method = HttpMethod.Get;
req.RequestUri = fileUrl;
using (HttpResponseMessage res = c.SendAsync(req).Result) {
if (res.StatusCode == HttpStatusCode.OK) {
using (Stream s = res.Content.ReadAsStreamAsync().Result) {
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
s.CopyTo(fs);
fs.Flush();
}
}
}
else {
String message = "Error. Server returned status code: " + res.StatusCode + " (" + (int) res.StatusCode + "). Headers: " + res.Headers.ToString();
throw new Exception(message);
}
}
}
}
// slight variation to DownloadFile1, but basically the same
public static void DownloadFile2(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
using (var c = new HttpClient()) {
c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
c.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
using (HttpResponseMessage res = c.GetAsync(fileUrl).Result) {
if (res.StatusCode == HttpStatusCode.OK) {
using (Stream s = res.Content.ReadAsStreamAsync().Result) {
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
s.CopyTo(fs);
fs.Flush();
}
}
}
else {
String message = "Error. Server returned status code: " + res.StatusCode + " (" + (int) res.StatusCode + "). Headers: " + res.Headers.ToString();
throw new Exception(message);
}
}
}
}
public static void DownloadFile3(Uri fileUrl, String accessToken, String filename, int timeoutSeconds = 60) {
HttpWebRequest req = WebRequest.CreateHttp(fileUrl);
//req.ContinueTimeout = ...;
//req.ReadWriteTimeout = ...;
req.Timeout = timeoutSeconds * 1000;
req.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + accessToken);
req.Method = "GET";
try {
using (var res = (HttpWebResponse) req.GetResponse()) {
using (Stream s = res.GetResponseStream()) {
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
s.CopyTo(fs);
fs.Flush();
}
}
}
} catch (Exception ex) {
if (ex is WebException) {
var we = (WebException) ex;
String headers = we.Response.Headers.ToString();
throw new WebException(we.Message + " headers: " + headers, we);
}
throw;
}
}
public static void DownloadFile4(Uri fileUrl, String accessToken, String filename) {
using (WebClient c = new WebClient()) {
c.Headers.Add("Authorization", "Bearer " + accessToken);
using (Stream s = c.OpenRead(fileUrl)) {
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
s.CopyTo(fs);
fs.Flush();
}
}
}
}
// Helper methods, not used:
///<summary>
///Makes an http request to the site url in order to read the GUID tenant-id (also called the realm) from the response headers.
///</summary>
public static Guid? GetTenantId(Uri siteUrl, int timeoutSeconds = 10) {
// the code: url = url.TrimEnd('/') + "/_vti_bin/client.svc"; is not needed
String url = siteUrl.GetLeftPart(UriPartial.Authority);
// use HttpClient to avoid Exception when using HttpWebRequest
using (var c = new HttpClient()) { // requires reference to System.Net.Http
c.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
var req = new HttpRequestMessage();
// "Bearer " without an access token results in StatusCode = Unauthorized (401) and the response headers contain the tenant-id
req.Headers.Add("Authorization", "Bearer ");
req.Method = HttpMethod.Get;
req.RequestUri = new Uri(url);
using (HttpResponseMessage res = c.SendAsync(req).Result) {
// HttpStatusCode code = res.StatusCode; // typically Unauthorized
System.Net.Http.Headers.HttpResponseHeaders h = res.Headers;
foreach (String s in h.GetValues("WWW-Authenticate")) { // should only have one
Guid? g = TryGetGuid(s);
if (g.HasValue)
return g;
}
}
}
return null;
}
public static Guid? GetTenantId_old(Uri siteUrl, int timeoutSeconds = 10) {
String url = siteUrl.GetLeftPart(UriPartial.Authority);
HttpWebRequest req = WebRequest.CreateHttp(url);
req.Timeout = timeoutSeconds * 1000;
req.Headers["Authorization"] = "Bearer ";
String header = null;
try {
using (req.GetResponse()) {}
} catch (WebException e) {
if (e.Response != null)
header = e.Response.Headers["WWW-Authenticate"];
}
return TryGetGuid(header);
}
private static Guid? TryGetGuid(String header) {
if (String.IsNullOrEmpty(header))
return null;
const String bearer = "Bearer realm="";
int bearerIndex = header.IndexOf(bearer, StringComparison.OrdinalIgnoreCase);
if (bearerIndex < 0)
return null;
int x1 = bearerIndex + bearer.Length;
int x2 = header.IndexOf('"', x1 + 1);
String realm = (x2 < 0 ? header.Substring(x1) : header.Substring(x1, x2 - x1));
Guid guid;
if (Guid.TryParse(realm, out guid))
return guid;
return null;
}
}
}

最新更新