使用 Cookie 进行 Blazor 服务器端身份验证

Posted

技术标签:

【中文标题】使用 Cookie 进行 Blazor 服务器端身份验证【英文标题】:Blazor-Server side authentication with Cookie 【发布时间】:2021-12-05 17:40:51 【问题描述】:

我正在尝试在 Blazor-Server 端应用程序上实现针对 LDAP 服务器的简单登录,并使用 cookie 来存储用户声明。我将 MainLayout 设置为 Authorized,如果用户未通过身份验证,它将被重定向到登录页面。我已经测试了 LDAP 连接并且它工作正常,问题是无论我做什么,cookie 都不会在浏览器中创建。当我运行 POST 命令时,我看到 HttpStatusCode.OK 但它没有创建 cookie,浏览器当然会再次重定向到登录页面。

谁能告诉我我做错了什么?我的代码:

Startup.cs

    public void ConfigureServices(IServiceCollection services)
     
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddControllersWithViews().AddRazorRuntimeCompilation();
      services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
    

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        
            endpoints.MapControllers();
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        );
    

AuthenticationController.cs

    [ApiController]
public class AuthenticationController : Controller

    [HttpPost]
    [Route("authentication/login")]
    public async Task<ActionResult> Login([FromBody]UserCredentials credentials)
    
        string path = "LDAP://serveraddress.xxx";
        try
        
            using DirectoryEntry entry = new(path, credentials.Username, credentials.Password);
            using DirectorySearcher searcher = new(entry);
            searcher.Filter = $"(&(objectclass=user)(objectcategory=person)(samaccountname=credentials.Username))";
            var result = searcher.FindOne();
            if (result != null)
            
                List<Claim> claims = new();                 
                claims.Add(new Claim(ClaimTypes.Name, credentials.Username));

                //Get Groups
                ResultPropertyCollection fields = result.Properties;
                foreach (var group in result.Properties["memberof"])
                
                    var distinguishedName = new X500DistinguishedName(group.ToString());
                    var commonNameData = new AsnEncodedData("CN", distinguishedName.RawData);
                    var commonName = commonNameData.Format(false);

                    if (!string.IsNullOrEmpty(commonName))
                    
                        claims.Add(new Claim(ClaimTypes.Role, commonName));
                    
                
                //Get Emails
                foreach (var email in result.Properties["mail"])
                
                    claims.Add(new Claim(ClaimTypes.Email, email.ToString()));
                

                ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);

                AuthenticationProperties authProperties = new()
                
                    AllowRefresh = true,
                    IssuedUtc = DateTime.Now,
                    ExpiresUtc = DateTimeOffset.Now.AddDays(1),
                    IsPersistent = true,
                    
                ;

                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
                return Ok();
            
            else
            
                return NotFound("User Not Found!");
            
        
        catch (Exception)
        
            return NotFound("Login credentials is incorrect!");
        
    

    [HttpPost]
    [Route("authentication/logout")]
    public async Task<IActionResult> Logout()
    
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return Ok();
    

Login.razor

@page "/login"
@page "/login/ErrorMessage"
@layout CenteredBlockLayout
@attribute [AllowAnonymous]

<MudPaper Elevation="25" Class="pa-8" Width="100%" MaxWidth="500px">
    <MudItem><img src="/images/logo.svg"  style="width:400px; height:50px;" /></MudItem>
    <MudText Typo="Typo.h4" GutterBottom="true">Sign In</MudText>
    <MudTextField @bind-Value="@Username" T="string" Label="Username"/>
    <MudTextField @bind-Value="@Password" T="string" Label="Password"/>
    <MudButton OnClick="(() => PerformLoginAsync())">Sign In</MudButton>
</MudPaper>
@if (!string.IsNullOrEmpty(ErrorMessage))

    <MudAlert Severity="Severity.Error">@ErrorMessage</MudAlert>

Login.razor.cs

