【问题标题】:Securing a SPA by authorization server before first load在首次加载之前通过授权服务器保护 SPA
【发布时间】:2018-05-08 04:34:54
【问题描述】:

我正在使用文章Use the Angular project template with ASP.NET Core 中所写的 dotnet core 2.1 中 Angular SPA 应用程序的“新”项目模板。

但是这篇文章没有提到任何关于保护 SPA 本身的内容。 我找到的所有信息都是关于保护 WEBAPI,但首先我对保护 SPA 感兴趣。

这意味着:当我打开我的 SPA 时,例如https://localhost:44329/ 我想立即被重定向到授权服务器,而不是点击一些按钮来进行身份验证。

背景:

  • 我必须确保只允许经过身份验证的用户查看 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?

【问题讨论】:

  • 那么,换句话说,你想控制对 html 和 javascript 的访问?
  • @john 是的,我想是的
  • 您找到解决此问题的方法了吗?我目前面临着完全相同的事情......
  • 还没有。但下周应该会有解决方案
  • 好的,那我继续研究。如果我设法得到一些东西,我会在这里发布我找到的任何东西

标签: c# angular asp.net-core .net-core


【解决方案1】:

我有一些似乎可以工作的东西。

在我的研究中,我偶然发现了 this post 建议使用中间件而不是 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.AuthenticationScheme); } else { await next(); } });
  • 我也有同样的问题,但我使用的是 aspnet core 3.0,这是我的问题:stackoverflow.com/questions/58363393/…
  • 这个问题是授权,而不是身份验证。使用此方法不会验证您的任何声明和政策!
  • 其实这很完美。 Cirem 的答案的问题是我在调试时无法使用它,因为 SPA 中间件没有在 HTML 文件中进行变量替换/插入。但是这种方式是完美的。只要确保 Use 出现在 UseSPA 之前即可
【解决方案2】:

使用@George 的中间件需要对所有请求进行身份验证。如果您只想为 localhost 运行它,请将其添加到 UseSpa 下,并包裹在 env.IsDevelopment() 块中。

另一个同样适用于已部署环境的选项是从您的 spa 后备路由返回 index.html。

启动:

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

家庭控制器:

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

如果您需要 base.href 来匹配浏览器请求 url(例如具有 Path 值的 cookie),您可以使用正则表达式对其进行模板化(或像其他示例一样使用剃刀视图)。

    [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");
        }
    }

对于本地运行和使用 Angular cli 开发服务器,您必须在代理(或在进程中启动开发服务器)之前要求经过身份验证的用户:

            app.UseSpa(spa =>
        {
            // To learn more about options for serving an Angular SPA from ASP.NET Core,
            // see https://go.microsoft.com/fwlink/?linkid=864501

            spa.Options.SourcePath = "ClientApp";

            if (env.IsDevelopment())
            {
                app.UseWhen(context => !context.Request.Path.ToString().EndsWith(".map"), appBuilder =>
                {
                    //appBuilder.UseMiddleware<RequireAuthenticationMiddleware>();
                    appBuilder.Run(async (context) =>
                    {
                        if (!context.User.Identity.IsAuthenticated)
                        {
                            await context.ChallengeAsync();
                        }

                    });
                });
                // spa.UseAngularCliServer(npmScript: "start");
                spa.UseProxyToSpaDevelopmentServer("http://localhost:4400");
            }
        });

【讨论】:

  • 它在生产中工作并帮助我设置基本 URL。谢谢
  • 如何初始化 _env 变量?
  • @Tal 将IHostingEnvironment 注入构造函数并分配给只读类变量。
  • @George 的中间件不需要对所有请求进行身份验证,如果你把它放在app.UseSpa 调用的正上方(在UseMvc / UseEndpoit 之后)
  • 太棒了!不知道为什么这似乎是一个如此晦涩的用例,但这是我在任何地方都能找到的唯一答案。谢谢。
【解决方案3】:

基于 Georges Legros,我已设法使此功能适用于带有 Identity Server 4 的 .Net Core 3(开箱即用的 VS 项目),因此如果用户使用 app.UseSpa 管道,则不会受到影响不首先通过身份服务器进行身份验证。这要好得多,因为您不必等待 SPA 加载然后重定向到登录。

您必须确保您的授权/角色正常工作,否则 User.Identity.IsAuthenticated 将始终为 false。

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/…
  • @AhmerAliAhsan 我的解决方案是针对 aspnet core 3.0 preview-8
  • @bob 你想看看我的问题吗?或许你能帮帮我
  • @AhmerAliAhsan 您不包括我上面提出的解决方案的 Services.AddIdentity 部分(我的猜测)-否则您肯定需要从内存中获取 .AddRoles() context.User.Identity.IsAuthenticated 将始终为 false
