From 41bc72d53b418755cac44a953040fdf76c9b0375 Mon Sep 17 00:00:00 2001 From: Young Date: Wed, 9 Oct 2024 15:22:47 +0800 Subject: [PATCH] added jwt bearer configuration --- .../Extensions/AuthenticationSetup.cs | 73 +++++++++++++++++++ .../Security/DefaultAuthenticationHandler.cs | 9 +-- .../Security/IEncryptionService.cs | 2 - src/Infrastructure/Security/ITokenBuilder.cs | 45 ++++++++++++ src/Infrastructure/Security/IUserContext.cs | 31 ++++++++ .../JwtBearerOptionsPostConfigureOptions.cs | 19 +++++ 6 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/Infrastructure/Extensions/AuthenticationSetup.cs create mode 100644 src/Infrastructure/Security/ITokenBuilder.cs create mode 100644 src/Infrastructure/Security/IUserContext.cs create mode 100644 src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs diff --git a/src/Infrastructure/Extensions/AuthenticationSetup.cs b/src/Infrastructure/Extensions/AuthenticationSetup.cs new file mode 100644 index 0000000..c472388 --- /dev/null +++ b/src/Infrastructure/Extensions/AuthenticationSetup.cs @@ -0,0 +1,73 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Infrastructure.Options; +using Infrastructure.Security; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace Infrastructure.Extensions; + +public static class AuthenticationSetup +{ + /// + /// 配置认证服务,包含jwt认证 + /// + /// + /// + /// 自定义认证服务配置 + /// + public static IServiceCollection AddDefaultAuthentication( + this IServiceCollection services, + IConfiguration configuration, + Action? builderOptions = null) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + var audienceOptions = configuration.GetSection(AudienceOptions.Name).Get(); + if (audienceOptions is null || !audienceOptions.IsEnable) + { + return services; + } + + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton, JwtBearerOptionsPostConfigureOptions>(); + services.AddSingleton(); + + var key = configuration["AUDIENCE_KEY"] ?? audienceOptions.Secret; + ArgumentException.ThrowIfNullOrEmpty(key); + var buffer = Encoding.UTF8.GetBytes(key); + var securityKey = new SymmetricSecurityKey(buffer); + + var tokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = securityKey, + ValidIssuer = audienceOptions.Issuer, + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = audienceOptions.Audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromSeconds(300), + RequireExpirationTime = true, + RoleClaimType = ClaimTypes.Role + }; + + var builder = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = nameof(DefaultAuthenticationHandler); + options.DefaultForbidScheme = nameof(DefaultAuthenticationHandler); + }); + builder.AddScheme( + nameof(DefaultAuthenticationHandler), + options => { }); + builder.AddJwtBearer(options => { options.TokenValidationParameters = tokenValidationParameters; }); + builderOptions?.Invoke(builder); + return services; + } +} \ No newline at end of file diff --git a/src/Infrastructure/Security/DefaultAuthenticationHandler.cs b/src/Infrastructure/Security/DefaultAuthenticationHandler.cs index 3313bb9..7202829 100644 --- a/src/Infrastructure/Security/DefaultAuthenticationHandler.cs +++ b/src/Infrastructure/Security/DefaultAuthenticationHandler.cs @@ -2,8 +2,6 @@ using System.Text.Encodings.Web; using Infrastructure.Utils; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Infrastructure.Security; @@ -14,6 +12,7 @@ public class DefaultAuthenticationHandler( : AuthenticationHandler(options, logger, encoder) { private const string Message = "You are not authorized to access this resource"; + private const string ContentType = "application/json"; protected override Task HandleAuthenticateAsync() { @@ -22,7 +21,7 @@ public class DefaultAuthenticationHandler( protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) { - Response.ContentType = "application/json"; + Response.ContentType = ContentType; Response.StatusCode = StatusCodes.Status200OK; await Response.WriteAsync(new MessageData(false, Message, 403) .Serialize()); @@ -30,9 +29,9 @@ public class DefaultAuthenticationHandler( protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - Response.ContentType = "application/json"; + Response.ContentType = ContentType; Response.StatusCode = StatusCodes.Status200OK; - await Response.WriteAsync(new MessageData(false,Message, 401) + await Response.WriteAsync(new MessageData(false, Message, 401) .Serialize()); } } \ No newline at end of file diff --git a/src/Infrastructure/Security/IEncryptionService.cs b/src/Infrastructure/Security/IEncryptionService.cs index 9eeda16..fa55a99 100644 --- a/src/Infrastructure/Security/IEncryptionService.cs +++ b/src/Infrastructure/Security/IEncryptionService.cs @@ -1,5 +1,3 @@ -using System.Text; -using Microsoft.Extensions.Configuration; using System.Security.Cryptography; namespace Infrastructure.Security; diff --git a/src/Infrastructure/Security/ITokenBuilder.cs b/src/Infrastructure/Security/ITokenBuilder.cs new file mode 100644 index 0000000..8711f51 --- /dev/null +++ b/src/Infrastructure/Security/ITokenBuilder.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using Infrastructure.Utils; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.JsonWebTokens; + +namespace Infrastructure.Security; + +public interface ITokenBuilder +{ + IList GetClaimsByUserContext(IUserContext userContext); + + void SetUserContext(TokenValidatedContext context); +} + +public class TokenBuilder : ITokenBuilder +{ + public IList GetClaimsByUserContext(IUserContext userContext) + { + var claims = new List() + { + new(JwtRegisteredClaimNames.UniqueName, userContext.Username), + new(JwtRegisteredClaimNames.NameId, userContext.Id.ToString()), + new(JwtRegisteredClaimNames.Name, userContext.Name), + new(JwtRegisteredClaimNames.Email, userContext.Email), + }; + claims.AddRange(userContext.RoleIds.Select(rId => new Claim(ClaimTypes.Role, rId))); + return claims; + } + + public void SetUserContext(TokenValidatedContext context) + { + var userContext = context.HttpContext.RequestServices.GetService() ?? + throw new NullReferenceException(nameof(IUserContext)); + var principal = context.Principal ?? throw new NullReferenceException(nameof(context.Principal)); + userContext.Id = long.Parse( + principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.NameId)?.Value); + userContext.Username = + principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.UniqueName)?.Value!; + userContext.Name = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Name)?.Value!; + userContext.Email = principal.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Email)?.Value!; + userContext.RoleIds = principal.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray(); + userContext.RemoteIpAddress = context.HttpContext.GetRequestIp(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Security/IUserContext.cs b/src/Infrastructure/Security/IUserContext.cs new file mode 100644 index 0000000..d9bdff6 --- /dev/null +++ b/src/Infrastructure/Security/IUserContext.cs @@ -0,0 +1,31 @@ +namespace Infrastructure.Security; + +public interface IUserContext +{ + long Id { get; set; } + + string Username { get; set; } + + string Name { get; set; } + + string Email { get; set; } + + string[] RoleIds { get; set; } + + string RemoteIpAddress { get; set; } +} + +public class UserContext : IUserContext +{ + public long Id { get; set; } + + public string Username { get; set; } + + public string Name { get; set; } + + public string Email { get; set; } + + public string[] RoleIds { get; set; } + + public string RemoteIpAddress { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs b/src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs new file mode 100644 index 0000000..f0e161b --- /dev/null +++ b/src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace Infrastructure.Security; + +public class JwtBearerOptionsPostConfigureOptions( + DefaultTokenHandler tokenHandler, + ITokenBuilder tokenBuilder) + : IPostConfigureOptions +{ + public void PostConfigure(string? name, JwtBearerOptions options) + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(tokenHandler); + options.Events.OnTokenValidated = context => + { + return Task.Run(() => { tokenBuilder.SetUserContext(context); }); + }; + } +} \ No newline at end of file