基于角色/api_key生成Swashbuckle API文档

5
我正在使用Swashbuckle (5.3.2),它会生成一个漂亮的API文档。
为了澄清我的问题,我建立了一个没有实际意义的小例子项目。
API只能使用有效的API密钥。 为此,我引入了一个ApiKeyFilter来验证api_key并读取相应的角色。 ApiKeyFilter
public class ApiKeyFilter : IAuthenticationFilter
{
    private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
    private readonly string authenticationScheme = "Bearer";
    private readonly string queryStringApiKey = "api_key";

    public bool AllowMultiple
    {
        get { return false; }
    }

    public ApiKeyFilter()
    {
        if (allowedApps.Count == 0)
        {
            allowedApps.Add("PetLover_api_key", new []{"PetLover"});
            allowedApps.Add("CarOwner_api_key", new []{"CarOwner"});
            allowedApps.Add("Admin_api_key", new []{"PetLover","CarOwner"});
        }
    }

    public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        var req = context.Request;
        Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
        string rawAuthzHeader = null;
        if (queryStrings.ContainsKey(queryStringApiKey))
        {
            rawAuthzHeader = queryStrings[queryStringApiKey];
        }
        else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
        {
            rawAuthzHeader = req.Headers.Authorization.Parameter;
        }
        if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
        {
            var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
            context.Principal = currentPrincipal;
        }
        else
        {
            context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
        }

        return Task.FromResult(0);
    }

    public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
    {
        context.Result = new ResultWithChallenge(context.Result);
        return Task.FromResult(0);
    }


}

public class ResultWithChallenge : IHttpActionResult
{
    private readonly string authenticationScheme = "amx";
    private readonly IHttpActionResult next;

    public ResultWithChallenge(IHttpActionResult next)
    {
        this.next = next;
    }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = await next.ExecuteAsync(cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(authenticationScheme));
        }

        return response;
    }
}

控制器/资源只能被拥有相应角色的请求者访问。 PetController
[Authorize(Roles = "PetLover")]
[RoutePrefix("api/pets")]
public class PetController : ApiController
{
    // GET api/pet
    [Route]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/pet/5
    [Route("{id:int}")]
    public string Get(int id)
    {
        return "value";
    }

    // POST api/pet
    [Route]
    public void Post([FromBody]string value)
    {
    }

    // PUT api/pet/5
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/pet/5
    public void Delete(int id)
    {
    }
}

汽车控制器

[RoutePrefix("api/cars")]
public class CarController : ApiController
{
    // GET api/car
    [AllowAnonymous]
    [Route]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/car/5
    [Authorize(Roles = "CarOwner")]
    [Route("{id:int}")]
    public string Get(int id)
    {
        return "value";
    }

    // POST api/car
    [Authorize(Roles = "CarOwner")]
    [Route]
    public void Post([FromBody]string value)
    {
    }
}


WebApiConfig

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();

            config.Filters.Add(new ApiKeyFilter());

            //config.MessageHandlers.Add(new CustomAuthenticationMessageHandler());
        }
    }

到目前为止一切都很好,没有问题。 问题: 现在我希望在API生成过程中考虑“用户”角色。我只想在文档中显示用户可以使用此api_key消费的资源和操作。 输出应该类似于(/swagger/ui/index?api_key=XXX):
Admin_api_key:
  • 汽车
    • 获取 /api/cars
    • 发布 /api/cars
    • 获取 /api/cars/{id}
  • 宠物
    • 获取 /api/pets
    • 发布 /api/pets
    • 获取 /api/pets/{id}
CarOwner_api_key:
  • 汽车
    • 获取 /api/cars
    • 发布 /api/cars
    • 获取 /api/cars/{id}
PetLover_api_key:
  • 汽车
    • 获取 /api/cars
  • 宠物
    • 获取 /api/pets
    • 发布 /api/pets
    • 获取 /api/pets/{id}
invalid_api_key:
  • 无内容显示
我在 API 规范生成期间无法访问 HttpRequest,以读取任何查询字符串或标头信息。
我已经查看了 DelegatingHandler,但在任何 Swashbuckle 过滤器(OperationFilter、DocumentFilter)中都无法读取 Principal,并且我也无法在 CustomProvider 中读取 Principal。
public class CustomAuthenticationMessageHandler : DelegatingHandler
    {
        private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
        private readonly string authenticationScheme = "Bearer";
        private readonly string queryStringApiKey = "api_key";

        public bool AllowMultiple
        {
            get { return false; }
        }

        public CustomAuthenticationMessageHandler()
        {
            if (allowedApps.Count == 0)
            {
                allowedApps.Add("PetLover_api_key", new[] {"PetLover"});
                allowedApps.Add("CarOwner_api_key", new[] {"CarOwner"});
                allowedApps.Add("Admin_api_key", new[] {"PetLover", "CarOwner"});
            }
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var req = request;
            Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
            string rawAuthzHeader = null;
            if (queryStrings.ContainsKey(queryStringApiKey))
            {
                rawAuthzHeader = queryStrings[queryStringApiKey];
            }
            else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
            {
                rawAuthzHeader = req.Headers.Authorization.Parameter;
            }
            if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
            {
                var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
                request.GetRequestContext().Principal = currentPrincipal;
            }
            else
            {

            }

            return await base.SendAsync(request, cancellationToken);
        }
    }

I found some similar question/issues but no real answer. 使用swagger编写Web API文档 使用Swashbuckle和ASP.NET Identity限制访问某些API控制器的Swagger {hxxps://}github.com/domaindrivendev/Swashbuckle/issues/334 {hxxps://}github.com/domaindrivendev/Swashbuckle/issues/735 {hxxps://}github.com/domaindrivendev/Swashbuckle/issues/478
1个回答

6

我现在找到了一个适合我的解决方案。

我使用了与我的问题中相同的CustomAuthenticationMessageHandler,将规则放入HttpContext中。

public class CustomAuthenticationMessageHandler : DelegatingHandler
    {
        private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
        private readonly string authenticationScheme = "Bearer";
        private readonly string queryStringApiKey = "api_key";

        public bool AllowMultiple
        {
            get { return false; }
        }

        public CustomAuthenticationMessageHandler()
        {
            if (allowedApps.Count == 0)
            {
                allowedApps.Add("PetLover_api_key", new[] {"PetLover"});
                allowedApps.Add("CarOwner_api_key", new[] {"CarOwner"});
                allowedApps.Add("Admin_api_key", new[] {"PetLover", "CarOwner"});
            }
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var req = request;
            Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
            string rawAuthzHeader = null;
            if (queryStrings.ContainsKey(queryStringApiKey))
            {
                rawAuthzHeader = queryStrings[queryStringApiKey];
            }
            else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
            {
                rawAuthzHeader = req.Headers.Authorization.Parameter;
            }
            if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
            {
                var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
                request.GetRequestContext().Principal = currentPrincipal;
            }
            else
            {

            }

            return await base.SendAsync(request, cancellationToken);
        }
    }

我介绍了一个自定义的Swashbuckle IDocumentFilter,它从HttpContext中读取规则,并从SwaggerDocument中删除根据api_key不允许的操作和资源。

public class AuthorizeRoleFilter : IDocumentFilter
    {
        public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
        {
            IPrincipal user = HttpContext.Current.User;

            foreach (ApiDescription apiDescription in apiExplorer.ApiDescriptions)
            {
                var authorizeAttributes = apiDescription
                    .ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>().ToList();
                authorizeAttributes.AddRange(apiDescription
                    .ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeAttribute>());

                if (!authorizeAttributes.Any())
                    continue;

                var roles =
                    authorizeAttributes
                        .SelectMany(attr => attr.Roles.Split(','))
                        .Distinct()
                        .ToList();
                if (!user.Identity.IsAuthenticated || !roles.Any(role => user.IsInRole(role) || role == ""))
                {
                    string key = "/" + apiDescription.RelativePath;
                    PathItem pathItem = swaggerDoc.paths[key];
                    switch (apiDescription.HttpMethod.Method.ToLower())
                    {
                        case "get":
                            pathItem.get = null;
                            break;
                        case "put":
                            pathItem.put = null;
                            break;
                        case "post":
                            pathItem.post = null;
                            break;
                        case "delete":
                            pathItem.delete = null;
                            break;
                        case "options":
                            pathItem.options = null;
                            break;
                        case "head":
                            pathItem.head = null;
                            break;
                        case "patch":
                            pathItem.patch = null;
                            break;
                    }
                    if (pathItem.get == null &&
                        pathItem.put == null &&
                        pathItem.post == null &&
                        pathItem.delete == null &&
                        pathItem.options == null &&
                        pathItem.head == null &&
                        pathItem.patch == null)
                    {
                        swaggerDoc.paths.Remove(key);
                    }
                }
            }

            swaggerDoc.paths = swaggerDoc.paths.Count == 0 ? null : swaggerDoc.paths;
            swaggerDoc.definitions = swaggerDoc.paths == null ? null : swaggerDoc.definitions;
        }
    }

我的WebApiConfig现在看起来是这样的。我删除了我的ApiKeyFilter因为它不再需要。
WebApiConfig
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        //config.Filters.Add(new ApiKeyFilter());

        config.MessageHandlers.Add(new CustomAuthenticationMessageHandler());
    }
}

SwaggerConfig

public class SwaggerConfig
{
    public static void Register()
    {
        GlobalConfiguration.Configuration
            .EnableSwagger(c =>
            {
                c.SingleApiVersion("v1", "SwashbuckleExample");
                c.DocumentFilter<AuthorizeRoleFilter>();
            })
            .EnableSwaggerUi(c => { });
    }
}



附加内容

在我的项目中,我使用了一个自定义提供程序(CustomProvider),它可以缓存每个api_key的SwaggerDocument

 public class CachingSwaggerProvider : ISwaggerProvider
    {
        private static ConcurrentDictionary<string, SwaggerDocument> _cache =
            new ConcurrentDictionary<string, SwaggerDocument>();

        private readonly ISwaggerProvider _swaggerProvider;

        public CachingSwaggerProvider(ISwaggerProvider swaggerProvider)
        {
            _swaggerProvider = swaggerProvider;
        }

        public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
        {
            HttpContext httpContext = HttpContext.Current;
            string name = httpContext.User.Identity.Name;
            var cacheKey = string.Format("{0}_{1}_{2}", rootUrl, apiVersion, name);
            return _cache.GetOrAdd(cacheKey, (key) => _swaggerProvider.GetSwagger(rootUrl, apiVersion));
        }
    }

我已按照上述实现,但不幸的是对我没有起作用,您能否指导我。我正在通过URL传递api_key和value。 - murtaza ali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接