.net core web api swagger ui 根据角色显示

环境:.net core web api+identity server4+swagger

问题:没有根据角色来显示对应有权限的api

        根据网络上以及官网的.net core web api配置identity server4和swagger结合的范例,我们会发现swagger会显示除了使用ApiExplorerSettings等属性隐藏的全部api,没有说怎么配置或者怎么改造得以根据identity后的角色来显示,因为如果api有角色权限控制,不同角色应该显示对应的api列表即可,虽然访问也是会返回401,但是还是不应该所有的api都暴露出来。

解决:DocumentFilter+js插件开发

1、SwaggerGenOptions.DocumentFilter去扩展Swagger来修改SwaggerDocuments

services.AddSwaggerGen(options =>
            {
                options.DocumentFilter();
            });

2、开发CustomSwaggerFilter根据角色过滤显示

internal class CustomSwaggerFilter: IDocumentFilter
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        private readonly IAuthenticationSchemeProvider _schemes;

        public CustomSwaggerFilter(IHttpContextAccessor httpContextAccessor, IAuthenticationSchemeProvider provider)
        {
            this._httpContextAccessor = httpContextAccessor;
            this._schemes = provider;
        }

        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            Authentication(this._httpContextAccessor.HttpContext);
            var user = this._httpContextAccessor.HttpContext.User;
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var endpointMetadata = apiDescription?.ActionDescriptor?.EndpointMetadata;
                if (null == endpointMetadata || endpointMetadata.Count == 0)
                {
                    continue;
                }
                var allowAnonymousAttributes = endpointMetadata.OfType();
                if (null != allowAnonymousAttributes && allowAnonymousAttributes.Count() > 0)
                {
                    //AllowAnonymousAttribute is to allow AllowAnonymous whether in control or method
                    continue;
                }
                var authorizeAttributes = endpointMetadata.OfType();
                if (null == authorizeAttributes || authorizeAttributes.Count() == 0)
                {
                    continue;
                }
                if (!IsInRole(authorizeAttributes, user))
                {
                    var key = "/" + apiDescription.RelativePath;
                    swaggerDoc.Paths.Remove(key);
                }
            }
            swaggerDoc.Paths = swaggerDoc.Paths.Count == 0 ? null : swaggerDoc.Paths;
            swaggerDoc.Components.Schemas = swaggerDoc.Paths == null ? null : swaggerDoc.Components.Schemas;
        }

        private bool IsInRole(IEnumerable authorizeAttributes, System.Security.Claims.ClaimsPrincipal user)
        {
            bool isInRole = true;
            foreach(var authorizeAttribute in authorizeAttributes)
            {
                if (string.IsNullOrWhiteSpace(authorizeAttribute.Roles))
                {
                    continue;
                }
                var roles = authorizeAttribute.Roles.Split(',');
                if(roles.Any(role => user.IsInRole(role)) == false)
                {
                    isInRole = false;
                    break;
                }
            }
            return isInRole;
        }

        /// 
        /// Authentication:
        /// mock AuthenticationMiddleware to get user claim
        /// 
        /// 
        private void Authentication(HttpContext httpContext)
        {
            if(IsAuthentication(httpContext) == false)
            {
                return;
            }
            var middleware = new AuthenticationMiddleware(c => Task.CompletedTask, this._schemes);
            middleware.Invoke(httpContext).ConfigureAwait(false).GetAwaiter().GetResult();
        }

        private bool IsAuthentication(HttpContext httpContext)
        {
            if (httpContext.Request.Headers.TryGetValue("Authorization", out var authorizationValue) == false)
            {
                return false;
            }
            const string BearerScheme = "Bearer";
            var authHeader = AuthenticationHeaderValue.Parse(authorizationValue);
            if (authHeader.Scheme.Equals(BearerScheme, StringComparison.CurrentCultureIgnoreCase) == false)
            {
                return false;
            }
            if (string.IsNullOrWhiteSpace(authHeader.Parameter))
            {
                return false;
            }
            return true;
        }
    }

说明:这边说下Authentication这个方法,因为在这边获取到的httpcontext的user始终是空的,所以这边需要根据token用AuthenticationMiddleware去验证下成功后就会有user

3、以上就把按角色过滤做好了,但是我们会发现,只有页面刷新才会进入,然后我们通过swagger的Authorize验证登录后也不会有刷新的动作,而且如果我们验证登录后手动刷新又是未验证的状态(ps:目前这个暂时没有解决),但是应该是在登录以及注销应该进行刷新页面显示对应角色的列表,然后未验证状态下显示没有角色控制的api列表,根据以上参考了github上issue上的也有人咨询相关的问题,目前暂时可以通过swaggger的index页面上js开发以支持登录后注销后刷新,同时在登录刷新后再次请求swagger.json这个文件前传入token以支持上一步的手动验证

// index.html
// Parse and add interceptor functions
            var interceptors = JSON.parse('%(Interceptors)');
            if (interceptors.RequestInterceptorFunction) {
                configObject.requestInterceptor = parseFunction(interceptors.RequestInterceptorFunction);
            } else {
                configObject.requestInterceptor = function (e) {
                    var token = configObject.system.auth().getIn(["authorized", "oauth2", "token", "access_token"]);
                    if (token != null && e.headers.Authorization == null) {
                        e.headers.Authorization = "Bearer " + token;
                    }
                    return e;
                }
            }
            if (interceptors.ResponseInterceptorFunction)
                configObject.responseInterceptor = parseFunction(interceptors.ResponseInterceptorFunction);

            configObject.plugins = [
                function (system) {
                    configObject.system = system;
                    return {
                        statePlugins: {
                            auth: {
                                wrapActions: {
                                    authorizeOauth2: (oriAction, system) => (a) => {
                                        var r = oriAction(a);
                                        system.specActions.download();
                                        return r;
                                    },
                                    logout: (oriAction, system) => (a) => {
                                        var r = oriAction(a);
                                        system.specActions.download();
                                        return r;
                                    }
                                }
                            }
                        }
                    }
                }
            ];

说明:这个index.html就是拷贝swagger的index.html,可到对应版本的swashbuckle.AspNetCore下载,例如:https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v6.1.1/src/Swashbuckle.AspNetCore.SwaggerUI/index.html

然后如下对应配置下:

app.UseSwaggerUI(option =>
            {
                option.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("xxx.index.html");
            });

以上就是我解决这个问题的步骤,如果你也有这个问题,希望看到这篇问题能给你带来帮助,同时如果有不好的地方欢迎指出,谢谢!

参考:https://stackoverflow.com/questions/38070950/generate-swashbuckle-api-documentation-based-on-roles-api-key

https://github.com/swagger-api/swagger-ui/issues/4734

你可能感兴趣的:(.net,core,web,api,.netcore,.net,swagger2,oauth2)