public partial class Login
       
        public string Username  get; set;     
        public string Password  get; set; 

        [Parameter]
        public string ErrorMessage  get; set; 

        [Inject]
        HttpClient Client  get; set; 

        [Inject]
        private NavigationManager NavMan  get; set; 
  
        private async Task PerformLoginAsync()
        
            if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
            
                UserCredentials cred = new UserCredentials
                
                    Username = Username,
                    Password = Password
                ;

                var serialized = JsonConvert.SerializeObject(cred);
                var stringContent = new StringContent(serialized, Encoding.UTF8, "application/json");

                using var result = await Client.PostAsync($"NavMan.BaseUriauthentication/login", stringContent);
                if (result.StatusCode == System.Net.HttpStatusCode.OK)
                                       
                    NavMan.NavigateTo("/", true);
                
                else
                
                    ErrorMessage = await result.Content.ReadAsStringAsync();
                                 
            
        
    

【问题讨论】:

有人可以帮忙吗? 【参考方案1】:

我相信您需要将 cookie 附加到响应中。我没有用你的代码对此进行测试,但它应该像这样工作:

HttpContext.Response.Cookies.Append("my_cookie", claimsString, new CookieOptions()

    Domain = "mydomain.com",
    SameSite = SameSiteMode.Lax,
    Secure = true,
    Path = "/",
    Expires = DateTime.UtcNow.AddDays(1)

(当然,这些 cookie 选项只是一个示例。根据您的特定需求定制它们。)

请记住,您需要将声明转换为字符串,以便将其作为值存储在 cookie 中。在我们的例子中,我们将声明存储在 JWT 中,这就是存储在 cookie 中的内容。这是我的做法:

public string CreateJWT(HttpContext httpContext, User user)

    var handler = new JwtSecurityTokenHandler();

    var descriptor = new SecurityTokenDescriptor
    
        Subject = new ClaimsIdentity(new Claim[] 
            new Claim(ClaimTypes.GivenName, user.FirstName),
            new Claim(ClaimTypes.Surname, user.LastName),
            new Claim(ClaimTypes.Name, $"user.FirstName user.LastName"),
            new Claim(ClaimTypes.Email, user.Email),
        ),
        Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins),
        Issuer = Config.Issuer,
        Audience = Config.Audience,
        SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256)
    ;

    var token = handler.CreateJwtSecurityToken(descriptor);
    var accessToken = handler.WriteToken(token);

    httpContext.Response.Cookies.Append("my_cookie", accessToken, new CookieOptions()
    
        Domain = Config.CookieDomain,
        SameSite = SameSiteMode.Lax,
        Secure = true,
        Path = "/",
        Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins)
    );

    return accessToken;

至于解析 JWT,我相信有很多方法可以解决。对我有用的是this one。

【讨论】:

您好,首先非常感谢您的帮助!我想问,我是否需要在 SignInAsync() 和 Ok() 之间的控制器代码中附加 cookie?或者我需要在发布请求后在登录页面中执行此操作吗?您最终是否有使用 JWT 存储/检索声明的示例? 乐于助人!我的建议是在 SignInAsync() 和 Ok() 之间进行。我已经更新了我的答案,包括我自己创建 JWT 的示例,以及对解析声明的方法的引用。 哦,我明白了..所以 JWT 的工作方式是用户在登录屏幕中键入 user/pwd,信息被发送到控制器,如果数据正确,则生成带有声明的令牌并返回到登录页面。然后我需要解析令牌,获取所有声明并将它们添加到 Blazor 身份服务,以告知应用用户已通过身份验证并且拥有 X 数量的声明。 :) 是的,你已经明白了。为了增加安全性,您甚至可以在前端验证令牌签名,前提是您使用的是非对称密钥签名。一切尽在你想如何实现它。不管怎样,虽然我希望这应该给你你所需要的。

以上是关于使用 Cookie 进行 Blazor 服务器端身份验证的主要内容,如果未能解决你的问题,请参考以下文章

简单服务器端Blazor Cookie身份验证的演示

如何使用 blazor 创建 cookie 客户端

如何在 Blazor 服务器中设置同意 cookie

如何在没有 Microsoft 身份的情况下对 blazor 服务器进行 jwt 身份验证?

HttpClient 在 Blazor Webassembly 应用程序中不包含带有请求的 cookie

如何从服务器端 Blazor 应用程序中的 Blazor 组件调用 razor 页面而不导致页面刷新