Защита SPA сервером авторизации перед первой загрузкой
Я использую "новые" шаблоны проектов для угловых приложений SPA в dotnet core 2.1, как написано в статье. Используйте шаблон Angular Project с ASP.NET Core.
Но в этой статье ничего не говорится о защите самого SPA. Вся информация, которую я нахожу, касается обеспечения WEBAPI, но в первую очередь я заинтересован в обеспечении SPA.
Это означает: когда я открываю свой SPA, например, https://localhost: 44329/ i, я бы сразу перенаправлялся на сервер авторизации, а не нажимал кнопку, которая будет выполнять аутентификацию.
Фон:
- Я должен убедиться, что только сертифицированным пользователям разрешено видеть SPA.
- Я хочу использовать лицензионный код авторизации, чтобы получить токены обновления с моего сервера авторизации.
- Я не могу использовать Implicit Grant, потому что токены обновления не могут быть закрыты в браузере
Текущий подход заключается в обеспечении соблюдения политики MVC, требующей аутентифицированного пользователя. Но это может быть применено только к контроллеру MVC. Вот почему я добавил HomeController для обслуживания первого запроса.
См. Структуру проекта:
Мой 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. После успешной проверки подлинности я вижу метод Index для домашнего контроллера, но не мой SPA.
Итак, вопрос в том, как я должен обслуживать свой SPA после того, как я был перенаправлен с сервера аутентификации?
Ответы
Ответ 1
У меня есть что-то, что кажется сработавшим.
В моих исследованиях я наткнулся на этот пост, предлагая использовать промежуточное программное обеспечение вместо атрибута Authorize.
Теперь метод, используемый в этой статье authService, кажется, не работает в моем случае (не знаю, почему, я продолжу расследование и опубликую whaterver, который я нахожу позже).
Поэтому я решил пойти с более простым решением. Вот моя конфигурация
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync("oidc");
}
else
{
await next();
}
});
В этом случае oidc пинает перед приложением Spa, и поток работает правильно. Нет необходимости в контроллере вообще.
НТН
Ответ 2
Использование middlware @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");
}
}
Ответ 3
Внесите это изменение в ваш startup.cs:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPage = "/home/index";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
Затем поместите ссылку на угловое приложение в 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.
Ответ 4
Похоже, что нет РЕАЛЬНОГО решения, когда речь идет о 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
}
}
}
Ответ 5
Основываясь на Жорже Легро, мне удалось заставить это работать для .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");
}
});
}