From fd676201ecd852193f8f5d2171dee65d3403e253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=B9=E9=B9=8F=E9=A3=9E?= Date: Thu, 30 May 2024 09:27:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .../Authorizers/HttpRolesAuthorizer.cs | 49 +++ .../Authorizers/IRolesAuthorizer.cs | 9 + .../Authorizers/RolesAuthorizerBase.cs | 24 ++ HuiXin.Gateway.Ocelot/Configs/jwt.json | 6 + HuiXin.Gateway.Ocelot/Configs/ocelot.json | 54 ++++ HuiXin.Gateway.Ocelot/Configs/serilog.json | 15 + .../Configurations/RolesAuthorizerConfiguration.cs | 9 + HuiXin.Gateway.Ocelot/Extensions/JWTExtensions.cs | 46 +++ .../Extensions/OcelotExtensions.cs | 64 ++++ HuiXin.Gateway.Ocelot/HuiXin.Gateway.Ocelot.csproj | 23 ++ HuiXin.Gateway.Ocelot/JWTUtil.cs | 64 ++++ .../Middlewares/AccessLoggingMiddleware.cs | 132 ++++++++ .../Middlewares/MyAuthorizationMiddleware.cs | 34 ++ HuiXin.Gateway.Ocelot/Program.cs | 78 +++++ .../Properties/launchSettings.json | 30 ++ HuiXin.Gateway.Ocelot/appsettings.Development.json | 9 + HuiXin.Gateway.Ocelot/appsettings.json | 9 + HuiXin.Gateway.Yarp/HuiXin.Gateway.Yarp.csproj | 13 + HuiXin.Gateway.Yarp/Program.cs | 15 + HuiXin.Gateway.Yarp/Properties/launchSettings.json | 29 ++ HuiXin.Gateway.Yarp/appsettings.Development.json | 8 + HuiXin.Gateway.Yarp/appsettings.json | 9 + HuiXin.Gateway.sln | 37 +++ HuiXin.Identity.OpenIddict/ApplicationDbContext.cs | 12 + .../Controllers/AuthorizationController.cs | 357 +++++++++++++++++++++ .../Controllers/UserController.cs | 21 ++ .../HuiXin.Identity.OpenIddict.csproj | 19 ++ HuiXin.Identity.OpenIddict/Program.cs | 108 +++++++ .../Properties/launchSettings.json | 29 ++ HuiXin.Identity.OpenIddict/UserInfo.cs | 11 + HuiXin.Identity.OpenIddict/Worker.cs | 41 +++ .../appsettings.Development.json | 8 + HuiXin.Identity.OpenIddict/appsettings.json | 9 + 34 files changed, 1384 insertions(+) create mode 100644 .gitignore create mode 100644 HuiXin.Gateway.Ocelot/Authorizers/HttpRolesAuthorizer.cs create mode 100644 HuiXin.Gateway.Ocelot/Authorizers/IRolesAuthorizer.cs create mode 100644 HuiXin.Gateway.Ocelot/Authorizers/RolesAuthorizerBase.cs create mode 100644 HuiXin.Gateway.Ocelot/Configs/jwt.json create mode 100644 HuiXin.Gateway.Ocelot/Configs/ocelot.json create mode 100644 HuiXin.Gateway.Ocelot/Configs/serilog.json create mode 100644 HuiXin.Gateway.Ocelot/Configurations/RolesAuthorizerConfiguration.cs create mode 100644 HuiXin.Gateway.Ocelot/Extensions/JWTExtensions.cs create mode 100644 HuiXin.Gateway.Ocelot/Extensions/OcelotExtensions.cs create mode 100644 HuiXin.Gateway.Ocelot/HuiXin.Gateway.Ocelot.csproj create mode 100644 HuiXin.Gateway.Ocelot/JWTUtil.cs create mode 100644 HuiXin.Gateway.Ocelot/Middlewares/AccessLoggingMiddleware.cs create mode 100644 HuiXin.Gateway.Ocelot/Middlewares/MyAuthorizationMiddleware.cs create mode 100644 HuiXin.Gateway.Ocelot/Program.cs create mode 100644 HuiXin.Gateway.Ocelot/Properties/launchSettings.json create mode 100644 HuiXin.Gateway.Ocelot/appsettings.Development.json create mode 100644 HuiXin.Gateway.Ocelot/appsettings.json create mode 100644 HuiXin.Gateway.Yarp/HuiXin.Gateway.Yarp.csproj create mode 100644 HuiXin.Gateway.Yarp/Program.cs create mode 100644 HuiXin.Gateway.Yarp/Properties/launchSettings.json create mode 100644 HuiXin.Gateway.Yarp/appsettings.Development.json create mode 100644 HuiXin.Gateway.Yarp/appsettings.json create mode 100644 HuiXin.Gateway.sln create mode 100644 HuiXin.Identity.OpenIddict/ApplicationDbContext.cs create mode 100644 HuiXin.Identity.OpenIddict/Controllers/AuthorizationController.cs create mode 100644 HuiXin.Identity.OpenIddict/Controllers/UserController.cs create mode 100644 HuiXin.Identity.OpenIddict/HuiXin.Identity.OpenIddict.csproj create mode 100644 HuiXin.Identity.OpenIddict/Program.cs create mode 100644 HuiXin.Identity.OpenIddict/Properties/launchSettings.json create mode 100644 HuiXin.Identity.OpenIddict/UserInfo.cs create mode 100644 HuiXin.Identity.OpenIddict/Worker.cs create mode 100644 HuiXin.Identity.OpenIddict/appsettings.Development.json create mode 100644 HuiXin.Identity.OpenIddict/appsettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3a6880 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs +bin +obj diff --git a/HuiXin.Gateway.Ocelot/Authorizers/HttpRolesAuthorizer.cs b/HuiXin.Gateway.Ocelot/Authorizers/HttpRolesAuthorizer.cs new file mode 100644 index 0000000..d34419c --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Authorizers/HttpRolesAuthorizer.cs @@ -0,0 +1,49 @@ +using Flurl; +using Flurl.Http; +using HuiXin.Gateway.Ocelot.Configurations; +using Microsoft.Extensions.Options; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using Serilog; + +namespace HuiXin.Gateway.Ocelot.Authorizers +{ + public class HttpRolesAuthorizer : RolesAuthorizerBase, IRolesAuthorizer + { + private readonly string _url; + private readonly FlurlClient _client; + + public HttpRolesAuthorizer(IClaimsParser claimsParser, IOptions configuration) : base(claimsParser, configuration) + { + _url = _configs.Url ?? throw new Exception("未配置角色验证的Url地址"); + _client = new FlurlClient(_url); + _client.Settings.Timeout = TimeSpan.FromMilliseconds(_configs.Timeout); + _client.Settings.Redirects.Enabled = false; + } + + public async Task> Authorize(List roles, string path) + { + try + { + bool pass = await _client.Request().AppendQueryParam("roles", roles).AppendQueryParam("path", path).GetJsonAsync(); + if (pass) + { + return await ReturnAsync(new OkResponse(true)); + } + else + { + return await ReturnAsync(new ErrorResponse(new HttpRolesAuthorizerFail("用户没有访问权限"))); + } + } + catch (Exception ex) + { + Log.Error(ex.Message, "验证用户角色权限出错"); + return await ReturnAsync(new ErrorResponse(new HttpRolesAuthorizerError("验证用户角色权限出错"))); + } + } + } + + public class HttpRolesAuthorizerError(string message) : Error(message, OcelotErrorCode.UnableToCompleteRequestError, 500){} + public class HttpRolesAuthorizerFail(string message) : Error(message, OcelotErrorCode.UnauthorizedError, 403){} +} \ No newline at end of file diff --git a/HuiXin.Gateway.Ocelot/Authorizers/IRolesAuthorizer.cs b/HuiXin.Gateway.Ocelot/Authorizers/IRolesAuthorizer.cs new file mode 100644 index 0000000..a097bac --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Authorizers/IRolesAuthorizer.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace HuiXin.Gateway.Ocelot.Authorizers +{ + public interface IRolesAuthorizer + { + Task> Authorize(List roles, string path); + } +} diff --git a/HuiXin.Gateway.Ocelot/Authorizers/RolesAuthorizerBase.cs b/HuiXin.Gateway.Ocelot/Authorizers/RolesAuthorizerBase.cs new file mode 100644 index 0000000..fe7ffd9 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Authorizers/RolesAuthorizerBase.cs @@ -0,0 +1,24 @@ +using HuiXin.Gateway.Ocelot.Configurations; +using Microsoft.Extensions.Options; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +namespace HuiXin.Gateway.Ocelot.Authorizers +{ + public class RolesAuthorizerBase + { + protected readonly IClaimsParser _claimsParser; + protected RolesAuthorizerConfiguration _configs; + + public RolesAuthorizerBase(IClaimsParser claimsParser, IOptions cfg) + { + _claimsParser = claimsParser; + _configs = cfg.Value ?? throw new Exception("未配置角色验证参数"); + } + + protected async Task> ReturnAsync(Response response) + { + return await Task.FromResult(response); + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Configs/jwt.json b/HuiXin.Gateway.Ocelot/Configs/jwt.json new file mode 100644 index 0000000..37074fe --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Configs/jwt.json @@ -0,0 +1,6 @@ +{ + "AuthenticationScheme": "MyKey", + "Authority": "http://localhost:5001", + "Audience": "huixin", + "IssuerSigningKeyBase64": "GcTdqSZdpRxBtdtgwvDHBzS427VGTQzbM+JD1CBbUZY=" +} \ No newline at end of file diff --git a/HuiXin.Gateway.Ocelot/Configs/ocelot.json b/HuiXin.Gateway.Ocelot/Configs/ocelot.json new file mode 100644 index 0000000..d2c767a --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Configs/ocelot.json @@ -0,0 +1,54 @@ +{ + "Routes": [ + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/user/{postId}", + "DownstreamPathTemplate": "/search?q=/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 443 + } + ], + "QoSOptions": { + "TimeoutValue": 5000 + }, + //"ServiceName": "auth", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, + "AuthenticationOptions": { + "AuthenticationProviderKey": "MyKey", + "AllowedScopes": [ "all" ] + } + }, + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/{url}", + "DownstreamPathTemplate": "/search?q={url}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "cn.bing.com", + "Port": 443 + } + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://api.mybusiness.com", + "RequestIdKey": "TraceId" + //"ServiceDiscoveryProvider": { + // "Scheme": "https", + // "Host": "localhost", + // "Port": 8500, + // "Type": "Consul" + //} + }, + "RolesAuthorizer": { + "Type": "http", + "Url": "http://auth.huixin.com/api/authorizer", + "Timeout": 2000 + } +} \ No newline at end of file diff --git a/HuiXin.Gateway.Ocelot/Configs/serilog.json b/HuiXin.Gateway.Ocelot/Configs/serilog.json new file mode 100644 index 0000000..43939b3 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Configs/serilog.json @@ -0,0 +1,15 @@ +{ + "Serilog": { + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "./Logs/gateway.txt", + "rollingInterval": "Day", + "fileSizeLimitBytes": 20971520 + } + }, + { "Name": "Console" } + ] + } +} \ No newline at end of file diff --git a/HuiXin.Gateway.Ocelot/Configurations/RolesAuthorizerConfiguration.cs b/HuiXin.Gateway.Ocelot/Configurations/RolesAuthorizerConfiguration.cs new file mode 100644 index 0000000..d142403 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Configurations/RolesAuthorizerConfiguration.cs @@ -0,0 +1,9 @@ +namespace HuiXin.Gateway.Ocelot.Configurations +{ + public class RolesAuthorizerConfiguration + { + public string Type { get; set; } + public string Url { get; set; } + public int Timeout { get; set; } + } +} diff --git a/HuiXin.Gateway.Ocelot/Extensions/JWTExtensions.cs b/HuiXin.Gateway.Ocelot/Extensions/JWTExtensions.cs new file mode 100644 index 0000000..400cacc --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Extensions/JWTExtensions.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace HuiXin.Gateway.Ocelot.Extensions +{ + public static class JWTExtensions + { + public static IServiceCollection AddJWT(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(configuration.GetValue("AuthenticationScheme") ?? throw new Exception("jwt的参数AuthenticationScheme未配置,请在jwt.json文件中配置"), options => + { + //options.Authority = cfgJwt.GetValue("Authority"); // OpenIddict服务端地址 + //options.BackchannelTimeout = TimeSpan.FromMilliseconds(300); + options.RequireHttpsMetadata = false; + options.Audience = configuration.GetValue("Audience"); // 与OpenIddict中定义的Audience匹配 + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = false, + IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(configuration.GetValue("IssuerSigningKeyBase64") ?? throw new Exception("jwt的参数IssuerSigningKeyBase64未配置,请在jwt.json文件中配置"))), + ValidateIssuer = false, + //ValidIssuer = "YOUR_ISSUER", + ValidateAudience = false, + //ValidAudience = "YOUR_AUDIENCE", + ValidateLifetime = true, + // 忽略 kid 参数 + ValidateTokenReplay = false, + }; + }); + services.AddAuthorization(); + + return services; + } + + public static IApplicationBuilder UseJWT(this WebApplication app) + { + app.UseAuthentication(); + app.UseAuthorization(); + + return app; + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Extensions/OcelotExtensions.cs b/HuiXin.Gateway.Ocelot/Extensions/OcelotExtensions.cs new file mode 100644 index 0000000..41ec935 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Extensions/OcelotExtensions.cs @@ -0,0 +1,64 @@ +using HuiXin.Gateway.Ocelot.Authorizers; +using HuiXin.Gateway.Ocelot.Configurations; +using HuiXin.Gateway.Ocelot.Middlewares; +using Ocelot.Authorization; +using Ocelot.DependencyInjection; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Middleware; +using Ocelot.Provider.Consul; +using System.Security.Claims; + +namespace HuiXin.Gateway.Ocelot.Extensions +{ + public static class OcelotExtensions + { + public static IServiceCollection AddMyOcelot(this IServiceCollection services, IConfiguration configuration) + { + services.AddOcelot().AddConsul(); + + services.AddSingleton(); + services.Configure(configuration.GetSection("RolesAuthorizer")); + + return services; + } + + public static IApplicationBuilder UseMyOcelot(this WebApplication app) + { + app.UseMiddleware(); + + app.UseOcelot(new OcelotPipelineConfiguration + { + PreQueryStringBuilderMiddleware = async (context, next) => + { + var claimsParser = app.Services.GetRequiredService(); + var values = claimsParser.GetValuesByClaimType(context.User.Claims, ClaimsIdentity.DefaultRoleClaimType); + if (values.IsError) + { + context.Items.UpsertErrors(values.Errors); + return; + } + if (values.Data == null || values.Data.Count == 0) + { + context.Items.SetError(new UserDoesNotHaveClaimError("token中未包含角色信息")); + return; + } + var downstreamRoute = context.Items.DownstreamRoute(); + var url = downstreamRoute.DownstreamPathTemplate.Value; + context.Items.TemplatePlaceholderNameAndValues().ForEach(nv => + { + url = url.Replace(nv.Name, nv.Value); + }); + var rolesAuthorizer = app.Services.GetRequiredService(); + var result = await rolesAuthorizer.Authorize(values.Data, url); + if (result.IsError) + { + context.Items.UpsertErrors(result.Errors); + return; + } + await next.Invoke(); + } + }).Wait(); + return app; + } + } +} diff --git a/HuiXin.Gateway.Ocelot/HuiXin.Gateway.Ocelot.csproj b/HuiXin.Gateway.Ocelot/HuiXin.Gateway.Ocelot.csproj new file mode 100644 index 0000000..772f3f4 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/HuiXin.Gateway.Ocelot.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/HuiXin.Gateway.Ocelot/JWTUtil.cs b/HuiXin.Gateway.Ocelot/JWTUtil.cs new file mode 100644 index 0000000..d03eda4 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/JWTUtil.cs @@ -0,0 +1,64 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace HuiXin.Gateway.Ocelot +{ + public class JWTUtil + { + + /// + /// 创建token + /// + /// + public static string CreateJwtToken(IDictionary payload, string secret, IDictionary extraHeaders = null) + { + //IJwtEncoder encoder = new JwtEncoder(new HMACSHA256Algorithm(), new JsonNetSerializer(), new JwtBase64UrlEncoder()); + //var token = encoder.Encode(extraHeaders,payload, secret); + //return token; + + Claim[] claims = new Claim[] + { + new Claim("Id", "aaa"), + new Claim("Name", "bbb"), + new Claim("Email", "ccc"), + }; + + var securityKey = new SymmetricSecurityKey(Convert.FromBase64String("GcTdqSZdpRxBtdtgwvDHBzS427VGTQzbM+JD1CBbUZY=")); + + var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + + var header = new JwtHeader(signingCredentials); + //header.Add("typ", "JWT"); // 默认情况下,typ通常是"JWT",但你可以明确设置它 + //header.Add("kid", "MyKey"); + //header.Add("key", "MyKey"); + //header.Add("keyid", "MyKey"); + + // 设置JWT载荷(payload) + var payload1 = new JwtPayload + { + //{ "kid", "MyKey" }, + //{ "key", "MyKey" }, + //{ "keyid", "MyKey" }, + { "issuer",""}, + { "sub", "1234567890" }, + { "name", "测试用户" }, + { "role", new []{"test","admin" } }, + { "scope",new []{"all" } }, + { "iat", ToUnixTime(DateTime.Now)}, + { "exp", ToUnixTime(DateTime.Now.AddDays(1)) } + }; + + // 创建JwtSecurityToken实例并结合header和payload + var jwt = new JwtSecurityToken(header, payload1); + return new JwtSecurityTokenHandler().WriteToken(jwt); + } + + private static long ToUnixTime(DateTime dt) + { + DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + return (long)(dt.ToUniversalTime() - epoch).TotalMilliseconds; + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Middlewares/AccessLoggingMiddleware.cs b/HuiXin.Gateway.Ocelot/Middlewares/AccessLoggingMiddleware.cs new file mode 100644 index 0000000..b31143b --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Middlewares/AccessLoggingMiddleware.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Ocelot.RequestId; +using Serilog; +using System.Diagnostics; +using System.Text; +using Yitter.IdGenerator; + +namespace HuiXin.Gateway.Ocelot.Middlewares +{ + public class AccessLoggingMiddleware + { + private readonly RequestDelegate _next; + + public AccessLoggingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var traceIdKey = DefaultRequestIdKey.Value; + var traceId = YitIdHelper.NextId().ToString(); + context.TraceIdentifier = traceId; + context.Request.Headers[traceIdKey] = traceId; + + var sw = Stopwatch.StartNew(); + sw.Start(); + + var sb = new StringBuilder(); + + sb.AppendLine(); + sb.AppendLine("-------------------Request Start-------------------"); + sb.AppendLine(DateTime.Now.ToString()); + sb.AppendLine($"追踪id:{traceId}"); + sb.AppendLine($"地址:{context.Request.GetDisplayUrl()}"); + sb.AppendLine("请求头:"); + foreach (var header in context.Request.Headers) + { + sb.AppendLine($"{header.Key}:{header.Value}"); + } + sb.AppendLine($"数据:{await GetRequestBody(context.Request)}"); + sb.AppendLine("===================Request End==================="); + + Log.Information(sb.ToString()); + sb.Clear(); + + //Copy a pointer to the original response body stream + var originalBodyStream = context.Response.Body; + + //Create a new memory stream... + using var responseBody = new MemoryStream(); + //...and use that for the temporary response body + context.Response.Body = responseBody; + + //Continue down the Middleware pipeline, eventually returning to this class + await _next(context); + + //Format the response from the server + //var response = await FormatResponse(context.Response); + sb.AppendLine(); + sb.AppendLine("-------------------Response Start-------------------"); + sb.AppendLine(DateTime.Now.ToString()); + sb.AppendLine($"追踪id:{traceId}"); + sb.AppendLine($"耗时:{sw.ElapsedMilliseconds}毫秒"); + sb.AppendLine($"状态码:{context.Response.StatusCode}"); + //sb.AppendLine("Headers: "); + //foreach (var header in context.Response.Headers) + //{ + // sb.AppendLine($"{header.Key}:{header.Value}"); + //} + sb.AppendLine($"数据:{await GetResponseBody(context.Response)}"); + sb.AppendLine("===================Response End==================="); + + //Save log to chosen datastore + Log.Information(sb.ToString()); + + sb.Clear(); + + //Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client. + await responseBody.CopyToAsync(originalBodyStream); + } + + private static async Task GetRequestBody(HttpRequest request) + { + request.EnableBuffering(); + // Leave the body open so the next middleware can read it. + using var reader = new StreamReader( + request.Body, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + leaveOpen: true); + var body = await reader.ReadToEndAsync(); + // Do some processing with body… + + //var formattedRequest = $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {body}"; + + // Reset the request body stream position so the next middleware can read it + request.Body.Position = 0; + + return body; + } + + private static async Task GetResponseBody(HttpResponse response) + { + //We need to read the response stream from the beginning... + response.Body.Seek(0, SeekOrigin.Begin); + + //...and copy it into a string + string text = await new StreamReader(response.Body).ReadToEndAsync(); + + //We need to reset the reader for the response so that the client can read it. + response.Body.Seek(0, SeekOrigin.Begin); + + //Return the string for the response, including the status code (e.g. 200, 404, 401, etc.) + //return $"{response.StatusCode}: {text}"; + return text; + } + + static AccessLoggingMiddleware() + { + // 创建 IdGeneratorOptions 对象,可在构造函数中输入 WorkerId: + var options = new IdGeneratorOptions(); + // options.WorkerIdBitLength = 10; // 默认值6,限定 WorkerId 最大值为2^6-1,即默认最多支持64个节点。 + // options.SeqBitLength = 6; // 默认值6,限制每毫秒生成的ID个数。若生成速度超过5万个/秒,建议加大 SeqBitLength 到 10。 + // options.BaseTime = Your_Base_Time; // 如果要兼容老系统的雪花算法,此处应设置为老系统的BaseTime。 + // ...... 其它参数参考 IdGeneratorOptions 定义。 + + // 保存参数(务必调用,否则参数设置不生效): + YitIdHelper.SetIdGenerator(options); + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Middlewares/MyAuthorizationMiddleware.cs b/HuiXin.Gateway.Ocelot/Middlewares/MyAuthorizationMiddleware.cs new file mode 100644 index 0000000..5783714 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Middlewares/MyAuthorizationMiddleware.cs @@ -0,0 +1,34 @@ +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Responses; + +namespace HuiXin.Gateway.Ocelot.Middlewares +{ + public class MyAuthorizationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IClaimsParser _claimsParser; + + public MyAuthorizationMiddleware(RequestDelegate next, + IClaimsParser claimsParser, + IOcelotLoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _claimsParser = claimsParser; + } + + public async Task Invoke(HttpContext httpContext) + { + Authorize(httpContext); + await _next.Invoke(httpContext); + } + + public Response Authorize(HttpContext httpContext) + { + var values = _claimsParser.GetValuesByClaimType(httpContext.User.Claims, "role"); + return new OkResponse(true); + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Program.cs b/HuiXin.Gateway.Ocelot/Program.cs new file mode 100644 index 0000000..8b5ddbd --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Program.cs @@ -0,0 +1,78 @@ +using HuiXin.Gateway.Ocelot.Extensions; +using Serilog; +using System.Security.Cryptography; + +namespace HuiXin.Gateway.Ocelot +{ + public class Program + { + public static void Main(string[] args) + { + //BuildJwt(); + + var configuration = new ConfigurationBuilder() + .AddJsonFile("Configs/ocelot.json", false, true) + .AddJsonFile("Configs/jwt.json", false, false) + .AddJsonFile("Configs/serilog.json", false, true) + .Build(); + + var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + Log.Logger = logger; + + try + { + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddConfiguration(configuration); + + var services = builder.Services; + + services.AddCors(); + services.AddLogging(loggingBuilder => loggingBuilder.ClearProviders().AddSerilog()); + + services.AddJWT(configuration); + + services.AddMyOcelot(configuration); + + var app = builder.Build(); + + app.UseCors(); + app.UseJWT(); + + app.UseMyOcelot(); + + app.Run(); + + Log.Information("ϵͳ"); + } + catch (Exception ex) + { + Log.Fatal(ex, "ϵͳ쳣"); + } + finally + { + Log.CloseAndFlush(); + } + } + + static void BuildJwt() + { + using var aes = Aes.Create(); + aes.KeySize = 256; + aes.GenerateKey(); + var sign = Convert.ToBase64String(aes.Key); + var extraHeaders = new Dictionary { { "kid", "MyKey" } }; + //ʱ(Բãʾǩ 10) + double exp = (DateTime.UtcNow.AddDays(10) - new DateTime(1970, 1, 1)).TotalSeconds; + var payload = new Dictionary + { + { "userId", "001" }, + { "userAccount", "fan" }, + { "exp",exp } + }; + Console.WriteLine("Token" + JWTUtil.CreateJwtToken(payload, sign, extraHeaders)); + } + } +} diff --git a/HuiXin.Gateway.Ocelot/Properties/launchSettings.json b/HuiXin.Gateway.Ocelot/Properties/launchSettings.json new file mode 100644 index 0000000..a193aa3 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21406", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "launchUrl": "user/getById", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/HuiXin.Gateway.Ocelot/appsettings.Development.json b/HuiXin.Gateway.Ocelot/appsettings.Development.json new file mode 100644 index 0000000..8634d29 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Ocelot": "Error" + } + } +} diff --git a/HuiXin.Gateway.Ocelot/appsettings.json b/HuiXin.Gateway.Ocelot/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/HuiXin.Gateway.Ocelot/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/HuiXin.Gateway.Yarp/HuiXin.Gateway.Yarp.csproj b/HuiXin.Gateway.Yarp/HuiXin.Gateway.Yarp.csproj new file mode 100644 index 0000000..42601e2 --- /dev/null +++ b/HuiXin.Gateway.Yarp/HuiXin.Gateway.Yarp.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/HuiXin.Gateway.Yarp/Program.cs b/HuiXin.Gateway.Yarp/Program.cs new file mode 100644 index 0000000..5f60e52 --- /dev/null +++ b/HuiXin.Gateway.Yarp/Program.cs @@ -0,0 +1,15 @@ +namespace HuiXin.Gateway.Yarp +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + + app.MapGet("/", () => "Hello World!"); + + app.Run(); + } + } +} diff --git a/HuiXin.Gateway.Yarp/Properties/launchSettings.json b/HuiXin.Gateway.Yarp/Properties/launchSettings.json new file mode 100644 index 0000000..2295992 --- /dev/null +++ b/HuiXin.Gateway.Yarp/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:17782", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5167", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/HuiXin.Gateway.Yarp/appsettings.Development.json b/HuiXin.Gateway.Yarp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/HuiXin.Gateway.Yarp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/HuiXin.Gateway.Yarp/appsettings.json b/HuiXin.Gateway.Yarp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/HuiXin.Gateway.Yarp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/HuiXin.Gateway.sln b/HuiXin.Gateway.sln new file mode 100644 index 0000000..72b41ba --- /dev/null +++ b/HuiXin.Gateway.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuiXin.Gateway.Ocelot", "HuiXin.Gateway.Ocelot\HuiXin.Gateway.Ocelot.csproj", "{2E2162C5-FC45-478B-83D4-14148477D627}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuiXin.Identity.OpenIddict", "HuiXin.Identity.OpenIddict\HuiXin.Identity.OpenIddict.csproj", "{819D01DC-9EFE-427E-B73E-9022DC7843EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuiXin.Gateway.Yarp", "HuiXin.Gateway.Yarp\HuiXin.Gateway.Yarp.csproj", "{55A12844-A4CE-407A-BD7C-94469AA407F0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2E2162C5-FC45-478B-83D4-14148477D627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E2162C5-FC45-478B-83D4-14148477D627}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E2162C5-FC45-478B-83D4-14148477D627}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E2162C5-FC45-478B-83D4-14148477D627}.Release|Any CPU.Build.0 = Release|Any CPU + {819D01DC-9EFE-427E-B73E-9022DC7843EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {819D01DC-9EFE-427E-B73E-9022DC7843EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {819D01DC-9EFE-427E-B73E-9022DC7843EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {819D01DC-9EFE-427E-B73E-9022DC7843EC}.Release|Any CPU.Build.0 = Release|Any CPU + {55A12844-A4CE-407A-BD7C-94469AA407F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55A12844-A4CE-407A-BD7C-94469AA407F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55A12844-A4CE-407A-BD7C-94469AA407F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55A12844-A4CE-407A-BD7C-94469AA407F0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8F9B214D-FFA3-4E2C-8103-6A92331507EA} + EndGlobalSection +EndGlobal diff --git a/HuiXin.Identity.OpenIddict/ApplicationDbContext.cs b/HuiXin.Identity.OpenIddict/ApplicationDbContext.cs new file mode 100644 index 0000000..6c6045c --- /dev/null +++ b/HuiXin.Identity.OpenIddict/ApplicationDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; + +namespace HuiXin.Identity.OpenIddict +{ + public class ApplicationDbContext : DbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/HuiXin.Identity.OpenIddict/Controllers/AuthorizationController.cs b/HuiXin.Identity.OpenIddict/Controllers/AuthorizationController.cs new file mode 100644 index 0000000..dfe4f88 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/Controllers/AuthorizationController.cs @@ -0,0 +1,357 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using System.Collections.Immutable; +using System.Security.Claims; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace HuiXin.Identity.OpenIddict.Controllers +{ + [ApiController] + [Route("auth")] + public class AuthorizationController : Controller + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictScopeManager _scopeManager; + + public AuthorizationController(IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager) + { + _applicationManager = applicationManager; + _scopeManager = scopeManager; + _authorizationManager = authorizationManager; + } + + /// + /// 登录权限校验 + /// + /// + /// + /// + [HttpGet("connect/authorize")] + [IgnoreAntiforgeryToken] + public async Task Authorize() + { + // var s=await _schemeProvider.GetAllSchemesAsync(); + + //通过扩展的获取自定义的参数校验 + var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("未获取到相关认证情况"); + + #region 存在登录凭证且明确了登录请求的行为 + // 存在登录凭证且明确了登录请求的行为 + if (request.HasPrompt(Prompts.Login)) + { + //这里有个小坑,在Challenge之前必须把这个行为去掉 不然 Challenge 进入 /connect/authorize 路由陷入死循环 + var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login)); + var parameters = Request.HasFormContentType ? + Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : + Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); + + parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt))); + + return Challenge( + authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, // IdentityConstants.ApplicationScheme, + properties: new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) + }); + } + #endregion + //检索本地的Cookies信息 确定重定向页面 这里都是UTC时间来设置的过期情况 这里没有用Identity 所以这里可以指定自己的应用名称 + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); //IdentityConstants.ApplicationScheme + #region 未获取本地Cookies信息或者 cookie过期的情况 + if (request == null || !result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null && + DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) + { + //是否是无效授权 + if (request.HasPrompt(Prompts.None)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户未登录." + })); + } + return Challenge( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create( + Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) + }); + } + #endregion + + + //var resultdata = _userService.GetLoginInfo(new Guid(result.Principal.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在"))) ?? throw new Exception("用户详细信息不存在"); + var resultdata = new UserInfo() + { + Id = "user1", + UserName = "测试用户名", + NickName = "测试昵称", + }; + + // 获取客户端详细信息 验证其他数据 + var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ?? throw new InvalidOperationException("未查找到该客户端的应用详细信息"); + + //查找当前情况客户端下请求用户的持久化授权数据信息 + var authorizations = _authorizationManager.FindAsync( + subject: resultdata.Id.ToString(), + client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("没有找到客户端的应用信息"), //这里区分下 是application的Id而不是ClientId + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: request.GetScopes() + ).ToBlockingEnumerable();//.ToListAsync(); + + var consenttype = await _applicationManager.GetConsentTypeAsync(application); + //获取授权同意确认页面 + switch (consenttype) + { + //判断授权同意的类型 + + //1 外部允许的且没有任何授权项 + case ConsentTypes.External when !authorizations.Any(): + return Forbid( + authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "登录用户没用访问该客户端应用的权限" + })); + + // 隐式、外部授权、显示模式模式 + case ConsentTypes.Implicit: + case ConsentTypes.External when authorizations.Any(): + case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent): + + ClaimsPrincipal principal = CreateUserPrincpal(resultdata); + //设置请求的范围 + principal.SetScopes(request.GetScopes()); + + //查找scope允许访问的资源 + var resources = _scopeManager.ListResourcesAsync(principal.GetScopes()).ToBlockingEnumerable(); + //通过扩展设置不同的资源访问 其实本质都是设置Claims 只是 key 在 scope以及Resource上不同 + //Resource = "oi_rsrc"; + // Scope = "oi_scp"; + + principal.SetResources(resources); + + // 自动创建一个永久授权,以避免需要明确的同意 用于包含相同范围的未来授权或令牌请求 + var authorization = authorizations.LastOrDefault(); + if (authorization is null) + { + authorization = await _authorizationManager.CreateAsync( + principal: principal, + subject: resultdata.Id.ToString(), + client: await _applicationManager.GetIdAsync(application), + type: AuthorizationTypes.Permanent, + scopes: principal.GetScopes()); + } + + principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } + //登录 OpenIddict签发令牌 + return SignIn(principal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // At this point, no authorization was found in the database and an error must be returned + // if the client application specified prompt=none in the authorization request. + case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): + case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): + return Forbid( + authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "Interactive user consent is required." + })); + + // In every other case, render the consent form. + //default: + // return View(new AuthorizeViewModel + // { + // ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), + // Scope = request.Scope + // }); + default: + return Challenge( + authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + properties: new AuthenticationProperties { RedirectUri = "/" } + ); + } + } + + + [HttpPost("connect/token"), Produces("application/json")] + public async Task ConnectToken() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OIDC请求不存在"); + + if (request.IsClientCredentialsGrantType()) + { + // Note: the client credentials are automatically validated by OpenIddict: + // if client_id or client_secret are invalid, this action won't be invoked. + + var application = await _applicationManager.FindByClientIdAsync(request.ClientId); + if (application is null) + { + Console.WriteLine($"client_id未注册:{request.ClientId}"); + throw new InvalidOperationException("The application cannot be found."); + } + Console.WriteLine($"获取到客户信息:{JsonConvert.SerializeObject(application)}"); + + // Create a new ClaimsIdentity containing the claims that + // will be used to create an id_token, a token or a code. + var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); + + // Use the client_id as the subject identifier. + identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application)); + identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application)); + + identity.SetDestinations(static claim => claim.Type switch + { + // Allow the "name" claim to be stored in both the access and identity tokens + // when the "profile" scope was granted (by calling principal.SetScopes(...)). + Claims.Name when claim.Subject.HasScope(Scopes.Profile) + => new[] { Destinations.AccessToken, Destinations.IdentityToken }, + + // Otherwise, only store the claim in the access tokens. + _ => new[] { Destinations.AccessToken } + }); + //Console.WriteLine($"identity信息:{JsonConvert.SerializeObject(identity)}"); + + var result = SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + //Console.WriteLine($"signIn信息:{JsonConvert.SerializeObject(result)}"); + return result; + } + else if (request.IsPasswordGrantType()) + { + //数据库获取用户信息 + var princpal = CreateUserPrincpal(new UserInfo() + { + Id = Guid.NewGuid().ToString(), + UserName = "测试用户名", + NickName = "测试昵称", + }); + //princpal.SetResources(_scopeManager.ListResourcesAsync(princpal.GetScopes())); + + foreach (var claim in princpal.Claims) + { + claim.SetDestinations(GetDestinations(claim, princpal)); + } + return SignIn(princpal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType()) + { + var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; + //var user = _userService.GetLoginInfo(new Guid(principal?.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在"))); + //UserInfo user = null; + //if (user == null) + //{ + // return Forbid( + // authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + // properties: new AuthenticationProperties(new Dictionary + // { + // [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + // [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "令牌已失效" + // }) + // ); + //} + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + throw new InvalidOperationException($"不支持的grant_type:{request.GrantType}"); + } + + [HttpPost("connect/logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(); + return SignOut( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = "/" + }); + } + + public static ClaimsPrincipal CreateUserPrincpal(UserInfo user, string claimsIdentityName = "USERINFO") + { + //登录成功流程 + ClaimsIdentity identity = new ClaimsIdentity(claimsIdentityName); + identity.AddClaim(new Claim(Claims.Subject, user.Id)); + identity.AddClaim(new Claim(Claims.Name, user.UserName)); + identity.AddClaim(new Claim(Claims.Nickname, user.NickName)); + return new ClaimsPrincipal(identity); + } + + private IEnumerable GetDestinations(Claim claim) + { + // Note: by default, claims are NOT automatically included in the access and identity tokens. + // To allow OpenIddict to serialize them, you must attach them a destination, that specifies + // whether they should be included in access tokens, in identity tokens or in both. + + return claim.Type switch + { + Claims.Name or + Claims.Subject + => ImmutableArray.Create(Destinations.AccessToken, Destinations.IdentityToken), + + _ => ImmutableArray.Create(Destinations.AccessToken), + }; + } + + private IEnumerable GetDestinations(Claim claim, ClaimsPrincipal principal) + { + + switch (claim.Type) + { + case Claims.Name: + yield return Destinations.AccessToken; + + if (principal.HasScope(Scopes.Profile)) + yield return Destinations.IdentityToken; + + yield break; + + case Claims.Email: + yield return Destinations.AccessToken; + + if (principal.HasScope(Scopes.Email)) + yield return Destinations.IdentityToken; + + yield break; + + case Claims.Role: + yield return Destinations.AccessToken; + + if (principal.HasScope(Scopes.Roles)) + yield return Destinations.IdentityToken; + + yield break; + + + case "AspNet.Identity.SecurityStamp": yield break; + + default: + yield return Destinations.AccessToken; + yield break; + } + } + } +} diff --git a/HuiXin.Identity.OpenIddict/Controllers/UserController.cs b/HuiXin.Identity.OpenIddict/Controllers/UserController.cs new file mode 100644 index 0000000..5af4cc8 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/Controllers/UserController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Abstractions; + +namespace HuiXin.Identity.OpenIddict.Controllers +{ + [ApiController] + [Route("user")] + public class UserController : Controller + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictScopeManager _scopeManager; + + public UserController(IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager) + { + _applicationManager = applicationManager; + _scopeManager = scopeManager; + _authorizationManager = authorizationManager; + } + } +} diff --git a/HuiXin.Identity.OpenIddict/HuiXin.Identity.OpenIddict.csproj b/HuiXin.Identity.OpenIddict/HuiXin.Identity.OpenIddict.csproj new file mode 100644 index 0000000..0cc254f --- /dev/null +++ b/HuiXin.Identity.OpenIddict/HuiXin.Identity.OpenIddict.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/HuiXin.Identity.OpenIddict/Program.cs b/HuiXin.Identity.OpenIddict/Program.cs new file mode 100644 index 0000000..67c6563 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/Program.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Validation.AspNetCore; +using System.Text; + +namespace HuiXin.Identity.OpenIddict +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddCors(); + builder.Services.AddControllers(); + + builder.Services + .AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme) + .AddCookie(); + + builder.Services.AddDbContext(options => + { + //options.UseSqlite("DataSource=:memory:"); + options.UseMySQL("server=8.134.236.110;port=13306;database=openiddict;user=test;password=test") + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + + // Register the entity sets needed by OpenIddict. + // Note: use the generic overload if you need to replace the default OpenIddict entities. + options.UseOpenIddict(); + }); + + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + builder.Services + .AddOpenIddict() + // Register the OpenIddict core components. + .AddCore(options => + { + // Configure OpenIddict to use the Entity Framework Core stores and models. + // Note: call ReplaceDefaultEntities() to replace the default entities. + options.UseEntityFrameworkCore() + .UseDbContext(); + }) + // Register the OpenIddict server components. + .AddServer(options => + { + options.SetAccessTokenLifetime(TimeSpan.FromMinutes(5)); + options.SetIdentityTokenLifetime(TimeSpan.FromMinutes(5)); + options.SetRefreshTokenLifetime(TimeSpan.FromDays(365 * 100)); + + // Enable the token endpoint. + options.SetTokenEndpointUris("auth/connect/token"); + options.SetAuthorizationEndpointUris("auth/connect/authorize"); + options.SetUserinfoEndpointUris("auth/connect/userinfo"); + options.SetLogoutEndpointUris("auth/connect/logout"); + + // Enable the client credentials flow. + options.AllowClientCredentialsFlow(); + options.AllowAuthorizationCodeFlow(); + options.AllowPasswordFlow(); + options.AllowRefreshTokenFlow(); + + // Register the signing and encryption credentials. + options.AddEncryptionKey(new SymmetricSecurityKey(Convert.FromBase64String("GcTdqSZdpRxBtdtgwvDHBzS427VGTQzbM+JD1CBbUZY="))); + options.AddDevelopmentSigningCertificate(); + + // Register the ASP.NET Core host and configure the ASP.NET Core options. + options.UseAspNetCore() + .EnableTokenEndpointPassthrough() + .EnableAuthorizationEndpointPassthrough() + .EnableUserinfoEndpointPassthrough() + .EnableLogoutEndpointPassthrough() + .DisableTransportSecurityRequirement(); + + options.IgnoreEndpointPermissions(); + options.IgnoreGrantTypePermissions(); + options.IgnoreScopePermissions(); + options.IgnoreResponseTypePermissions(); + }) + // Register the OpenIddict validation components. + .AddValidation(options => + { + // Import the configuration from the local OpenIddict server instance. + options.UseLocalServer(); + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }); + builder.Services.AddHostedService(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + + //app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/HuiXin.Identity.OpenIddict/Properties/launchSettings.json b/HuiXin.Identity.OpenIddict/Properties/launchSettings.json new file mode 100644 index 0000000..5424a9d --- /dev/null +++ b/HuiXin.Identity.OpenIddict/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61533", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/HuiXin.Identity.OpenIddict/UserInfo.cs b/HuiXin.Identity.OpenIddict/UserInfo.cs new file mode 100644 index 0000000..fb07813 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/UserInfo.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace HuiXin.Identity.OpenIddict +{ + public class UserInfo : IdentityUser + { + public string Id { get; set; } + public string UserName { get; set; } + public string NickName { get; set; } + } +} \ No newline at end of file diff --git a/HuiXin.Identity.OpenIddict/Worker.cs b/HuiXin.Identity.OpenIddict/Worker.cs new file mode 100644 index 0000000..44b9912 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/Worker.cs @@ -0,0 +1,41 @@ +using OpenIddict.Abstractions; + +namespace HuiXin.Identity.OpenIddict +{ + public class Worker : IHostedService + { + private readonly IServiceProvider _serviceProvider; + + public Worker(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + public async Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _serviceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(cancellationToken); + + var manager = scope.ServiceProvider.GetRequiredService(); + //var authManager = scope.ServiceProvider.GetRequiredService(); + + var data = await manager.FindByClientIdAsync("apisix", cancellationToken) ?? await manager.CreateAsync( + new OpenIddictApplicationDescriptor + { + ClientId = "apisix", + ClientSecret = "388D45FA-B36B-4988-BA59-B187D329C207", + DisplayName = "APISIX的测试客户", + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Logout, + OpenIddictConstants.Permissions.Endpoints.Token + }, + RedirectUris = { new Uri("http://8.134.191.93:9080/test/callback")}, + }, cancellationToken); + //await manager.DeleteAsync(data); + Console.WriteLine($"APISIX的测试客户:{System.Text.Json.JsonSerializer.Serialize(data)}"); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/HuiXin.Identity.OpenIddict/appsettings.Development.json b/HuiXin.Identity.OpenIddict/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/HuiXin.Identity.OpenIddict/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/HuiXin.Identity.OpenIddict/appsettings.json b/HuiXin.Identity.OpenIddict/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/HuiXin.Identity.OpenIddict/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}