From d14e6de1f27a1aa8fa9e83c046e942fc0c062c78 Mon Sep 17 00:00:00 2001 From: Young Date: Mon, 14 Oct 2024 22:59:40 +0800 Subject: [PATCH] optimise user context feature --- .../Extensions/TokenContextSetup.cs | 1 - .../Extensions/UserContextSetup.cs | 7 +- .../HttpUserContext/IUserContext.cs | 22 ++++++ .../HttpUserContext/UserContext.cs | 69 ++++++++++++++++++ .../Security/DefaultTokenBuilder.cs | 9 --- .../Security/DefaultUserContext.cs | 5 -- src/Infrastructure/Security/ITokenBuilder.cs | 70 ------------------- src/Infrastructure/Security/IUserContext.cs | 31 -------- .../JwtBearerOptionsPostConfigureOptions.cs | 7 +- 9 files changed, 97 insertions(+), 124 deletions(-) create mode 100644 src/Infrastructure/HttpUserContext/IUserContext.cs create mode 100644 src/Infrastructure/HttpUserContext/UserContext.cs delete mode 100644 src/Infrastructure/Security/DefaultTokenBuilder.cs delete mode 100644 src/Infrastructure/Security/DefaultUserContext.cs delete mode 100644 src/Infrastructure/Security/ITokenBuilder.cs delete mode 100644 src/Infrastructure/Security/IUserContext.cs diff --git a/src/Infrastructure/Extensions/TokenContextSetup.cs b/src/Infrastructure/Extensions/TokenContextSetup.cs index e723a97..1f305f4 100644 --- a/src/Infrastructure/Extensions/TokenContextSetup.cs +++ b/src/Infrastructure/Extensions/TokenContextSetup.cs @@ -7,7 +7,6 @@ public static class TokenContextSetup ArgumentNullException.ThrowIfNull(services); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); return services; } } \ No newline at end of file diff --git a/src/Infrastructure/Extensions/UserContextSetup.cs b/src/Infrastructure/Extensions/UserContextSetup.cs index 7ed4b10..37e6b97 100644 --- a/src/Infrastructure/Extensions/UserContextSetup.cs +++ b/src/Infrastructure/Extensions/UserContextSetup.cs @@ -1,11 +1,14 @@ +using Infrastructure.HttpUserContext; + namespace Infrastructure.Extensions; public static class UserContextSetup { - public static IServiceCollection AddDefaultUserContext(this IServiceCollection services) + public static IServiceCollection AddDefaultUserContext(this IServiceCollection services) where T : IEquatable { ArgumentNullException.ThrowIfNull(services); - services.TryAddScoped(typeof(IUserContext<>), typeof(DefaultUserContext)); + services.AddHttpContextAccessor(); + services.TryAddScoped(typeof(IUserContext), typeof(UserContext)); return services; } } \ No newline at end of file diff --git a/src/Infrastructure/HttpUserContext/IUserContext.cs b/src/Infrastructure/HttpUserContext/IUserContext.cs new file mode 100644 index 0000000..fcd9ba8 --- /dev/null +++ b/src/Infrastructure/HttpUserContext/IUserContext.cs @@ -0,0 +1,22 @@ +using System.Security.Claims; + +namespace Infrastructure.HttpUserContext; + +public interface IUserContext where TId : IEquatable +{ + TId Id { get; } + + string Username { get; } + + string Name { get; } + + string Email { get; } + + string[] RoleIds { get; } + + string RemoteIpAddress { get; } + + JwtTokenInfo GenerateTokenInfo(); + + IList GetClaimsFromUserContext(); +} \ No newline at end of file diff --git a/src/Infrastructure/HttpUserContext/UserContext.cs b/src/Infrastructure/HttpUserContext/UserContext.cs new file mode 100644 index 0000000..65536f3 --- /dev/null +++ b/src/Infrastructure/HttpUserContext/UserContext.cs @@ -0,0 +1,69 @@ +using System.Globalization; +using System.Security.Claims; +using Infrastructure.Utils; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Tokens; + +namespace Infrastructure.HttpUserContext; + +public class UserContext( + IHttpContextAccessor httpContextAccessor, + JwtOptions jwtOptions, + IEncryptionService encryptionService, + JwtSecurityTokenHandler jwtSecurityTokenHandler) + : IUserContext where TId : IEquatable +{ + private readonly ClaimsPrincipal principal = httpContextAccessor?.HttpContext?.User; + + public TId Id => GetIdFromClaims(); + + public string Username => principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.UniqueName).Value; + + public string Name => principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.Name).Value; + + public string Email => principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.Email).Value; + + public string[] RoleIds => principal.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray(); + + public string RemoteIpAddress => httpContextAccessor.HttpContext?.GetRequestIp()!; + + public JwtTokenInfo GenerateTokenInfo() + { + var claims = GetClaimsFromUserContext(); + var jwtToken = new JwtSecurityToken( + issuer: jwtOptions.Issuer, + audience: jwtOptions.Audience, + claims: claims, + notBefore: DateTime.Now, + expires: DateTime.Now.AddSeconds(jwtOptions.Expiration), + signingCredentials: jwtOptions.SigningCredentials); + var token = jwtSecurityTokenHandler.WriteToken(jwtToken); + token = encryptionService.Encrypt(token); + return new JwtTokenInfo(token, jwtOptions.Expiration, + JwtBearerDefaults.AuthenticationScheme); + } + + public IList GetClaimsFromUserContext() + { + var claims = new List() + { + new(JwtRegisteredClaimNames.UniqueName, Username), + new(JwtRegisteredClaimNames.NameId, Id.ToString() ?? string.Empty), + new(JwtRegisteredClaimNames.Name, Name), + new(JwtRegisteredClaimNames.Email, Email), + new(JwtRegisteredClaimNames.Iat, + EpochTime.GetIntDate(DateTime.Now).ToString(CultureInfo.InvariantCulture), + ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Exp, jwtOptions.Expiration.ToString()) + }; + claims.AddRange(RoleIds.Select(rId => new Claim(ClaimTypes.Role, rId))); + return claims; + } + + private TId GetIdFromClaims() + { + var idClaim = principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.NameId); + return (TId)Convert.ChangeType(idClaim.Value, typeof(TId)); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Security/DefaultTokenBuilder.cs b/src/Infrastructure/Security/DefaultTokenBuilder.cs deleted file mode 100644 index df56ff8..0000000 --- a/src/Infrastructure/Security/DefaultTokenBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Infrastructure.Security; - -public class DefaultTokenBuilder( - JwtOptions jwtOptions, - JwtSecurityTokenHandler jwtSecurityTokenHandler, - IEncryptionService encryptionService) - : TokenBuilderBase(jwtOptions, jwtSecurityTokenHandler, encryptionService) -{ -} \ No newline at end of file diff --git a/src/Infrastructure/Security/DefaultUserContext.cs b/src/Infrastructure/Security/DefaultUserContext.cs deleted file mode 100644 index 15f3b00..0000000 --- a/src/Infrastructure/Security/DefaultUserContext.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Infrastructure.Security; - -public class DefaultUserContext : UserContextBase -{ -} \ No newline at end of file diff --git a/src/Infrastructure/Security/ITokenBuilder.cs b/src/Infrastructure/Security/ITokenBuilder.cs deleted file mode 100644 index e0c83de..0000000 --- a/src/Infrastructure/Security/ITokenBuilder.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Globalization; -using System.Security.Claims; -using Infrastructure.Utils; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; - -namespace Infrastructure.Security; - -public interface ITokenBuilder -{ - IList GetClaimsFromUserContext(IUserContext userContext) where TId : IEquatable; - - void SetUserContext(TokenValidatedContext context); - - JwtTokenInfo GenerateJwtTokenInfo(IReadOnlyCollection claims); -} - -public abstract class TokenBuilderBase( - JwtOptions jwtOptions, - JwtSecurityTokenHandler jwtSecurityTokenHandler, - IEncryptionService encryptionService) - : ITokenBuilder where TId : IEquatable -{ - public virtual IList GetClaimsFromUserContext(IUserContext userContext) where TId : IEquatable - { - var claims = new List() - { - new(JwtRegisteredClaimNames.UniqueName, userContext.Username), - new(JwtRegisteredClaimNames.NameId, userContext.Id.ToString() ?? string.Empty), - new(JwtRegisteredClaimNames.Name, userContext.Name), - new(JwtRegisteredClaimNames.Email, userContext.Email), - new(JwtRegisteredClaimNames.Iat, - EpochTime.GetIntDate(DateTime.Now).ToString(CultureInfo.InvariantCulture), - ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Exp, jwtOptions.Expiration.ToString()) - }; - claims.AddRange(userContext.RoleIds.Select(rId => new Claim(ClaimTypes.Role, rId))); - return claims; - } - - public virtual void SetUserContext(TokenValidatedContext context) - { - var userContext = - context.HttpContext.RequestServices.GetService(typeof(IUserContext)) as IUserContext ?? - throw new NullReferenceException(nameof(IUserContext)); - var principal = context.Principal ?? throw new NullReferenceException(nameof(context.Principal)); - var idClaim = principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.NameId); - userContext.Id = (TId)Convert.ChangeType(idClaim.Value, typeof(TId)); - userContext.Username = - principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.UniqueName).Value; - userContext.Name = principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.Name).Value; - userContext.Email = principal.Claims.First(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()!; - } - - public virtual JwtTokenInfo GenerateJwtTokenInfo(IReadOnlyCollection claims) - { - var jwtToken = new JwtSecurityToken( - issuer: jwtOptions.Issuer, - audience: jwtOptions.Audience, - claims: claims, - notBefore: DateTime.Now, - expires: DateTime.Now.AddSeconds(jwtOptions.Expiration), - signingCredentials: jwtOptions.SigningCredentials); - var token = jwtSecurityTokenHandler.WriteToken(jwtToken); - return new JwtTokenInfo(encryptionService.Encrypt(token), jwtOptions.Expiration, - JwtBearerDefaults.AuthenticationScheme); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Security/IUserContext.cs b/src/Infrastructure/Security/IUserContext.cs deleted file mode 100644 index 15f36fc..0000000 --- a/src/Infrastructure/Security/IUserContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Infrastructure.Security; - -public interface IUserContext where TId : IEquatable -{ - TId Id { get; set; } - - string Username { get; set; } - - string Name { get; set; } - - string Email { get; set; } - - string[] RoleIds { get; set; } - - string RemoteIpAddress { get; set; } -} - -public abstract class UserContextBase : IUserContext where TId : IEquatable -{ - public TId 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 index f0e161b..ba65afe 100644 --- a/src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs +++ b/src/Infrastructure/Security/JwtBearerOptionsPostConfigureOptions.cs @@ -3,17 +3,12 @@ namespace Infrastructure.Security; public class JwtBearerOptionsPostConfigureOptions( - DefaultTokenHandler tokenHandler, - ITokenBuilder tokenBuilder) + DefaultTokenHandler tokenHandler) : 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