【问题标题】:Blazor-Server side authentication with Cookie使用 Cookie 进行 Blazor 服务器端身份验证
【发布时间】: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" alt="Logo" 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.BaseUri}authentication/login", stringContent);
                if (result.StatusCode == System.Net.HttpStatusCode.OK)
                {                       
                    NavMan.NavigateTo("/", true);
                }
                else
                {
                    ErrorMessage = await result.Content.ReadAsStringAsync();
                }                 
            }
        }
    }

【问题讨论】:

  • 有人可以帮忙吗?

标签: cookies blazor blazor-server-side


【解决方案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 数量的声明。 :)
  • 是的,你已经明白了。为了增加安全性,您甚至可以在前端验证令牌签名,前提是您使用的是非对称密钥签名。一切尽在你想如何实现它。不管怎样,虽然我希望这应该给你你所需要的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-30
  • 1970-01-01
  • 1970-01-01
  • 2021-02-27
  • 2020-02-27
  • 2017-06-30
相关资源
最近更新 更多