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