Как я могу использовать IdentityServer4 изнутри и снаружи устройства докеров?

Я хочу иметь возможность аутентифицироваться от Identity Server (STS) снаружи и внутри докерной машины.

У меня возникли проблемы с настройкой правильного авторитета, который работает как внутри, так и снаружи контейнера. Если я устанавливаю полномочия на внутреннее имя mcoidentityserver:5000, тогда API может аутентифицироваться, но клиент не может получить токен, поскольку клиент находится за пределами сети докеров. Если я установил полномочия на внешнее имя localhost:5000, тогда клиент может получить токен, но API не распознает имя полномочий (поскольку localhost в этом случае является хост-машиной).

Что мне нужно настроить для этого органа? Или, возможно, мне нужно настроить сеть докеров?

Диаграмма

Красная стрелка - это та часть, с которой у меня проблемы. Три контейнера докеров в сети, клиент и PostgreSQL Admin, их порты и красная стрелка, показывающие, где я думаю проблема лежит.

Деталь

Я настраиваю среду разработки докеров Windows 10, которая использует API-интерфейс ASP.NET Core (в Linux), Identity Server 4 (ASP.NET Core в Linux) и базу данных PostgreSQL. PostgreSQL не является проблемой, включенной в диаграмму для полноты. Он сопоставлен с 9876, потому что на данный момент у меня также есть экземпляр PostgreSQL, запущенный на хосте. mco - сокращенное название нашей компании.

Я слежу за инструкциями Identity Server 4 для запуска и запуска.

код

Я не включаю docker-compose.debug.yml, потому что он запускал команды, относящиеся только к работе в Visual Studio.

Докер-compose.yml

version: '2'

services:
mcodatabase:
    image: mcodatabase
    build:
    context: ./Data
    dockerfile: Dockerfile
    restart: always
    ports:
    - 9876:5432
    environment:
    POSTGRES_USER: mcodevuser
    POSTGRES_PASSWORD: password
    POSTGRES_DB: mcodev
    volumes:
    - postgresdata:/var/lib/postgresql/data
    networks:
    - mconetwork

mcoidentityserver:
    image: mcoidentityserver
    build:
    context: ./Mco.IdentityServer
    dockerfile: Dockerfile
    ports:
    - 5000:5000
    networks:
    - mconetwork

mcoapi:
    image: mcoapi
    build:
    context: ./Mco.Api
    dockerfile: Dockerfile
    ports:
    - 56107:80
    links:
    - mcodatabase
    depends_on:
    - "mcodatabase"
    - "mcoidentityserver"
    networks:
    - mconetwork

volumes:
postgresdata:

networks:
mconetwork:
    driver: bridge

Докер-compose.override.yml

Это создается плагином Visual Studio для ввода дополнительных значений.

version: '2'

services:
mcoapi:
    environment:
    - ASPNETCORE_ENVIRONMENT=Development
    ports:
    - "80" 

mcoidentityserver:
    environment:
    - ASPNETCORE_ENVIRONMENT=Development
    ports:
    - "5000" 

API Dockerfile

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "Mco.Api.dll"]

Док файл сервера удостоверений

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
EXPOSE 5000
ENV ASPNETCORE_URLS http://*:5000
ENTRYPOINT ["dotnet", "Mco.IdentityServer.dll"]

API Startup.cs

Где мы говорим API для использования сервера удостоверений и установки полномочий.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        // This can't work because we're running in docker and it doesn't understand what localhost:5000 is!
        Authority = "http://localhost:5000", 
        RequireHttpsMetadata = false,

        ApiName = "api1"
    });

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Identity Server Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentityServer()
            .AddTemporarySigningCredential()
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients());
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();

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

        app.UseIdentityServer();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

Identity Server Config.cs

public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",

                // no interactive user, use the clientid/secret for authentication
                AllowedGrantTypes = GrantTypes.ClientCredentials,

                // secret for authentication
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

                // scopes that client has access to
                AllowedScopes = { "api1" }
            }
        };
    }
}

Client

Запуск в консольном приложении.

var discovery = DiscoveryClient.GetAsync("localhost:5000").Result;
var tokenClient = new TokenClient(discovery.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("api1").Result;

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return 1;
}

var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);

var response = client.GetAsync("http://localhost:56107/test").Result;
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = response.Content.ReadAsStringAsync().Result;
    Console.WriteLine(JArray.Parse(content));
}

Спасибо заранее.

Ответ 1

Убедитесь, что для IssuerUri задана явная константа. У нас были похожие проблемы с доступом к экземпляру Identity Server по IP/имени хоста, и мы решили это следующим образом:

services.AddIdentityServer(x =>
{
    x.IssuerUri = "my_auth";
})

PS Почему вы не унифицируете URL hostname:5000 для hostname:5000? Да, клиент и API могут вызывать одно и то же hostname:5000 URL hostname:5000 если:

  • Порт 5000 выставлен (вижу нормально)
  • DNS разрешен внутри док-контейнера.
  • У вас есть доступ к hostname:5000 (проверьте брандмауэры, топологию сети и т.д.)

DNS - самая сложная часть. Если у вас есть какие-либо проблемы с этим, я рекомендую вам попытаться связаться с Identity Server по указанному IP-адресу, а не по hostname.

Ответ 2

Чтобы сделать это, мне нужно было передать две переменные окружения в docker-compose.yml и настроить CORS на экземпляре сервера идентификации, чтобы API было разрешено вызывать его. Настройка CORS выходит за рамки этого вопроса; этот вопрос хорошо это освещает.

Docker-Compose изменений

Идентификационному серверу требуется IDENTITY_ISSUER, то есть имя, которое идентификационный сервер даст сам. В этом случае я использовал IP хоста док-станции и порт сервера идентификации.

  mcoidentityserver:
    image: mcoidentityserver
    build:
      context: ./Mco.IdentityServer
      dockerfile: Dockerfile
    environment:
      IDENTITY_ISSUER: "http://10.0.75.1:5000"
    ports:
       - 5000:5000
    networks:
     - mconetwork

API должен знать, где находятся полномочия. Мы можем использовать имя сети докера для полномочий, потому что вызов не должен выходить за пределы сети докера, API только вызывает идентификационный сервер для проверки токена.

  mcoapi:
    image: mcoapi
    build:
      context: ./Mco.Api
      dockerfile: Dockerfile
    environment:
      IDENTITY_AUTHORITY: "http://mcoidentityserver:5000"
    ports:
       - 56107:80
    links:
     - mcodatabase
     - mcoidentityserver
    depends_on:
     - "mcodatabase"
     - "mcoidentityserver"
    networks:
     - mconetwork

Используя эти значения в С#

Identity Server.cs

Вы устанавливаете имя издателя удостоверений в ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
        var sqlConnectionString = Configuration.GetConnectionString("DefaultConnection");

        services
            .AddSingleton(Configuration)
            .AddMcoCore(sqlConnectionString)
            .AddIdentityServer(x => x.IssuerUri = Configuration["IDENTITY_ISSUER"])
            .AddTemporarySigningCredential()
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddCorsPolicyService<InMemoryCorsPolicyService>()
            .AddAspNetIdentity<User>();
    }

API Startup.cs

Теперь мы можем установить Authority для переменной среды.

app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        Authority = Configuration["IDENTITY_AUTHORITY"],
        RequireHttpsMetadata = false,
        ApiName = "api1"
    });

Недостатки

Как показано здесь, docker-compose не годится для производства, так как издатель идентификаторов с жестким кодом является локальным IP. Вместо этого вам понадобится правильная запись DNS, которая будет сопоставлена с экземпляром докера с запущенным на нем сервером идентификации. Для этого я бы создал файл переопределения docker-compose и собрал бы производство с переопределенным значением.

Спасибо Илья-Чумакову за помощь.

редактировать

В дополнение к этому я написал весь процесс создания докера Linux + ASP.NET Core 2 + OAuth с Identity Server в своем блоге.