diff --git a/src/Hua.Abp.Demo.HttpApi.Host/DemoHttpApiHostModule.cs b/src/Hua.Abp.Demo.HttpApi.Host/DemoHttpApiHostModule.cs index 5f65fd3..11e9937 100644 --- a/src/Hua.Abp.Demo.HttpApi.Host/DemoHttpApiHostModule.cs +++ b/src/Hua.Abp.Demo.HttpApi.Host/DemoHttpApiHostModule.cs @@ -40,6 +40,7 @@ using Volo.Abp.OpenIddict; using Volo.Abp.Swashbuckle; using Volo.Abp.Studio.Client.AspNetCore; using Volo.Abp.Security.Claims; +using Hua.Abp.Demo.Swagger; namespace Hua.Abp.Demo; @@ -135,6 +136,12 @@ public class DemoHttpApiHostModule : AbpModule var configuration = context.Services.GetConfiguration(); context.Services.AddAuthentication() + .AddJwtBearer(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); + options.Audience = "Demo"; + }) .AddOpenIdConnect("WeGit", "Login with WeGit", options => { options.Authority = "https://git.we965.cn"; @@ -241,6 +248,31 @@ public class DemoHttpApiHostModule : AbpModule options.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" }); options.DocInclusionPredicate((docName, description) => true); options.CustomSchemaIds(type => type.FullName); + + // 注册 OpenIddict 文档过滤器 + options.DocumentFilter(); + + // 添加 JWT Bearer 认证支持 + var securityScheme = new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }; + + options.AddSecurityDefinition("Bearer", securityScheme); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { securityScheme, new string[] { } } + }); }); } diff --git a/src/Hua.Abp.Demo.HttpApi.Host/Hua.Abp.Demo.HttpApi.Host.csproj b/src/Hua.Abp.Demo.HttpApi.Host/Hua.Abp.Demo.HttpApi.Host.csproj index ebebfac..ef99d96 100644 --- a/src/Hua.Abp.Demo.HttpApi.Host/Hua.Abp.Demo.HttpApi.Host.csproj +++ b/src/Hua.Abp.Demo.HttpApi.Host/Hua.Abp.Demo.HttpApi.Host.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Hua.Abp.Demo.HttpApi.Host/Swagger/OpenIddictDocumentFilter.cs b/src/Hua.Abp.Demo.HttpApi.Host/Swagger/OpenIddictDocumentFilter.cs new file mode 100644 index 0000000..94d7ae0 --- /dev/null +++ b/src/Hua.Abp.Demo.HttpApi.Host/Swagger/OpenIddictDocumentFilter.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Hua.Abp.Demo.Swagger; + +public class OpenIddictDocumentFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var paths = new Dictionary + { + ["/.well-known/openid-configuration"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Tags = new List { new OpenApiTag { Name = "OpenID Connect" } }, + Summary = "获取 OpenID Connect 发现文档", + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "成功", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["issuer"] = new OpenApiSchema { Type = "string", Description = "发行者 URL", Example = new OpenApiString("https://localhost:44322") }, + ["authorization_endpoint"] = new OpenApiSchema { Type = "string", Description = "授权端点 URL", Example = new OpenApiString("https://localhost:44322/connect/authorize") }, + ["token_endpoint"] = new OpenApiSchema { Type = "string", Description = "令牌端点 URL", Example = new OpenApiString("https://localhost:44322/connect/token") }, + ["jwks_uri"] = new OpenApiSchema { Type = "string", Description = "公钥集 URL", Example = new OpenApiString("https://localhost:44322/.well-known/jwks") }, + ["userinfo_endpoint"] = new OpenApiSchema { Type = "string", Description = "用户信息端点 URL", Example = new OpenApiString("https://localhost:44322/connect/userinfo") }, + ["scopes_supported"] = new OpenApiSchema { Type = "array", Description = "支持的作用域", Items = new OpenApiSchema { Type = "string" } }, + ["claims_supported"] = new OpenApiSchema { Type = "array", Description = "支持的声明", Items = new OpenApiSchema { Type = "string" } }, + ["response_types_supported"] = new OpenApiSchema { Type = "array", Description = "支持的响应类型", Items = new OpenApiSchema { Type = "string" } } + } + } + } + } + } + } + } + } + }, + ["/connect/token"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Post] = new OpenApiOperation + { + Tags = new List { new OpenApiTag { Name = "OpenID Connect" } }, + Summary = "获取访问令牌 (Access Token)", + Description = "使用授权码、密码或刷新令牌来换取访问令牌", + RequestBody = new OpenApiRequestBody + { + Content = new Dictionary + { + ["application/x-www-form-urlencoded"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["grant_type"] = new OpenApiSchema { Type = "string", Description = "授权类型 (password, client_credentials, authorization_code, refresh_token)", Example = new OpenApiString("password") }, + ["username"] = new OpenApiSchema { Type = "string", Description = "用户名 (密码模式必填)", Example = new OpenApiString("admin") }, + ["password"] = new OpenApiSchema { Type = "string", Description = "密码 (密码模式必填)", Example = new OpenApiString("1q2w3E*") }, + ["scope"] = new OpenApiSchema { Type = "string", Description = "请求的作用域 (空格分隔)", Example = new OpenApiString("openid profile email offline_access Demo") }, + ["client_id"] = new OpenApiSchema { Type = "string", Description = "客户端 ID", Example = new OpenApiString("Demo_App") }, + ["client_secret"] = new OpenApiSchema { Type = "string", Description = "客户端密钥 (如适用)" }, + ["code"] = new OpenApiSchema { Type = "string", Description = "授权码 (授权码模式必填)" }, + ["redirect_uri"] = new OpenApiSchema { Type = "string", Description = "重定向 URI" }, + ["refresh_token"] = new OpenApiSchema { Type = "string", Description = "刷新令牌 (刷新模式必填)" } + }, + Required = new HashSet { "grant_type" } + } + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "成功", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["access_token"] = new OpenApiSchema { Type = "string", Description = "访问令牌 (JWT)" }, + ["token_type"] = new OpenApiSchema { Type = "string", Description = "令牌类型 (通常为 Bearer)", Example = new OpenApiString("Bearer") }, + ["expires_in"] = new OpenApiSchema { Type = "integer", Format = "int32", Description = "过期时间 (秒)", Example = new OpenApiInteger(3600) }, + ["scope"] = new OpenApiSchema { Type = "string", Description = "实际授予的作用域" }, + ["id_token"] = new OpenApiSchema { Type = "string", Description = "身份令牌 (JWT,仅在请求 openid scope 时返回)" }, + ["refresh_token"] = new OpenApiSchema { Type = "string", Description = "刷新令牌 (仅在请求 offline_access scope 时返回)" } + } + } + } + } + }, + ["400"] = new OpenApiResponse + { + Description = "请求无效", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["error"] = new OpenApiSchema { Type = "string", Description = "错误代码" }, + ["error_description"] = new OpenApiSchema { Type = "string", Description = "错误描述" } + } + } + } + } + } + } + } + } + }, + ["/connect/authorize"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Tags = new List { new OpenApiTag { Name = "OpenID Connect" } }, + Summary = "发起授权请求", + Description = "重定向用户到此端点以开始登录流程", + Parameters = new List + { + new OpenApiParameter { Name = "response_type", In = ParameterLocation.Query, Required = true, Schema = new OpenApiSchema { Type = "string", Example = new OpenApiString("code") }, Description = "响应类型 (如 code)" }, + new OpenApiParameter { Name = "client_id", In = ParameterLocation.Query, Required = true, Schema = new OpenApiSchema { Type = "string", Example = new OpenApiString("Demo_App") }, Description = "客户端 ID" }, + new OpenApiParameter { Name = "redirect_uri", In = ParameterLocation.Query, Schema = new OpenApiSchema { Type = "string" }, Description = "登录成功后的重定向 URI" }, + new OpenApiParameter { Name = "scope", In = ParameterLocation.Query, Schema = new OpenApiSchema { Type = "string", Example = new OpenApiString("openid profile") }, Description = "请求的作用域" }, + new OpenApiParameter { Name = "state", In = ParameterLocation.Query, Schema = new OpenApiSchema { Type = "string" }, Description = "状态值 (用于防止 CSRF)" } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "成功 (返回 HTML 登录页)" } + } + } + } + }, + ["/connect/userinfo"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Tags = new List { new OpenApiTag { Name = "OpenID Connect" } }, + Summary = "获取用户信息", + Description = "使用 Access Token 获取当前用户的详细信息", + Security = new List + { + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + new string[] { } + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "成功", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["sub"] = new OpenApiSchema { Type = "string", Description = "用户唯一标识 (ID)" }, + ["name"] = new OpenApiSchema { Type = "string", Description = "用户名" }, + ["email"] = new OpenApiSchema { Type = "string", Description = "邮箱" }, + ["email_verified"] = new OpenApiSchema { Type = "boolean", Description = "邮箱是否已验证" } + } + } + } + } + } + } + } + } + } + }; + + foreach (var path in paths) + { + if (!swaggerDoc.Paths.ContainsKey(path.Key)) + { + swaggerDoc.Paths.Add(path.Key, path.Value); + } + } + } +}