同时
this answer
技术上是正确的,这不是对我最初问题的直接回答。
我正在寻找一种方法来向业务用户提供任何BLB的直接URI,因此他们可以在任何Web浏览器中简单地打开它,并查看该文件。
在我的例子中,我们希望允许用户通过我们的支持bot(构建在Microsoft bot框架上)访问已上载到blob存储的文件。例如,将附件作为支持系统中的链接,供支持代理访问。
在深入研究这个问题之后,我可以回答我自己的问题:
随着azure storage对基于azure active directory的访问控制的支持的宣布,是否可以仅通过其uri在web浏览器上提供blob(特定文件)?
不
,这是不可能的。更具体地说,仅仅在浏览器中打开指向blob的直接uri并不会触发oauth流。相反,它总是会给你
ResourceNotFound
响应,除非您提供SAS查询令牌或将blob设置为public。从安全性的角度来看,这两种解决方案都是不好的(当涉及到普通用户时),而且显然是不好的用户体验。
解决方案
为了找到一种方法来准确地存档我想要的内容,我想出了一个azure函数的想法,通过传递
fileName
使用路由模板构造路径的uri。
考虑到安全性和访问令牌的需要,您可以通过平台身份验证(也称为easyauth)来保护Function应用程序。
然而
,这还不够,而且配置解决方案的所有部分也不是一帆风顺的。这就是我分享的原因。
tl;dr高级步骤:
-
创建新的功能应用程序(建议使用v2)
-
为启用功能应用程序
authentication
(伊斯亚特)
-
为功能应用程序创建服务主体(也称为应用程序注册)(步骤2隐含)
-
添加其他允许的令牌访问群体
https://storage.microsoft.com
应用程序注册
-
编辑应用注册的清单以包含azure存储api权限(请参阅下面的特别说明)
-
修改azure资源管理器中的authsettings以包括
additionalLoginParams
对于令牌响应和resourceid
-
至少给
Storage Blob Data Reader
对访问文件的所有用户的blob权限
-
部署您的函数应用程序,调用它,访问用户令牌,调用blob存储并将结果呈现给用户(请参阅下面的代码示例)
有关azure存储api权限和访问令牌的说明(步骤5&6)
如最新声明
documentation
对于azure存储上的aad身份验证支持,应用程序必须
user_impersonation
resourceid的权限范围
https://storage.azure.com/
. 不幸的是,文档没有说明如何设置这个api权限,因为它在门户中不可见(至少我没有找到它)。
因此,唯一的方法是通过在Azure门户中直接编辑应用程序注册清单来设置它的全局GUID(可以在Internet上找到)。
更新
:
事实证明,在门户中找不到正确的权限是一个错误。看我的答案
here
. 手动修改清单也会得到同样的结果,但是直接在门户中进行修改要方便得多。
"requiredResourceAccess": [
{
"resourceAppId": "e406a681-f3d4-42a8-90b6-c2b029497af1",
"resourceAccess": [
{
"id": "03e0da56-190b-40ad-a80c-ea378c433f7f",
"type": "Scope"
}
]
},
{
"resourceAppId": "00000002-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6",
"type": "Scope"
}
]
}
]
第一个是
用户模拟
azure存储上的作用域,第二个是
User.Read
,这在大多数情况下是有帮助或需要的。
在您上传了修改后的清单之后,您可以在
API权限
你的应用注册标签。
由于easyauth正在使用aad的v1端点,因此您的应用程序需要通过传递
resource=https://storage.azure.com/
当触发oauth流时。
此外,azure存储需要认证头的承载模式,因此需要jwt令牌。要从端点获取jwt令牌,我们需要传递
response_type=code id_token
作为附加登录参数。
两者只能通过
Azure Resource explorer
或者动力地狱。
使用azure资源管理器,您必须一直导航到功能应用程序上的authsettings并设置
附加登录参数
因此。
"additionalLoginParams": [
"response_type=code id_token",
"resource=https://storage.azure.com/"
]
代码样本
下面是一个使用上述所有机制的简单azure函数的完整代码示例。
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
namespace Controller.Api.v1.Org
{
public static class GetAttachment
{
private const string defaultContentType = "application/octet-stream";
[FunctionName("GetAttachment")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "v1/attachments")] HttpRequest req,
ILogger log)
{
if (!req.Query.ContainsKey("fileName"))
return new BadRequestResult();
// Set the file name from query parameter
string fileName = req.Query["fileName"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
fileName = fileName ?? data?.name;
// Construct the final uri. In this sample we have a applicaiton setting BLOB_URL
// set on the function app to store the target blob
var blobUri = Environment.GetEnvironmentVariable("BLOB_URL") + $"/{fileName}";
// The access token is provided as this special header by easyAuth.
var accessToken = req.Headers.FirstOrDefault(p => p.Key.Equals("x-ms-token-aad-access-token", StringComparison.OrdinalIgnoreCase));
// Construct the call against azure storage and pass the user token we got from easyAuth as bearer
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value.FirstOrDefault());
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate");
client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("x-ms-version", "2017-11-09");
// Serve the response directly in users browser. This code works against any browser, e.g. chrome, edge or even internet explorer
var response = await client.GetAsync(blobUri);
var contentType = response.Content.Headers.FirstOrDefault(p => p.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
var byteArray = await response.Content.ReadAsByteArrayAsync();
var result = new FileContentResult(byteArray, contentType.Value.Any() ? contentType.Value.First() : defaultContentType);
return result;
}
}
}
}