.NET 7 Web API部署Ubuntu VPS时读取appsettings.json失败报JWT错误
将.NET 7 Web API部署到Ubuntu 23.04的VPS,已安装.NET 7 SDK、Nginx等组件,但调用API时触发未处理异常:
'Connection id "0HMT9CF8GN50H", Request id "0HMT9CF8GN50H:00000001":
An unhandled exception was thrown by the application.
SoriooWebApi[365268]: System.ArgumentNullException: Value cannot be null. (Parameter 's')'
经排查确认是JWT Bearer相关错误,相关配置文件如下:
Program.cs
global using Microsoft.EntityFrameworkCore; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; using Retyoo.Data; using Retyoo.Data.Auth; using Retyoo.Models; using Retyoo.WebApi.Data.Auth; using Retyoo.WebApi.Mappings; using Retyoo.WebApi.Helpers; using Retyoo.WebApi.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. //Add DB builder.Services.AddControllers(); builder.Services.AddHttpContextAccessor(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // builder.Services.AddDbContext<AppDbContext>(options => { // options.UseNpgsql(builder.Configuration.GetConnectionString("RetyooDatabase"), assembly => assembly.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)); // }); builder.Services.AddDbContext<AppDbContext>(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("SoriooDB"), assembly => assembly.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)); }); //Email config builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailConfiguration")); //Add Identity builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; }) .AddEntityFrameworkStores<AppDbContext>() .AddDefaultTokenProviders(); //Config Identity builder.Services.Configure<IdentityOptions>(options => { options.Password.RequiredLength = 6; options.Password.RequireDigit = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireLowercase = false; options.Password.RequireUppercase = false; options.SignIn.RequireConfirmedEmail = true; options.User.RequireUniqueEmail = true; } ); //Add Authentication and JwtBearer builder.Services .AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = true, ValidIssuer = builder.Configuration["AppSettings:ValidIssuer"], ValidAudience = builder.Configuration["AppSettings:ValidAudience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["AppSettings:Secret"]!)), }; }) .AddGoogle(googleOptions => { googleOptions.ClientId = builder.Configuration["Authentication:GoogleWebClientId"]!; googleOptions.ClientSecret = builder.Configuration["Authentication:ClientSecret"]!; }); builder.Services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); //Inject app Dependencies (DI) builder.Services.AddScoped<IAuthRepository, AuthRepository>(); builder.Services.AddScoped<ITokenRepository, TokenRepository>(); builder.Services.AddScoped<ICategoryService, CategoryService>(); builder.Services.AddScoped<IImageService, ImageService>(); builder.Services.AddScoped<ISellerService, SellerService>(); builder.Services.AddScoped<ISellerCompanyService, SellerCompanyService>(); builder.Services.AddTransient<IEmailService, EmailService>(); //pipeline var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } else { app.UseHttpsRedirection(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
appsettings.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "SoriooDB": "Host=localhost;Port=5432;Database=sorioodb;User Id=xxx;Password=xxx;" }, "AppSettings": { "ValidIssuer": "http://localhost:5000", "ValidAudience": "http://localhost:3000", "Secret": "Asdad9DSADAOSDJADA=D)ASDAJADSALKSAD=ASKASDlkerjelkjadaşkAADKLJLK)=(098098ASDJKHkmKJHmnnbvv===", "RefreshTokenValidityInDays": 7 }, "EmailConfiguration": { "From": "xxx", "SmtpServer": "xxx", "Port": 587, "UserName": "xxx", "Password": "xxx" }, "Authentication": { "GoogleWebClientId": "xxx", "GoogleAndroidClientId": "xxx", "GoogleIOSClientId": "xxx", "ClientSecret": "xxx" } }
systemd服务配置文件
[Unit] Description=SoriooWebApi [Service] WorkingDirectory /home/app/SoriooWebApi ExecStart=dotnet /home/app/SoriooWebApi/Retyoo.WebApi.dll SyslogIdentifier=SoriooWebApi User=ubuntu Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target
本地发布运行无异常,已确认发布目录存在appsettings.json和appsettings.Production.json,无法定位问题根源,寻求解决方案。
1. 检查生产环境配置文件内容
.NET在生产环境下优先读取appsettings.Production.json,需验证该文件中AppSettings节点的Secret、ValidIssuer、ValidAudience是否完整赋值。如果这些字段缺失或为空,会直接导致配置取值为null,触发ArgumentNullException。
2. 修复配置取值的空值断言问题
代码中使用!强制断言配置项不为null,但生产环境若配置缺失会直接抛出异常。建议添加显式空值检查并抛出明确错误:
// 替换JWT配置部分 options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = true, ValidIssuer = builder.Configuration["AppSettings:ValidIssuer"] ?? throw new InvalidOperationException("AppSettings:ValidIssuer 未配置"), ValidAudience = builder.Configuration["AppSettings:ValidAudience"] ?? throw new InvalidOperationException("AppSettings:ValidAudience 未配置"), IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( builder.Configuration["AppSettings:Secret"] ?? throw new InvalidOperationException("AppSettings:Secret 未配置") )), };
Google认证配置也需要同步添加检查:
.AddGoogle(googleOptions => { googleOptions.ClientId = builder.Configuration["Authentication:GoogleWebClientId"] ?? throw new InvalidOperationException("Authentication:GoogleWebClientId 未配置"); googleOptions.ClientSecret = builder.Configuration["Authentication:ClientSecret"] ?? throw new InvalidOperationException("Authentication:ClientSecret 未配置"); });
3. 验证服务工作目录权限
确保ubuntu用户对/home/app/SoriooWebApi目录有读取权限,否则应用无法加载配置文件。执行以下命令修复权限:
sudo chown -R ubuntu:ubuntu /home/app/SoriooWebApi sudo chmod -R 755 /home/app/SoriooWebApi
4. 查看完整异常堆栈
当前日志仅显示异常头部,需查看完整堆栈确认具体空值参数。执行以下命令实时查看日志:
journalctl -u SoriooWebApi.service -f
重新触发API请求后,通过完整堆栈定位是JWT密钥还是其他配置项导致的空值问题。
5. 调试配置加载情况
在Program.cs中添加临时调试输出,验证生产环境配置是否正确加载:
// 调试代码,生产环境排查后可移除 Console.WriteLine($"ValidIssuer: {builder.Configuration["AppSettings:ValidIssuer"]}"); Console.WriteLine($"ValidAudience: {builder.Configuration["AppSettings:ValidAudience"]}"); Console.WriteLine($"Secret: {builder.Configuration["AppSettings:Secret"]}");
重新发布后查看日志输出,确认配置项是否被正确读取。
内容的提问来源于stack exchange,提问作者mehmetserif

