using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using QYZH.InteractiveMagazine.Infrastructure.Auth; using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.Models.Settings; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; namespace QYZH.InteractiveMagazine.Infrastructure.Extensions; /// /// Infrastructure service registration extensions. /// public static class DependencyInjectionExtensions { /// /// Registers infrastructure services. /// public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null) { AddJwtAuthentication(services, configuration, environment); services.AddTransient(); services.AddTransient(); } private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null) { if (environment?.IsDevelopment() == true) { services.AddAuthentication("NoAuth") .AddScheme("NoAuth", options => { }); return; } var jwtSettings = configuration.GetSection("JwtSettings").Get()!; services.AddSingleton(jwtSettings); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtSettings.Issuer, ValidateAudience = true, ValidAudience = jwtSettings.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey!)), ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; options.Events = new JwtBearerEvents { OnTokenValidated = async context => { var currentToken = GetBearerToken(context); if (string.IsNullOrEmpty(currentToken)) { context.Fail("Invalid token"); return; } var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) { context.Fail("Invalid token"); return; } var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId)); var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value; if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken)) { return; } if (!string.IsNullOrEmpty(wxUserId)) { var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId)); if (wechatToken == currentToken) { return; } } context.Fail("Token expired, please login again"); } }; }); } private static string? GetBearerToken(TokenValidatedContext context) { var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault(); if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return null; } return authHeader.Substring("Bearer ".Length).Trim(); } } /// /// Authentication handler used only in development. /// public class NoAuthHandler : AuthenticationHandler { public NoAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task HandleAuthenticateAsync() { var claims = new[] { new Claim(ClaimTypes.Name, "DevUser"), new Claim(ClaimTypes.NameIdentifier, "0") }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } }