Защита SPA сервером авторизации перед первой загрузкой

Я использую «новые» шаблоны проектов для приложений angular SPA в dotnet core 2.1, как написано в статье Используйте шаблон проекта Angular с ASP.NET Core.

Но в этой статье ничего не говорится о защите самого SPA. Вся информация, которую я нахожу, касается защиты WEBAPI, но в первую очередь меня интересует защита SPA.

Это означает: когда я открываю свой СПА, например, https: // localhost: 44329 / Я хотел бы, чтобы меня перенаправили на сервер авторизации немедленно вместо нажатия какой-либо кнопки, которая выполнит аутентификацию.

Задний план:

  • Я должен убедиться, что только авторизованные пользователи могут видеть SPA.
  • Я хочу использовать Предоставление кода авторизации для получения токенов обновления с моего сервера авторизации.
  • Я не могу использовать Неявное предоставление, потому что токены обновления не могут быть приватными в браузере

Текущий подход предназначен для обеспечения соблюдения политики MVC, которая требует аутентифицированного пользователя. Но это можно применить только к контроллеру MVC. Вот почему я добавил HomeController для обслуживания первого запроса.

См. Структуру проекта:

Защита SPA сервером авторизации перед первой загрузкой

Мой Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "CustomScheme";
        })
        .AddCookie()
        .AddOAuth("CustomScheme", options =>
        {
            // Removed for brevity
        });

    services.AddMvc(config =>
    {
        // Require a authenticated user
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseAuthentication();

    app.UseStaticFiles();
    app.UseSpaStaticFiles();

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

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
}

Текущее поведение: Когда я запускаю свой SPA, я сразу перенаправляюсь на мой сервер авторизации из-за политики MVC. После успешной аутентификации я вижу метод Индекс домашнего контроллера, но не мой SPA.

Итак, вопрос в том, как мне обслуживать свой SPA после того, как я был перенаправлен с сервера аутентификации?

Другими словами, вы хотите контролировать доступ к HTML и JavaScript?

Llama 08.05.2018 06:48

@john да, я так думаю

Daniel 08.05.2018 06:54

Вы нашли решение этой проблемы? Сейчас я сталкиваюсь с тем же самым ...

Georges Legros 06.06.2018 11:29

Еще нет. Но на следующей неделе должно быть Решение

Daniel 06.06.2018 11:30

Хорошо, тогда я продолжу свои исследования. Я выложу здесь все, что найду, если мне что-нибудь удастся

Georges Legros 06.06.2018 11:41

@ Дэниел, ты понял, что нужно делать? У меня такая же проблема, но мне кажется, что она не работает

goldsmit409 31.07.2018 22:13

@ goldsmit409 добавил текущий ответ. Я спросил об этом эксперта по разработке Google. И то, что я написал, было его предложением.

Daniel 01.08.2018 11:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
31
7
9 677
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Ответ принят как подходящий

У меня есть кое-что, что вроде работает.

В своих исследованиях я наткнулся на apon эта почта, предлагающий использовать промежуточное ПО вместо атрибута Authorize.

Теперь метод, использованный в этом посте authService, похоже, не работает в моем случае (не знаю почему, я продолжу расследование и опубликую все, что найду позже).

Поэтому я решил пойти с более простым решением. Вот мой конфиг

        app.Use(async (context, next) =>
        {
            if (!context.User.Identity.IsAuthenticated)
            {
                await context.ChallengeAsync("oidc");
            }
            else
            {
                await next();
            }
        });

В этом случае oidc запускается ДО приложения Spa, и поток работает правильно. Нет необходимости в контроллере.

HTH

Для Azure также необходимо отфильтровать URL-адрес входа, чтобы избежать бесконечного цикла входа, и настроить заголовки CORS, чтобы браузеры могли работать в междоменном режиме: app.UseCors(policy => policy.SetIsOriginAllowed(origin => origin == "https://login.microsoftonline.com")); app.UseAuthentication(); app.Use(async (context, next) => { if (!context.User.Identity.IsAuthenticated && context.Request.Path != "/signin-oidc") { await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationS‌​cheme); } else { await next(); } });

Kram 28.05.2019 23:46

У меня тот же вопрос, но я использую aspnet core 3.0, вот мой вопрос: stackoverflow.com/questions/58363393/…

Ahmer Ali Ahsan 13.10.2019 14:06

Внесите это изменение в свой startup.cs:

app.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/home/index";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
});

Затем поместите ссылку на приложение angular в index.cshtml:

<app-root></app-root>

и убедитесь, что вы включили все необходимые файлы в файл index.cshtml или в свой макет:

<link href = "~/styles.bundle.css" rel = "stylesheet" />

<script type = "text/javascript" src = "~/inline.bundle.js" asp-append-version = "true"></script>
<script type = "text/javascript" src = "~/polyfills.bundle.js" asp-append-version = "true"></script>
<script type = "text/javascript" src = "~/vendor.bundle.js" asp-append-version = "true"></script>
<script type = "text/javascript" src = "~/main.bundle.js" asp-append-version = "true"></script>

Мы все еще работаем над устранением неполадок со всеми нашими ссылочными пакетами, но это приведет к тому, что базовый SPA будет работать за asp.net auth.

Вы когда-нибудь задумывались, как ссылаться на файлы bundle.js в производственной сборке? Кажется, что SPA выводит их в формате main.[random string].bundle.js при производственной сборке, поэтому статическая ссылка у вас прерывается.

Kyle V. 05.12.2018 22:13

Мы сделали, но это не идеальное решение. Вы можете использовать переменные среды, чтобы определить, какие версии загружать, например : <environment names = "Development"> <script type = "text/javascript" src = "~/runtime.js" asp-append-version = "true"></script> </environment> <environment names = "Staging,Production"> <script type = "text/javascript" asp-src-include = "~/dist/runtime.*.js" asp-append-version = "true"></script> </environment>. Однако будьте осторожны, если вы не удаляете старые файлы, * загрузит все файлы, соответствующие шаблону, что вызовет конфликты.

chris1out 07.12.2018 16:03

Похоже, что нет НАСТОЯЩЕГО решения, когда речь идет о SPA.

Чтобы выполнить некоторую логику в SPA, SPA должен быть изначально загружен.

Но есть какая-то уловка: в RouterModule вы можете предотвратить начальную навигацию, как показано:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'about',
    pathMatch: 'full'
  },
  {
    path: '**',
    redirectTo: 'about'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { initialNavigation: false })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Затем в вашем app.component.ts вы можете позаботиться о своей аутентификации:

@Component({
  selector: 'flight-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private router: Router, private oauthService: OAuthService) {
    if (this.oauthService.isAuthenticated()) {
      this.router.navigate(['/home']);
    } else {
      // login Logic
    }
  }
}

Глядя на другие комментарии, определенно есть другие варианты

S. ten Brinke 08.06.2021 17:15

Использование промежуточного программного обеспечения @George потребует аутентификации для всех запросов. Если вы хотите запустить это только для localhost, добавьте его в UseSpa, завернутый в блок env.IsDevelopment ().

Другой вариант, который также хорошо работает для развернутых сред, - это вернуть index.html из резервного маршрута вашего спа.

Запускать:

        if (!env.IsDevelopment())
        {
            builder.UseMvc(routes =>
            {
                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new { controller = "Home", action = "AuthorizedSpaFallBack" });
            });
        }

HomeController:

[Authorize]
public IActionResult AuthorizedSpaFallBack()
{
    var file = _env.ContentRootFileProvider.GetFileInfo("ClientApp/dist/index.html");
    return PhysicalFile(file.PhysicalPath, "text/html");
}

