MailKit.Security.SslHandshakeException:主机名与服务器 SSL 证书中给定的名称不匹配。ASP.NET 核心、Nginx

Pet*_*pyl 7 ssl reverse-proxy nginx mailkit asp.net-core

当我尝试通过 Google 和生产容器化应用程序上的 MailKit 库发送电子邮件时,出现以下异常:

MailKit.Security.SslHandshakeException:尝试建立 SSL 或 TLS 连接时发生错误。主机名与服务器 SSL 证书中给出的名称不匹配。

我使用 ASP.NET Core 5 和 Kestrel。Nginx 是我的反向代理。当我使用 Postman 获取数据时,SSL 工作正常。但是当我尝试发送邮件时,出现异常。在没有 Nginx 服务器的开发环境中,代理可以正常工作。

这是我的 nginx.conf 文件:

server {
    client_max_body_size 6M;

    listen 80;

    server_name myhost.com www.myhost.com;

    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    client_max_body_size 6M;

    listen 443 ssl;

    server_name myhost.com www.myhost.com;

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 0;
    gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;

    #SSL code
    ssl_certificate /etc/letsencrypt/live/myhost.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myhost.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    #headers
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-SSL-CERT $ssl_client_escaped_cert;
    
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

   # location /map {
   #    proxy_pass http://client;
   # }

    location /admin {
        proxy_pass http://client-admin;
    }

    location /api {
        proxy_pass http://api:5000;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的 Program.cs 文件:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MonumentsMap
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureLogging(logging => {
                    logging.ClearProviders();
                    logging.AddConsole();
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.ConfigureKestrel(conf => {
                        conf.Limits.MaxRequestBodySize = 6_000_000;
                    });
                });
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的 Startup.cs 文件:

namespace MonumentsMap
{
    public class Startup
    {

        public Startup(IConfiguration configuration)
        {
            this.Configuration = configuration;

        }
        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(o => o.AddPolicy("WebClientPolicy", builder =>
            {
                builder.AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowAnyOrigin();
            }));
            services.AddControllers().AddNewtonsoftJson(options =>
                options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
            );
            services.AddDbContext<ApplicationContext>(options =>
            {
                options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), o => 
                {
                    o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
                });
            });
            services.AddMemoryCache();

            services.AddRepositories();
            services.AddServices();

            services.AddSingleton(Configuration.GetSection("ImageFilesParams").Get<ImageFilesParams>());
            services.AddScoped<CultureCodeResourceFilter>();
            services.Configure<MailSettings>(Configuration.GetSection("MailSettings"));

            services.AddIdentity<ApplicationUser, IdentityRole>(opts =>
            {
                opts.Password.RequireDigit = true;
                opts.Password.RequireLowercase = true;
                opts.Password.RequireUppercase = true;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequiredLength = 7;
            }).AddEntityFrameworkStores<ApplicationContext>();

            services.AddAuthentication(opts =>
            {
                opts.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = Configuration["Auth:Jwt:Issuer"],
                    ValidAudience = Configuration["Auth:Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"])
                    ),
                    ClockSkew = TimeSpan.Zero,
                    RequireExpirationTime = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidateAudience = true
                };
            });

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc(name: "v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Monuments Map Api", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {

            app.UseExceptionHandler("/errors/500");

            app.UseStatusCodePagesWithReExecute("/errors/{0}");

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.All
            });

            app.UseSwagger();

            app.UseSwaggerUI(c => {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Monuments Map Api V1");
            });

            app.UseCors("WebClientPolicy");

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationContext>();
                var roleManager = serviceScope.ServiceProvider.GetService<RoleManager<IdentityRole>>();
                var userManager = serviceScope.ServiceProvider.GetService<UserManager<ApplicationUser>>();
                context.Database.Migrate();
                var cultures = Configuration.GetSection("SupportedCultures").Get<List<Culture>>();
                DbSeed.Seed(context, roleManager, userManager, cultures, Configuration);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

MailService.cs 文件:

namespace MonumentsMap.Data.Services
{
    public class MailService : IMailService
    {
        private readonly MailSettings _mailSettings;

        public MailService(IOptions<MailSettings> mailSettings) => _mailSettings = mailSettings.Value;

        public async Task SendEmailAsync(MailRequestDto mailRequest)
        {
            var email = new MimeMessage();
            email.Sender = MailboxAddress.Parse(_mailSettings.Mail);
            email.To.Add(MailboxAddress.Parse(mailRequest.ToEmail));
            email.Subject = mailRequest.Subject;
            var builder = new BodyBuilder();
            if (mailRequest.Attachments != null)
            {
                byte[] fileBytes;
                foreach (var file in mailRequest.Attachments)
                {
                    if (file.Length > 0)
                    {
                        using (var ms = new MemoryStream())
                        {
                            file.CopyTo(ms);
                            fileBytes = ms.ToArray();
                        }
                        builder.Attachments.Add(file.FileName, fileBytes, ContentType.Parse(file.ContentType));
                    }
                }
            }
            builder.HtmlBody = mailRequest.Body;
            email.Body = builder.ToMessageBody();
            using var smtp = new SmtpClient();
            smtp.Connect(_mailSettings.Host, _mailSettings.Port);
            smtp.Authenticate(_mailSettings.Mail, _mailSettings.Password);
            await smtp.SendAsync(email);
            smtp.Disconnect(true);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

应用程序设置.json:

  "MailSettings": {
    "Mail": "mymail@gmail.com",
    "DisplayName": "My mail",
    "Password": "application_pass",
    "Host": "smtp.gmail.com",
    "Port": 465
  },
Run Code Online (Sandbox Code Playgroud)

docker-compose 文件:

version: "3.8"
services:
  api:
    container_name: api
    build: ./Api
    depends_on:
      - db
    restart: unless-stopped
    environment: 
      ASPNETCORE_URLS: http://+:5000
    volumes: 
      - ./Images:/app/Images
  db:
    container_name: db
    image: postgres
    restart: always
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root
      POSTGRES_DB: api_db
    volumes:
      - ./postgres-data:/var/lib/postgresql/data:rw
  client:
    container_name: client
    build: ./client
    depends_on:
      - api
    restart: unless-stopped
  client-admin:
    container_name: client-admin
    build: ./client-admin
    depends_on:
      - api
    restart: unless-stopped
    stdin_open: true
  nginx:
    image: nginx:stable-alpine
    container_name: docker-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes: 
      - ./nginx/nginx.conf.prod:/etc/nginx/conf.d/nginx.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    depends_on: 
      - client
      - client-admin
      - certbot
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
  certbot:
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
Run Code Online (Sandbox Code Playgroud)

小智 9

您可以通过以下方式禁用 SSL/TLS

smtp.Connect(_mailSettings.Host, _mailSettings.Port, **MailKit.Security.SecureSocketOptions.None**);
Run Code Online (Sandbox Code Playgroud)

请参阅http://www.mimekit.net/docs/html/T_MailKit_Security_SecureSocketOptions.htm