【解决方案4】:

对您的 startup.cs 进行此更改:

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

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

然后将angular app的引用放到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 的格式输出它们,因此您的静态引用会中断。
  • 我们做到了,但这不是一个完美的解决方案。您可以使用环境变量来确定要加载的版本,例如:&lt;environment names="Development"&gt; &lt;script type="text/javascript" src="~/runtime.js" asp-append-version="true"&gt;&lt;/script&gt; &lt;/environment&gt; &lt;environment names="Staging,Production"&gt; &lt;script type="text/javascript" asp-src-include="~/dist/runtime.*.js" asp-append-version="true"&gt;&lt;/script&gt; &lt;/environment&gt;。但请注意,如果您不删除旧文件,* 将加载所有匹配该模式的文件,从而导致冲突。
【解决方案5】:

说到 SPA,似乎没有真正的解决方案。

为了在 SPA 中执行某些逻辑,必须先加载 SPA。

但是有一些技巧:在RouterModule 中你可以阻止initialNavigation 如图:

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
    }
  }
}

【讨论】:

  • 看看其他cmets肯定还有其他选择
【解决方案6】:

对于 azure 广告(georges 回答 karmas edit without cors):

在 ConfigureServices(IServiceCollection 服务)中:

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

在配置中(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();
});
        

【讨论】:

    【解决方案7】:

    其他答案很好,但它们不验证您的策略,它们仅验证您是否已通过身份验证。

    一种可能的解决方案是使用IMiddelware 工厂:

    services.AddAuthorization(options =>
    {
        options.AddPolicy("Tenant", policy => policy.RequireAuthenticatedUser().Requirements.Add(new TenantRequirement()));
    });
    
    app.UseMiddleware<TenantAuthorizationMiddleware>();
    
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Http;
    
    namespace FieldCap.Service.Middleware
    {
        public class TenantAuthorizationMiddleware : IMiddleware
        {
            private readonly IAuthorizationService _authorizationService;
    
            public TenantAuthorizationMiddleware(IAuthorizationService authorizationService)
            {
                _authorizationService = authorizationService;
            }
    
            public async Task InvokeAsync(HttpContext context, RequestDelegate next)
            {
                var result = await _authorizationService.AuthorizeAsync(context.User, "Tenant");
    
                if (!result.Succeeded)
                {
                    context.Response.Redirect("/login");
    
                    return;
                }
    
                await next(context);
            }
        }
    }
    
    

    这将确保每个请求都通过tenant 的策略检查。

    【讨论】:

      【解决方案8】:

      我今天遇到了同样的问题,终于找到了解决方案,我们使用 .NET 5 并迁移到 .NET 6。 这个想法的灵感来自MapFallbackToFile 方法和上面的帖子。 下面的本地函数CreateRequestDelegate是参考aspnetcore代码这里https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/StaticFiles/src/StaticFilesEndpointRouteBuilderExtensions.cs

      app.UseEndpoints(endpoints =>
              {
                  //Razor pages
                  endpoints.MapRazorPages();
                  //for Web API
                  endpoints.MapControllers();
                  //fallback to SPA
                  //endpoints.MapFallbackToFile("index.html");
                  endpoints.MapFallback(CreateRequestDelegate(endpoints, "index.html"));
                  //local function
                  RequestDelegate CreateRequestDelegate(IEndpointRouteBuilder endpoints,string filePath)
                  {
                      var app = endpoints.CreateApplicationBuilder();
                      //Use Cookie authentication to secure angular index.html file
                      app.Use(async (context, next) =>
                      {
                          bool isAuth=false;
                          //TODO: run logic to check authentication
                          //....
      
                          if (!isAuth)
                          {
                              await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                          }
                          else
                          {
                              await next.Invoke();
                          }
      
                      });
                      //to SPA index.html
                      app.Use(next => context =>
                      {
                          context.Request.Path = "/" + filePath;
                          // Set endpoint to null so the static files middleware will handle the request.
                          context.SetEndpoint(null);
                          return next(context);
                      });
                      app.UseStaticFiles();
                      return app.Build();
                  }
                  //
              });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-08-10
        • 1970-01-01
        • 2016-04-10
        • 1970-01-01
        • 2019-02-01
        • 2018-01-14
        • 2019-08-11
        相关资源
        最近更新 更多