Если вам нужно, чтобы base.href соответствовал URL-адресу запроса браузера (например, cookie, который имеет значение Path), вы можете шаблонизировать его с помощью регулярного выражения (или использовать представление бритвы, как в других примерах).

    [Authorize]
    public IActionResult SpaFallback()
    {
        var fileInfo = _env.ContentRootFileProvider.GetFileInfo("ClientApp/dist/index.html");
        using (var reader = new StreamReader(fileInfo.CreateReadStream()))
        {
            var fileContent = reader.ReadToEnd();
            var basePath = !string.IsNullOrWhiteSpace(Url.Content("~")) ? Url.Content("~") + "/" : "/";

            //Note: basePath needs to match request path, because cookie.path is case sensitive
            fileContent = Regex.Replace(fileContent, "<base.*", $"<base href=\"{basePath}\">");
            return Content(fileContent, "text/html");
        }
    }

Он работает в продакшене и помогает мне установить базовый URL. Спасибо

Kashif Hanif 12.02.2019 12:42

Как инициализировать переменную _env?

Tal 24.03.2020 11:39

@Tal Вставить IHostingEnvironment в конструктор и присвоить переменной класса только для чтения.

Cirem 24.03.2020 15:07

Промежуточное ПО @George не будет требовать аутентификации для всех запросов, если вы поместите его прямо над вызовом app.UseSpa (после UseMvc / UseEndpoit)

torvin 27.12.2020 12:21

На основе Georges Legros мне удалось заставить это работать для .Net Core 3 с Identity Server 4 (готовый проект VS), так что конвейер app.UseSpa не попадет, если пользователь не аутентифицирован сначала через сервер идентификации. Это намного приятнее, потому что вам не нужно ждать, пока загрузится SPA, только для того, чтобы затем вас перенаправили на логин.

Вы должны убедиться, что у вас есть авторизация / роли, работающие правильно, иначе User.Identity.IsAuthenticated всегда будет ложным.

public void ConfigureServices(IServiceCollection services)
{
    ...

    //Change the following pre-fab lines from

    //services.AddDefaultIdentity<ApplicationUser>()
    //    .AddEntityFrameworkStores<ApplicationDbContext>();

    //To

    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddRoles<IdentityRole>()
            //You might not need the following two settings
            .AddDefaultUI()
            .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

    ...
}

Затем добавьте следующую настройку следующего канала:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

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

    //Added this to redirect to Identity Server auth prior to loading SPA    
    app.Use(async (context, next) =>
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            await context.ChallengeAsync("Identity.Application");
        }
        else
        {
            await next();
        }
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
} 

У меня тот же вопрос, но я использую aspnet core 3.0, вот мой вопрос: stackoverflow.com/questions/58363393/…

Ahmer Ali Ahsan 13.10.2019 14:06

@AhmerAliAhsan мое решение для aspnet core 3.0 preview-8

Rob 14.10.2019 03:26

@bob, не могли бы вы проверить мой вопрос. Может ты сможешь мне помочь

Ahmer Ali Ahsan 14.10.2019 05:49

@AhmerAliAhsan, вы не включаете часть Services.AddIdentity в решение, которое я предложил выше (это мое предположение) - вам определенно нужно иметь .AddRoles <IdentityRole> () из памяти, иначе context.User.Identity.IsAuthenticated всегда будет ложным

Rob 06.02.2020 13:09

Для лазурного объявления (Жорж отвечает с karmas edit без корса):

в ConfigureServices (сервисы IServiceCollection):

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(_configuration.GetSection("AzureAd"));

в Configure (приложение IApplicationBuilder, IWebHostEnvironment env, ILoggerFactory loggerFactory):

app.UseAuthentication();

app.Use(async (context, next) =>
{
    if (!context.User.Identity.IsAuthenticated && context.Request.Path != "/signin-oidc")
    {
        await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme);
    }
    else
    {
        await next();
    }
});

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

Другие вопросы по теме