diff --git a/Configurations/SimApiDocOptions.cs b/Configurations/SimApiDocOptions.cs new file mode 100644 index 0000000..00a1e1e --- /dev/null +++ b/Configurations/SimApiDocOptions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace SimApi.Configs +{ + /// + /// 文档组配置 + /// + public class SimApiDocGroupOption + { + /// + /// 文档标识 + /// + public string Id { get; set; } + + /// + /// 文档名称 + /// + public string Name { get; set; } + + /// + /// 文档描述 + /// + public string Description { get; set; } + } + + /// + /// 授权配置, Type支持 "SimApiAuth","ClientCredentials","Implicit","AuthorizationCode" + /// + public class SimApiAuthOption + { + public string[] Type { get; set; } = new[] {"SimApiAuth"}; + public string Description { get; set; } = "认证服务器颁发的AccessToken"; + public string AuthorizationUrl { get; set; } + public string TokenUrl { get; set; } + public Dictionary Scopes { get; set; } + } + + /// + /// 文档配置 + /// + public class SimApiDocOptions + { + /// + /// 文档组配置 + /// + public SimApiDocGroupOption[] ApiGroups { get; set; } = new[] + { + new SimApiDocGroupOption + { + Id = "api", + Name = "Api", + Description = "Api接口文档" + } + }; + + /// + /// 授权配置 + /// + public SimApiAuthOption ApiAuth { get; set; } = new SimApiAuthOption(); + + /// + /// 文档页面标题 + /// + public string DocumentTitle { get; set; } = "API接口文档"; + + /// + /// 接口支持的调用方式 + /// + public SubmitMethod[] SupportedMethod { get; set; } = new[] {SubmitMethod.Post}; + } +} \ No newline at end of file diff --git a/Configurations/SimApiOptions.cs b/Configurations/SimApiOptions.cs new file mode 100644 index 0000000..67b2a69 --- /dev/null +++ b/Configurations/SimApiOptions.cs @@ -0,0 +1,27 @@ +using System; + +namespace SimApi.Configs +{ + public class SimApiOptions + { + public bool EnableSimApiAuth { get; set; } = false; + public bool EnableSimApiDoc { get; set; } = true; + public bool EnableSimApiException { get; set; } = true; + public bool EnableSimApiStorage { get; set; } = false; + public bool EnableForwardHeaders { get; set; } = true; + public bool EnableLowerUrl { get; set; } = true; + public SimApiDocOptions SimApiDocOptions { get; set; } = new SimApiDocOptions(); + public SimApiStorageOptions SimApiStorageOptions { get; set; } = new SimApiStorageOptions(); + + + public void ConfigureSimApiDoc(Action options = null) + { + options?.Invoke(SimApiDocOptions); + } + + public void ConfigureSimApiStorage(Action options = null) + { + options?.Invoke(SimApiStorageOptions); + } + } +} \ No newline at end of file diff --git a/Configurations/SimApiStorageOptions.cs b/Configurations/SimApiStorageOptions.cs new file mode 100644 index 0000000..fdd3110 --- /dev/null +++ b/Configurations/SimApiStorageOptions.cs @@ -0,0 +1,11 @@ +namespace SimApi.Configs +{ + public class SimApiStorageOptions + { + public string Endpoint { get; set; } + public string ServeUrl { get; set; } + public string Bucket { get; set; } + public string AccessKey { get; set; } + public string SecretKey { get; set; } + } +} \ No newline at end of file diff --git a/Helpers/SimApiStorage.cs b/Helpers/SimApiStorage.cs new file mode 100644 index 0000000..c63b091 --- /dev/null +++ b/Helpers/SimApiStorage.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Http; +using Minio; +using Minio.Exceptions; +using SimApi.Configs; + +namespace SimApi.Helpers +{ + public class SimApiStorage + { + private MinioClient Mc { get; } + private string ServeUrl { get; } + private string Bucket { get; } + private IHttpContextAccessor HttpContextAccessor { get; } + + public SimApiStorage(SimApiOptions apiOptions, IHttpContextAccessor httpContextAccessor) + { + var options = apiOptions.SimApiStorageOptions; + HttpContextAccessor = httpContextAccessor; + var useSsl = false; + var endpoint = string.Empty; + if (options.Endpoint.StartsWith("http://")) + { + endpoint = options.Endpoint.Replace("http://", string.Empty); + } + else if (options.Endpoint.StartsWith("https://")) + { + endpoint = options.Endpoint.Replace("https://", string.Empty); + useSsl = true; + } + else + { + throw new Exception("SimApiStorage: Error Endpoint"); + } + + ServeUrl = options.ServeUrl; + Bucket = options.Bucket; + if (useSsl) + { + Mc = new MinioClient(endpoint, options.AccessKey, options.SecretKey).WithSSL(); + } + else + { + Mc = new MinioClient(endpoint, options.AccessKey, options.SecretKey); + } + + bool found = Mc.BucketExistsAsync(Bucket).Result; + if (!found) + { + Mc.MakeBucketAsync(Bucket).Wait(); + } + } + + public string UploadFile(string path, Stream stream) + { + try + { + Mc.PutObjectAsync(Bucket, path, stream, stream.Length).Wait(); + return null; + } + catch (MinioException e) + { + Console.WriteLine("Error occurred: " + e); + return e.Message; + } + } + + public string FullUrl(string path) + { + var httpRequest = HttpContextAccessor.HttpContext.Request; + var url = $"{httpRequest.Scheme}://{httpRequest.Host}"; + if (string.IsNullOrEmpty(path)) + { + return path; + } + + return path.StartsWith("~") ? url + path.Substring(1, path.Length - 1) : $"{ServeUrl}{path}"; + } + } +} \ No newline at end of file diff --git a/Helpers/SimApiUpload.cs b/Helpers/SimApiUpload.cs deleted file mode 100644 index 92f12eb..0000000 --- a/Helpers/SimApiUpload.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using Microsoft.AspNetCore.Hosting; - -namespace SimApi.Helpers -{ - public class SimApiUpload - { - private string FilePathFolder = "/uploads/"; - public string FilePath - { - set - { - if (!value.StartsWith("/")) - { - FilePathFolder = $"/{value}"; - } - if (!FilePathFolder.EndsWith("/")) - { - FilePathFolder += "/"; - } - } - get - { - return FilePathFolder; - } - } - - private IWebHostEnvironment Env { get; } - - public SimApiUpload(IWebHostEnvironment env) - { - Env = env; - } - - public class SimApiUploadInfo - { - public string Path { get; set; } - public int Size { get; set; } - } - - /// - /// 保存base64文件,如果有标头按照标头自动识别 - /// - /// - /// 扩展名 - /// 存放路径 - /// 文件名 - /// - public SimApiUploadInfo SaveFile(string base64, string ext = null, string path = null, string fileName = null) - { - byte[] bt; - var filePath = FilePath + path; - if (fileName == null) - { - fileName = Guid.NewGuid().ToString(); - } - var baseArr = base64.Split(";"); - if (baseArr.Length == 2) - { - var info = baseArr[0].Split(":")[1].Split("/"); - if (path == null) - { - filePath += $"{info[0]}/"; - } - if (ext == null) - { - ext = info[1]; - } - var data = baseArr[1].Split(","); - bt = Convert.FromBase64String(data[1]); - } - else - { - bt = Convert.FromBase64String(base64); - } - var fn = fileName + (string.IsNullOrEmpty(ext) ? string.Empty : $".{ext}"); - return WriteFile(filePath, fn, bt); - } - - /// - /// 把数据写入文件 - /// - /// - /// - /// - /// - public SimApiUploadInfo WriteFile(string filePath, string fileName, byte[] data) - { - var realPath = Directory.GetCurrentDirectory() + "/wwwroot" + filePath; - if (!Directory.Exists(realPath)) - { - Directory.CreateDirectory(realPath); - } - File.WriteAllBytes(realPath + fileName, data); - return new SimApiUploadInfo - { - Path = filePath + fileName, - Size = data.Length - }; - } - } -} diff --git a/README.md b/README.md index 4e58da0..655ac47 100644 --- a/README.md +++ b/README.md @@ -23,43 +23,33 @@ namespace Controllers } ``` -2. 基于Swagger的API文档服务 +统一配置,现在只需要进行AddSimApi的配置, Configure中直接 UseSimApi即可。 ```C# -#Startup.cs - - services.AddSimApiDoc("文档名称", "文档描述"); - - app.UseSimApiDoc("名称",SubmitMethod[]) - -#控制器中可以直接使用特性 - - [SimApiDoc("分组","名称")] +services.AddSimApi(options => +{ + options.ConfigureSimApiDoc(options => + { + options.ApiGroups = new[] + { + new SimApiDocGroupOption + {Id = "admin", Name = "后台管理接口", Description = "本接口调用需要Scope:sac.api.admin"}, + new SimApiDocGroupOption + {Id = "user-v1", Name = "用户中心接口", Description = "本接口调用需要Scope:sac.api.user"} + }; + options.ApiAuth = new SimApiAuthOption + { + Type = new[] {"ClientCredentials", "Implicit", "AuthorizationCode"}, + Scopes = new Dictionary + { + {"sac.api.user", "用户信息接口权限"}, + {"sac.api.admin", "后台管理API"} + }, + AuthorizationUrl = "/connect/authorize", + TokenUrl = "/connect/token" + }; + }); + options.EnableSimApiStorage = true; + options.SimApiStorageOptions = Configuration.GetSection("S3").Get(); +}); ``` - -3. 简单的基于Redis的登录TOKEN服务 - -```C# -#Startup.cs -service.AddSimApiAuth(); -``` -添加时候, 可以从DI中获取 Auth 类,调用 Auth.Set(int,string) 将用户ID/类型和生成的Token绑定,本方法返回缓存中的Key名称 - -```C# -#Startup.cs -app.UseSimApiAuth(); -``` -调用本中间件,然后再需要登录认证的地方,使用 [YYAuth] 特性,即可完成检测登录相关的操作, -如果需要获取用户的ID, 只需要 直接使用 LoginId 属性即可获取 - -4. 统一返回 -所有Response 均需要继承 SimApiBaseResponse类 - - -5. 异常处理 -本异常中间件封装了API错误的异常处理,控制器可以使用 Error(int,string) 直接返回API错误 - -```C# -#Startup.cs -app.UseSimApiException(); -``` \ No newline at end of file diff --git a/SimApi.csproj b/SimApi.csproj index 2a4120d..45cb0c1 100644 --- a/SimApi.csproj +++ b/SimApi.csproj @@ -25,10 +25,12 @@ + - - + + + diff --git a/SimApiExtensions.cs b/SimApiExtensions.cs index a3552b6..c67e3d8 100644 --- a/SimApiExtensions.cs +++ b/SimApiExtensions.cs @@ -3,188 +3,190 @@ using SimApi.Helpers; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerUI; using SimApi.Middlewares; using Microsoft.AspNetCore.HttpOverrides; +using SimApi.Configs; namespace SimApi { /// /// 加入系统的扩展信息 /// - public static class Extensions + public static class SimApiExtensions { - public static string DocumentTitle = ""; - public static string DocumentDescription = ""; - //**********快捷添加************** - - /// - /// 添加整个SimAPI,同时增加CORS规则 - /// - /// - /// - /// - /// - public static IServiceCollection AddSimApi(this IServiceCollection builder, string title, - string description = null) + public static IServiceCollection AddSimApi(this IServiceCollection builder, + Action options = null) { - return builder.AddSimApiAuth().AddSimApiDoc(title, description).AddCors().AddSimApiUpload().Configure(options => + var simApiOptions = new SimApiOptions(); + options?.Invoke(simApiOptions); + + // 是否使用SIMAUTH + if (simApiOptions.EnableSimApiAuth) { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto; - // Only loopback proxies are allowed by default. - // Clear that restriction because forwarders are enabled by explicit - // configuration. - options.KnownNetworks.Clear(); - options.KnownProxies.Clear(); - }); ; + builder.AddScoped(); + } + + // 使用SimApiDoc + if (simApiOptions.EnableSimApiDoc) + { + var docOptions = simApiOptions.SimApiDocOptions; + builder.AddSwaggerGen(x => + { + foreach (var group in docOptions.ApiGroups) + { + x.SwaggerDoc(group.Id, new OpenApiInfo {Title = group.Name, Description = group.Description}); + } + + x.EnableAnnotations(); + var haveOauth = false; + var haveSimApiAuth = false; + var oauthFlows = new OpenApiOAuthFlows(); + foreach (var auth in docOptions.ApiAuth.Type) + { + switch (auth) + { + case "SimApiAuth": + x.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + {Type = ReferenceType.SecurityScheme, Id = "HeaderToken"} + }, + new[] {"readAccess", "writeAccess"} + } + }); + haveSimApiAuth = true; + break; + case "ClientCredentials": + oauthFlows.ClientCredentials = new OpenApiOAuthFlow + { + TokenUrl = new Uri(docOptions.ApiAuth.TokenUrl, UriKind.RelativeOrAbsolute), + Scopes = docOptions.ApiAuth.Scopes + }; + haveOauth = true; + break; + case "Implicit": + oauthFlows.Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(docOptions.ApiAuth.AuthorizationUrl, + UriKind.RelativeOrAbsolute), + Scopes = docOptions.ApiAuth.Scopes + }; + haveOauth = true; + break; + case "AuthorizationCode": + oauthFlows.AuthorizationCode = new OpenApiOAuthFlow + { + TokenUrl = new Uri(docOptions.ApiAuth.TokenUrl, UriKind.RelativeOrAbsolute), + AuthorizationUrl = new Uri(docOptions.ApiAuth.AuthorizationUrl, + UriKind.RelativeOrAbsolute), + Scopes = docOptions.ApiAuth.Scopes + }; + haveOauth = true; + break; + } + } + + if (haveOauth) + { + x.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = oauthFlows, + Description = docOptions.ApiAuth.Description, + In = ParameterLocation.Header + }); + x.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + {Type = ReferenceType.SecurityScheme, Id = "oauth2"} + }, + new[] {"SimApiAuth"} + } + }); + } + + if (haveSimApiAuth) + { + x.AddSecurityDefinition("HeaderToken", + new OpenApiSecurityScheme {Name = "Token", In = ParameterLocation.Header}); + } + }); + } + + // 使用Header转发,应对代理后获取真实ip + if (simApiOptions.EnableForwardHeaders) + { + builder.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + } + + if (simApiOptions.EnableLowerUrl) + { + builder.AddRouting(options => options.LowercaseUrls = true); + } + + if (simApiOptions.EnableSimApiStorage) + { + builder.AddSingleton(); + } + + builder.AddSingleton(simApiOptions); + return builder; } /// /// 使用所有SimApi自定义中间件 /// /// - /// - /// - /// /// - public static IApplicationBuilder UseSimApi(this IApplicationBuilder builder, params SubmitMethod[] submitMethods) + public static IApplicationBuilder UseSimApi(this IApplicationBuilder builder) { - return builder.UseSimApiException().UseSimApiDoc(submitMethods).UseMiddleware().UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()).UseSimApiUpload().UseForwardedHeaders(); - } - - - - //*********组件快捷方式************* - - /// - /// 添加自定义授权认证中间价 - /// - /// - /// - public static IServiceCollection AddSimApiAuth(this IServiceCollection builder) - { - return builder.AddScoped(); - } - - /// - /// 添加Base64上传组件 - /// - /// - /// - public static IServiceCollection AddSimApiUpload(this IServiceCollection builder) - { - return builder.AddSingleton(); - } - - /// - /// 添加Api文档服务 - /// - /// - /// 文档标题 - /// 文档描述 - /// - public static IServiceCollection AddSimApiDoc(this IServiceCollection builder, string title, - string description = null) - { - DocumentTitle = title; - DocumentDescription = description; - return builder.AddSwaggerGen(x => + var options = builder.ApplicationServices.GetService(); + if (options.EnableSimApiAuth) { - x.SwaggerDoc("api", new OpenApiInfo { Title = DocumentTitle, Description = DocumentDescription }); - x.EnableAnnotations(); - x.AddSecurityRequirement(new OpenApiSecurityRequirement + builder.UseMiddleware(); + } + + if (options.EnableSimApiDoc) + { + var docOptions = options.SimApiDocOptions; + return builder.UseSwagger(x => x.RouteTemplate = "/swagger/{documentName}.json").UseSwaggerUI(x => { + x.DocumentTitle = docOptions.DocumentTitle; + foreach (var group in docOptions.ApiGroups) { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "HeaderToken"} - }, - new[] {"readAccess", "writeAccess"} + x.SwaggerEndpoint($"/swagger/{group.Id}.json", name: group.Name); } + + x.EnableValidator(); + x.SupportedSubmitMethods(docOptions.SupportedMethod); + x.DisplayRequestDuration(); }); - x.AddSecurityDefinition("HeaderToken", - new OpenApiSecurityScheme { Name = "Token", In = ParameterLocation.Header }); - }); - } + } - - /// - /// 使用异常中间价扩展 - /// - /// - /// - public static IApplicationBuilder UseSimApiException(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - - /// - /// 配置上传组件必须 - /// - /// - /// - public static IApplicationBuilder UseSimApiUpload(this IApplicationBuilder builder) - { - return builder.UseStaticFiles(new StaticFileOptions + if (options.EnableSimApiException) { - DefaultContentType = "application/x-msdownload", - ServeUnknownFileTypes = true - }); - } + builder.UseMiddleware(); + } - /// - /// 使用文档UI - /// - /// - /// 文档标题 - /// 文档支持的提交方式(如果不指定,默认使用POST) - /// - public static IApplicationBuilder UseSimApiDoc(this IApplicationBuilder builder, params SubmitMethod[] submitMethods) - { - return builder.UseSwagger(x => x.RouteTemplate = "docs/{documentName}.json").UseSwaggerUI(x => + if (options.EnableForwardHeaders) { - x.RoutePrefix = "docs"; - x.DocumentTitle = DocumentTitle; - x.SwaggerEndpoint("/docs/api.json", name: DocumentTitle); - x.EnableValidator(); - if (submitMethods.Length > 0) - { - x.SupportedSubmitMethods(submitMethods); - } - else - { - x.SupportedSubmitMethods(SubmitMethod.Post); - } + builder.UseForwardedHeaders(); + } - x.DisplayRequestDuration(); - }); + return builder; } - - - /// - /// 同时使用Api文档和异常处理中间件 - /// - /// - /// 文档标题 - /// API提交方式定义 - /// - public static IApplicationBuilder UseSimApiDocEx(this IApplicationBuilder builder, params SubmitMethod[] submitMethods) - { - return builder.UseSimApiException().UseSimApiDoc(submitMethods); - } - - /// - /// 使用自定义认证中间件(依赖IDistributedCache) - /// - /// - /// - public static IApplicationBuilder UseSimApiAuth(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - - } } \ No newline at end of file