You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 06:27:03