使用 Microsoft Graph 将 C# Asp.Net Core 中的文件上传到 Sharepoint/OneDrive,无需用户交互
Upload a file in C# Asp.Net Core to Sharepoint/OneDrive using Microsoft Graph without user interaction
当我尝试使用 Microsoft Graph API 通过守护程序应用程序将文件上传到我的 OneDrive 时,我收到错误 400 Bad Request。我使用 HttpClient,而不是 GraphServiceClient,因为后者假定交互并与 DelegatedAuthenticationProvider(?) 一起工作。
- 该应用已在 AAD 中注册并具有正确的应用程序权限(Microsoft 图形/文件 ReadWrite.All)
- 注册是针对一个租户的,没有重定向url(按照说明)
主要方法 Upload
通过 Helper AuthenticationConfig
获取 AccessToken 并使用 Helper ProtectedApiCallHelper
.
将文件放入 OneDrive/SharePoint
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file)
{
var toegang = new AuthenticationConfig();
var token = toegang.GetAccessTokenAsync().GetAwaiter().GetResult();
var httpClient = new HttpClient();
string bestandsnaam = file.FileName;
var serviceEndPoint = "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/";
var wurl = serviceEndPoint + bestandsnaam + "/content";
// The variable wurl looks as follows: "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content"
var apicaller = new ProtectedApiCallHelper(httpClient);
apicaller.PostWebApi(wurl, token.AccessToken, file).GetAwaiter();
return View();
}
我使用以下标准助手获得了正确的访问令牌 AuthenticationConfig.GetAccessToken()
public async Task<AuthenticationResult> GetAccessTokenAsync()
{
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
return result;
}
catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
{
...
return result;
}
}
使用 AccessToken、Graph-Url 和要上传的文件(作为 IFormFile),Helper ProtectedApiCallHelper.PostWebApi
被调用
public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
{
Stream stream = fileToUpload.OpenReadStream();
var x = stream.Length;
HttpContent content = new StreamContent(stream);
if (!string.IsNullOrEmpty(accessToken))
{
var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;
HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
// Here the 400 Bad Request happens
HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);
if (response.IsSuccessStatusCode)
{
return;
}
else
{
//error handling
return;
}
}
}
编辑
请参阅下面的工作解决方案。
您可以使用 GraphServiceClient,而无需使用客户端 ID 和客户端密码进行用户交互。首先,创建一个名为 GraphAuthProvider 的 class:
public class GraphAuthProvider
{
public async Task<GraphServiceClient> AuthenticateViaAppIdAndSecret(
string tenantId,
string clientId,
string clientSecret)
{
var scopes = new string[] { "https://graph.microsoft.com/.default" };
// Configure the MSAL client as a confidential client
var confidentialClient = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}/v2.0")
.WithClientSecret(clientSecret)
.Build();
// Build the Microsoft Graph client. As the authentication provider, set an async lambda
// which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
// and inserts this access token in the Authorization header of each API request.
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClient
.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
return graphServiceClient;
}
}
然后您可以创建经过身份验证的 GraphServiceClient 并使用它们上传文件,例如上传到 SharePoint:
GraphServiceClient _graphServiceClient = await _graphAuthProvider.AuthenticateViaAppIdAndSecret(
tenantId,
clientId,
appSecret);
using (Stream fileStream = new FileStream(
fileLocation,
FileMode.Open,
FileAccess.Read))
{
resultDriveItem = await _graphServiceClient.Sites[sites[0]]
.Drives[driveId].Root.ItemWithPath(fileName).Content.Request().PutAsync<DriveItem>(fileStream);
}
关于权限:您可能需要比 Files.ReadWrite.All 更多的权限。据我所知,应用程序需要应用程序权限 Sites.ReadWrite.All 才能将文档上传到 SharePoint。
根据文档:Upload or replace the contents of a DriveItem
如果使用客户端凭证流(没有用户的 M2M 流),您应该使用以下请求:
PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content
而不是:
https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content
这是使用 GraphServiceClient 的最终工作示例
public async Task<DriveItem> UploadSmallFile(IFormFile file, bool uploadToSharePoint)
{
IFormFile fileToUpload = file;
Stream ms = new MemoryStream();
using (ms = new MemoryStream()) //this keeps the stream open
{
await fileToUpload.CopyToAsync(ms);
ms.Seek(0, SeekOrigin.Begin);
var buf2 = new byte[ms.Length];
ms.Read(buf2, 0, buf2.Length);
ms.Position = 0; // Very important!! to set the position at the beginning of the stream
GraphServiceClient _graphServiceClient = await AuthenticateViaAppIdAndSecret();
DriveItem uploadedFile = null;
if (uploadToSharePoint == true)
{
uploadedFile = (_graphServiceClient
.Sites["root"]
.Drives["{DriveId}"]
.Items["{Id_of_Targetfolder}"]
.ItemWithPath(fileToUpload.FileName)
.Content.Request()
.PutAsync<DriveItem>(ms)).Result;
}
else
{
// Upload to OneDrive (for Business)
uploadedFile = await _graphServiceClient
.Users["{Your_EmailAdress}"]
.Drive
.Root
.ItemWithPath(fileToUpload.FileName)
.Content.Request()
.PutAsync<DriveItem>(ms);
}
ms.Dispose(); //clears memory
return uploadedFile; //returns a DriveItem.
}
}
您也可以使用 HttpClient
public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
{
//Create a Stream and convert it to a required HttpContent-stream (StreamContent).
// Important is the using{...}. This keeps the stream open until processed
using (MemoryStream data = new MemoryStream())
{
await fileToUpload.CopyToAsync(data);
data.Seek(0, SeekOrigin.Begin);
var buf = new byte[data.Length];
data.Read(buf, 0, buf.Length);
data.Position = 0;
HttpContent content = new StreamContent(data);
if (!string.IsNullOrEmpty(accessToken))
{
// NO Headers other than the AccessToken should be added. If you do
// an Error 406 is returned (cannot process). So, no Content-Types, no Conentent-Dispositions
var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);
if (response.IsSuccessStatusCode)
{
return;
}
else
{
// do something else
return;
}
}
content.Dispose();
data.Dispose();
} //einde using memorystream
}
}
当我尝试使用 Microsoft Graph API 通过守护程序应用程序将文件上传到我的 OneDrive 时,我收到错误 400 Bad Request。我使用 HttpClient,而不是 GraphServiceClient,因为后者假定交互并与 DelegatedAuthenticationProvider(?) 一起工作。
- 该应用已在 AAD 中注册并具有正确的应用程序权限(Microsoft 图形/文件 ReadWrite.All)
- 注册是针对一个租户的,没有重定向url(按照说明)
主要方法 Upload
通过 Helper AuthenticationConfig
获取 AccessToken 并使用 Helper ProtectedApiCallHelper
.
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file)
{
var toegang = new AuthenticationConfig();
var token = toegang.GetAccessTokenAsync().GetAwaiter().GetResult();
var httpClient = new HttpClient();
string bestandsnaam = file.FileName;
var serviceEndPoint = "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/";
var wurl = serviceEndPoint + bestandsnaam + "/content";
// The variable wurl looks as follows: "https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content"
var apicaller = new ProtectedApiCallHelper(httpClient);
apicaller.PostWebApi(wurl, token.AccessToken, file).GetAwaiter();
return View();
}
我使用以下标准助手获得了正确的访问令牌 AuthenticationConfig.GetAccessToken()
public async Task<AuthenticationResult> GetAccessTokenAsync()
{
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
return result;
}
catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
{
...
return result;
}
}
使用 AccessToken、Graph-Url 和要上传的文件(作为 IFormFile),Helper ProtectedApiCallHelper.PostWebApi
被调用
public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
{
Stream stream = fileToUpload.OpenReadStream();
var x = stream.Length;
HttpContent content = new StreamContent(stream);
if (!string.IsNullOrEmpty(accessToken))
{
var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;
HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
// Here the 400 Bad Request happens
HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);
if (response.IsSuccessStatusCode)
{
return;
}
else
{
//error handling
return;
}
}
}
编辑
请参阅下面的工作解决方案。
您可以使用 GraphServiceClient,而无需使用客户端 ID 和客户端密码进行用户交互。首先,创建一个名为 GraphAuthProvider 的 class:
public class GraphAuthProvider
{
public async Task<GraphServiceClient> AuthenticateViaAppIdAndSecret(
string tenantId,
string clientId,
string clientSecret)
{
var scopes = new string[] { "https://graph.microsoft.com/.default" };
// Configure the MSAL client as a confidential client
var confidentialClient = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}/v2.0")
.WithClientSecret(clientSecret)
.Build();
// Build the Microsoft Graph client. As the authentication provider, set an async lambda
// which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
// and inserts this access token in the Authorization header of each API request.
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClient
.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
return graphServiceClient;
}
}
然后您可以创建经过身份验证的 GraphServiceClient 并使用它们上传文件,例如上传到 SharePoint:
GraphServiceClient _graphServiceClient = await _graphAuthProvider.AuthenticateViaAppIdAndSecret(
tenantId,
clientId,
appSecret);
using (Stream fileStream = new FileStream(
fileLocation,
FileMode.Open,
FileAccess.Read))
{
resultDriveItem = await _graphServiceClient.Sites[sites[0]]
.Drives[driveId].Root.ItemWithPath(fileName).Content.Request().PutAsync<DriveItem>(fileStream);
}
关于权限:您可能需要比 Files.ReadWrite.All 更多的权限。据我所知,应用程序需要应用程序权限 Sites.ReadWrite.All 才能将文档上传到 SharePoint。
根据文档:Upload or replace the contents of a DriveItem
如果使用客户端凭证流(没有用户的 M2M 流),您应该使用以下请求:
PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content
而不是:
https://graph.microsoft.com/v1.0/drive/items/{Id_Of_Specific_Folder}/proefdocument.txt/content
这是使用 GraphServiceClient 的最终工作示例
public async Task<DriveItem> UploadSmallFile(IFormFile file, bool uploadToSharePoint)
{
IFormFile fileToUpload = file;
Stream ms = new MemoryStream();
using (ms = new MemoryStream()) //this keeps the stream open
{
await fileToUpload.CopyToAsync(ms);
ms.Seek(0, SeekOrigin.Begin);
var buf2 = new byte[ms.Length];
ms.Read(buf2, 0, buf2.Length);
ms.Position = 0; // Very important!! to set the position at the beginning of the stream
GraphServiceClient _graphServiceClient = await AuthenticateViaAppIdAndSecret();
DriveItem uploadedFile = null;
if (uploadToSharePoint == true)
{
uploadedFile = (_graphServiceClient
.Sites["root"]
.Drives["{DriveId}"]
.Items["{Id_of_Targetfolder}"]
.ItemWithPath(fileToUpload.FileName)
.Content.Request()
.PutAsync<DriveItem>(ms)).Result;
}
else
{
// Upload to OneDrive (for Business)
uploadedFile = await _graphServiceClient
.Users["{Your_EmailAdress}"]
.Drive
.Root
.ItemWithPath(fileToUpload.FileName)
.Content.Request()
.PutAsync<DriveItem>(ms);
}
ms.Dispose(); //clears memory
return uploadedFile; //returns a DriveItem.
}
}
您也可以使用 HttpClient
public async Task PostWebApi(string webApiUrl, string accessToken, IFormFile fileToUpload)
{
//Create a Stream and convert it to a required HttpContent-stream (StreamContent).
// Important is the using{...}. This keeps the stream open until processed
using (MemoryStream data = new MemoryStream())
{
await fileToUpload.CopyToAsync(data);
data.Seek(0, SeekOrigin.Begin);
var buf = new byte[data.Length];
data.Read(buf, 0, buf.Length);
data.Position = 0;
HttpContent content = new StreamContent(data);
if (!string.IsNullOrEmpty(accessToken))
{
// NO Headers other than the AccessToken should be added. If you do
// an Error 406 is returned (cannot process). So, no Content-Types, no Conentent-Dispositions
var defaultRequestHeaders = HttpClient.DefaultRequestHeaders;
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
HttpResponseMessage response = await HttpClient.PutAsync(webApiUrl, content);
if (response.IsSuccessStatusCode)
{
return;
}
else
{
// do something else
return;
}
}
content.Dispose();
data.Dispose();
} //einde using memorystream
}
}