【问题标题】:OIDC authentication in server-side Blazor服务器端 Blazor 中的 OIDC 身份验证
【发布时间】:2021-02-27 09:24:20
【问题描述】:

我使用了这种方法,但不知何故这是不对的,因为@attribute [AllowAnonymous] 并没有真正起作用,所以我使用[Authorized] 属性而不是[AllowAnonymous],然后删除RequireAuthenticatedUser,但OIDC 不会将客户端重定向到服务器登录页面。

我检查了SteveSanderson github article 关于 blazor 中的身份验证和授权,但他没有谈论 OIDC。

那么我该如何处理呢?

启动类:

services.AddAuthentication(config =>
{
    config.DefaultScheme = "Cookie";
    config.DefaultChallengeScheme = "oidc";
})
    .AddCookie("Cookie")
    .AddOpenIdConnect("oidc", config =>
    {
        config.Authority = "https://localhost:44313/";
        config.ClientId = "client";
        config.ClientSecret = "secret";
        config.SaveTokens = true;
        config.ResponseType = "code";
        config.SignedOutCallbackPath = "/";
        config.Scope.Add("openid");
        config.Scope.Add("api1");
        config.Scope.Add("offline_access");
    });

services.AddMvcCore(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser() // site-wide auth
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
});

【问题讨论】:

    标签: asp.net-core blazor blazor-server-side asp.net-authentication


    【解决方案1】:

    以下是该问题的完整且有效的解决方案:

    首先,您需要提供一种身份验证质询请求机制,以支持重定向到身份验证代理,例如 IdentityServer。这仅适用于 HttpContext,它在 SignalR(Blazor 服务器应用程序)中不可用。为了解决这个问题,我们将添加几个 HttpContext 可用的 Razor 页面。更多答案...

    创建 Blazor 服务器应用程序。

    Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.0 或更高版本。

    创建一个名为 LoginDisplay (LoginDisplay.razor) 的组件,并将其放置在 共享文件夹。该组件用于MainLayout组件中:

    <AuthorizeView>
        <Authorized>
            <a href="logout">Hello, @context.User.Identity.Name !</a>
            <form method="get" action="logout">
                <button type="submit" class="nav-link btn btn-link">Log 
                       out</button>
            </form>
        </Authorized>
        <NotAuthorized>
            <a href="login?redirectUri=/">Log in</a>
        </NotAuthorized>
     </AuthorizeView>
    

    将 LoginDisplay 组件添加到 MainLayout 组件中,就在 About 上方 锚元素,像这样

    <div class="top-row px-4">
        <LoginDisplay />
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>
    

    注意:为了将登录和注销请求重定向到 IdentityServer,我们必须创建两个 Razor 页面,如下所示:

    1. 创建一个 Login Razor 页面 Login.cshtml (Login.cshtml.cs) 并将它们放在 Pages 文件夹中,如下所示: 登录.cshtml.cs

    using Microsoft.AspNetCore.Authentication;
     using Microsoft.AspNetCore.Authentication.OpenIdConnect;
     using Microsoft.AspNetCore.Authentication.Cookies;
     using Microsoft.IdentityModel.Tokens;
    
    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            await HttpContext.ChallengeAsync("oidc", new 
                AuthenticationProperties { RedirectUri = redirectUri } );
        }  
    }
    

    此代码启动您在 Startup 类中定义的 Open Id Connect 身份验证方案的质询。

    1. 创建一个 Logout Razor 页面 Logout.cshtml (Logout.cshtml.cs) 并将它们也放在 Pages 文件夹中: 注销.cshtml.cs

    using Microsoft.AspNetCore.Authentication;
    
    public class LogoutModel : PageModel
     {
        public async Task<IActionResult> OnGetAsync()
        {
            await HttpContext.SignOutAsync();
            return Redirect("/");
        }
    }
    

    此代码将您注销,将您重定向到 Blazor 应用的主页。

    将 App.razor 中的代码替换为以下代码:

    @inject NavigationManager NavigationManager
    
    <CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @{
                        var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                        
                        NavigationManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);
                        
                    }
    
                </NotAuthorized>
                <Authorizing>
                    Wait...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
    
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
    
        </NotFound>
    
    </Router>
    </CascadingAuthenticationState>
    

    将 Startup 类中的代码替换为以下内容:

    using Microsoft.AspNetCore.Authentication.OpenIdConnect; 
    using Microsoft.AspNetCore.Authorization; 
    using Microsoft.AspNetCore.Mvc.Authorization; 
    using System.Net.Http;
    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.IdentityModel.Tokens;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.IdentityModel.Logging;
    
    
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddAuthorizationCore();
            services.AddSingleton<WeatherForecastService>();
                        
            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultAuthenticateScheme = 
                     CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultSignInScheme = 
                    CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = 
                   OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect("oidc", options =>
             {
                 options.Authority = "https://demo.identityserver.io/";
                 options.ClientId = "interactive.confidential.short"; 
                 options.ClientSecret = "secret";
                 options.ResponseType = "code";
                 options.SaveTokens = true;
                 options.GetClaimsFromUserInfoEndpoint = true;
                 options.UseTokenLifetime = false;
                 options.Scope.Add("openid");
                 options.Scope.Add("profile");
                 options.TokenValidationParameters = new 
                        TokenValidationParameters
                        {
                            NameClaimType = "name"
                        };
                        
                 options.Events = new OpenIdConnectEvents
                 {
                   OnAccessDenied = context =>
                            {
                              context.HandleResponse();
                              context.Response.Redirect("/");
                              return Task.CompletedTask;
                           }
           };
     });
    
    }
    
    
      // This method gets called by the runtime. Use this method to configure 
         the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
    
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
           
    
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }
    }
    

    重要提示:在上面的所有代码示例中,您必须根据需要添加 using 语句。其中大部分是默认提供的。此处提供的使用是启用身份验证和授权流程所必需的。

    • 运行您的应用程序,单击登录按钮进行身份验证。您将被重定向到允许您执行 OIDC 登录的 IdentityServer 测试服务器。您可以输入用户名:bob 和密码bob,点击确定按钮后,您将被重定向到您的主页。另请注意,您可以使用外部登录提供商 Google(尝试一下)。请注意,在您使用身份服务器登录后,LoginDisplay 组件会显示字符串 "Hello, &lt;your user name&gt;"

    注意:当你在试验你的应用时,你应该清除浏览数据,如果你想被重定向到身份服务器的登录页面,否则你的浏览器可能会使用缓存的数据。请记住,这是一种基于 cookie 的授权机制...

    请注意,在这里创建登录机制不会使您的应用程序比以前更安全。任何用户都无需登录即可访问您的网络资源。为了保护您网站的某些部分,您还必须实施授权,通常,经过身份验证的用户有权访问受保护的资源,除非实施其他措施,例如角色、策略等。以下是演示如何您可以保护您的 Fetchdata 页面免受未经授权的用户的攻击(同样,经过身份验证的用户被视为有权访问 Fetchdata 页面)。

    在 Fetchdata 组件页面的顶部,为 Authorize 属性添加 @attribute 指令,如下所示:@attribute [Authorize] 当未经身份验证的用户尝试访问 Fetchdata 页面时,会执行 AuthorizeRouteView.NotAuthorized 委托属性,因此我们可以添加一些代码将用户重定向到同一身份服务器的登录页面进行身份验证。

    NotAuthorized 元素中的代码如下所示:

    <NotAuthorized>
        @{
            var returnUrl = 
            NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
            NavigationManager.NavigateTo($"login?redirectUri= 
                                  {returnUrl}", forceLoad: true);
         }
    </NotAuthorized>
    

    这会检索您尝试访问的最后一个页面的 url,即 Fetchdata 页面,然后导航到执行密码质询的 Login Razor 页面,即,用户被重定向到身份服务器的登录页面进行身份验证。

    用户通过身份验证后,他被重定向到 Fetchdata 页面。

    【讨论】:

      【解决方案2】:

      对于服务器端 Blazor,身份验证发生在托管 Blazor 应用程序的 Razor 页面上。对于默认模板,这是 _Host.cshtml Razor 页面,它被配置为 服务器端 路由的后备页面。由于该页面就像一个普通的 Razor 页面,您可以在那里使用 [Authorize][AllowAnonymous] 属性。

      您对_Host.cshtml 应用的任何授权都会影响对 Blazor 应用程序本身的一般访问权限的授权方式。如果您只希望经过身份验证的用户访问该应用程序,则应要求授权;如果您希望任何个未经身份验证的用户访问该应用程序,您无法保护应用程序访问本身。

      页面的授权并不意味着你不能在你的应用中拥有更细粒度的授权。您仍然可以对应用程序中的特定组件使用不同的规则和策略。为此,您可以使用&lt;AuthorizeView&gt; 组件。

      服务器端 Blazor 可能存在两种常见情况:

      • 对整个 Blazor 应用程序的访问仅限于经过身份验证的用户。未通过身份验证的用户应立即进行身份验证(例如使用 OIDC),以免匿名用户点击应用。

        在这种情况下,通过要求经过身份验证的用户(通过[Authorize] 属性或在AddRazorPages() 调用中使用约定)来保护_Host.cshtml 就足够了。

        在未经身份验证的情况下访问 Blazor 应用程序时,默认授权中间件将引发身份验证质询并重定向到 OIDC 登录。

      • 未经身份验证的用户应该能够访问 Blazor 应用程序,但 Blazor 应用程序将使用&lt;AuthorizeView&gt;IAuthorizationService 使用更详细的授权。

        在这种情况下,_Host.cshtml 必须不受保护,因为匿名用户需要访问它。这也意味着作为 Razor 页面的一部分运行的默认授权中间件不会执行任何操作。所以你必须自己应对挑战。

        执行此操作的“简单”方法是提供指向不同服务器端路由的登录链接,然后该路由将触发身份验证质询并重定向到 OIDC 登录。例如,您可以有这样的 MVC 操作:

        [HttpGet("/login")]
        public IActionResult Login()
            => Challenge();
        

        在您的 Blazor 应用中,您现在可以添加指向此路由的链接并允许用户以这种方式登录:

        <AuthorizeView>
          <Authorized>
            Signed in as @context.User.Identity.Name.
          </Authorized>
          <NotAuthorized>
            <a href="/login">Sign in here</a>
          </NotAuthorized>
        </AuthorizeView>
        

      【讨论】:

      • 谢谢,但是在第二种情况下如何生成 returnUrl ?
      • 您可以在 Blazor 中使用 NavigationManager 访问当前 URL,然后您可以将其附加到 /login URL。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-04-30
      • 1970-01-01
      • 2023-03-03
      • 2021-10-25
      • 2021-12-05
      • 2022-12-28
      • 1970-01-01
      相关资源
      最近更新 更多