【问题标题】:Implement Remember Me utilizing Cookie with expiration time instead of LocalStorage使用带有过期时间的 Cookie 而不是 LocalStorage 实现记住我
【发布时间】:2022-12-12 16:18:45
【问题描述】:

很长一段时间以来,我一直在努力使它正常工作,但不知道如何正确地进行。我能够使用 LocalStorage 实现 Rememeber Me。但是,我想使用 cookie 通过 JWT 实现记住我的功能,我可以在其中设置过期时间。我想我弄乱了登录逻辑?有人可以指出这里有什么问题吗?

如有必要,我还可以从我的应用程序中添加其他部分。

授权控制器.cs:

[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
  ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);

  if (user == null)
  {
    List<string> errors = new List<string>();
    errors.Add("No such user has been found.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  bool emailConfirmed = await this.UserManager.IsEmailConfirmedAsync(user);

  if (!emailConfirmed)
  {
    List<string> errors = new List<string>();
    errors.Add("Email not confirmed.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  Microsoft.AspNetCore.Identity.SignInResult result =
    await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);

  if (!result.Succeeded)
  {
    List<string> errors = new List<string>();
    errors.Add("Email and password are invalid.");
    return BadRequest(new LoginResult
    {
      Successful = false,
      Errors = errors,
    });
  }

  IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);

  List<Claim> claims = new List<Claim>
  {
    new Claim(ClaimTypes.Name, login.Email)
  };

  ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
  ClaimsPrincipal principal = new ClaimsPrincipal(identity);
  AuthenticationProperties props = new AuthenticationProperties
  {
    IsPersistent = true,
    ExpiresUtc = DateTime.UtcNow.AddMonths(1)
  };

  // to register the cookie to the browser
  this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();

  foreach (string role in roles)
  {
    claims.Add(new Claim(ClaimTypes.Role, role));
  }

  SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
  SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
  DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));

  JwtSecurityToken token = new JwtSecurityToken(
    this.Configuration["JwtIssuer"],
    this.Configuration["JwtAudience"],
    claims,
    expires: expiry,
    signingCredentials: creds
  );

  return Ok(new LoginResult
  {
    Successful = true,
    Token = new JwtSecurityTokenHandler().WriteToken(token),
  });
}

启动.cs:

  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
      options.TokenValidationParameters = new TokenValidationParameters
      {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = Configuration["JwtIssuer"],
        ValidAudience = Configuration["JwtAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
      };
    })
    .AddCookie(options =>
     {
       options.Cookie.Name = "MySpecialCookie";
       options.LoginPath = "/login";
       //options.LogoutPath = "/Home/Index";
       //options.AccessDeniedPath = "AccessDenied";
       options.ExpireTimeSpan = TimeSpan.FromDays(30);
       options.SlidingExpiration = true; // the cookie would be re-issued on any request half way through the ExpireTimeSpan
                                         //options.Cookie.Expiration = TimeSpan.FromDays(5);
       options.EventsType = typeof(CookieAuthEvent);
     });
  services.AddScoped<CookieAuthEvent>();

  services.AddAuthorization(config =>
  {
    config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
    config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
  });

  services.ConfigureApplicationCookie(options =>
  {
    options.Cookie.HttpOnly = true;
    options.Events.OnRedirectToLogin = context =>
    {
      context.Response.StatusCode = 401;
      return Task.CompletedTask;
    };
  });

在客户端,我目前正在使用 AuthorizeApi 和 LocalStorage。这行得通,但我想将其移至 Cookie。

授权API.cs:

public async Task<LoginResult> Login(LoginModel loginModel)
{
  //var stringContent = new StringContent(JsonSerializer.Serialize(LoginModel), Encoding.UTF8, "application/json");
  HttpResponseMessage responseMessage = await this.HttpClient.PostAsJsonAsync("Authorize/Login", loginModel);
  LoginResult result = await responseMessage.Content.ReadFromJsonAsync<LoginResult>();

  if (result.Successful)
  {
    if (loginModel.RememberMe)
    {
      await this.LocalStorage.SetItemAsync("MySpecialToken", result.Token);
    }

    ((ApiAuthenticationStateProvider)this.AuthenticationStateProvider).MarkUserAsAuthenticated(result.Token);
    this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);

    return result;
  }

  return result;
}

ApiAuthenticationStateProvider.cs:

public void MarkUserAsAuthenticated(string token)
{
  ClaimsPrincipal authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
  Task<AuthenticationState> authState = Task.FromResult(new AuthenticationState(authenticatedUser));
  NotifyAuthenticationStateChanged(authState);
}

【问题讨论】:

    标签: c# asp.net jwt blazor


    【解决方案1】:

    在一个较旧的项目中,我创建了使用 cookie 来存储会话标识符的自定义身份验证。

    这个过程很简单。首先添加一个服务来存放当前用户会话的令牌,该令牌可能会发送到您的 API,或者在身份验证后立即更新您注入的HttpClient,或者一旦您从曲奇饼:

    if (!httpClient.DefaultRequestHeaders.Contains("SessionID"))
        httpClient.DefaultRequestHeaders.Add("SessionID", await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID"));
    

    然后在你的MainLayout 上检查 cookie 是否有值,然后再导航到身份验证。为此,我使用了以下 JavaScript:

    window.MyJs = {
        Cookies: {
            Set: function (name, value, date) {
                var d = new Date(date);
                var expires = "expires=" + d.toUTCString();
                document.cookie = name + "=" + value + ";" + expires + ";path=/";
            },
            Get: function (name) {
                name = name + "=";
                var ca = document.cookie.split(';');
                for (var i = 0; i < ca.length; i++) {
                    var c = ca[i];
                    while (c.charAt(0) == ' ') {
                        c = c.substring(1);
                    }
                    if (c.indexOf(name) == 0) {
                        return c.substring(name.length, c.length);
                    }
                }
                return "";
            },
            Remove: function (name) {
                RadixTrie.Cookies.Set(name, "", "01 Jan 1970 00:00:00 UTC");
            }
        }
    }
    

    像这样检查 MainLayout:

    protected override async Task OnAfterRenderAsync(bool firstRender) {
        if (firstRender)
        {
            ...
            var sessionID = await JSRuntime.InvokeAsync<string>("MyJs.Cookies.Get", "SessionID");
    
            if (string.IsNullOrWhiteSpace(sessionID))
                NavigationManager.NavigateTo("Auth/Login", true);
            ...
        }
    }
    
    

    通过身份验证后,您只需设置 cookie:

    internal async Task Login() {
        ...
        await JSRuntime.InvokeVoidAsync("MyJs.Cookies.Set", "SessionID", loginResponse.Token, loginResponse.Expires);
        //{loginResponse.Token:string} {loginResponse.Expires:datetime}
        ...
    }
    

    编辑:

    我对这个答案不满意,花了一些时间考虑替代方案。我没有完全编码的解决方案,但是实现令牌进行身份验证的更好方法,并且在您的情况下保持会话有效,将在您的 API 响应中使用 Set-Cookie 标头。

    我建议创建 middleware 来处理令牌的读取和重置。

    但让我们从登录开始。用户通过身份验证后,您可以更新端点中的响应,例如:

    [HttpPost]
    public async Task<IActionResult> Login([FromBody] LoginModel login)
    {
        ...
        Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
        ...
        return Ok();
    }
    
    

    此时使令牌和 cookie 字符串生成可重用是个好主意。还要考虑加密。

    此后,将 middleware 添加到您的 API,并在每次请求时读取 cookie 以获取令牌:

    public async Task Invoke(HttpContext context)
    {
        ...
        context.Request.Cookies.TryGetValue("SessionID", out string sessionID);
        ...
        await _next(context);
        ...
        //Reset the token after each request for improved security
        context.Response.Headers.Add("Set-Cookie", $"SessionID={Guid.NewGuid()}; Expires={DateTime.Now.AddMonths(1).ToString("dd MMM yyyy hh:mm:ss") + " UTC"}; HttpOnly"); //Valid for 1 month, HttpOnly
    }
    

    【讨论】:

    • 谢谢你的回答,但我认为应该有一些本机的方式来实现这个而不使用定制的 javascript?
    • 这些类型的 cookie 不应该是仅限 http 的吗?
    • 好吧,如果没有处理 JWT 的本机方法并且定制的 JavaScript 是唯一的方法,我想这一定是一个答案。我不是 Blazor 和 asp.net 世界的最高专业人士。如果没有 JWT 使用 asp.net 本机解决方案,会有多少不安全的网站?
    • @10101,我有时间思考,不认为这是最好的解决方案。我可以猜到,但还没有实施。您可以在登录后使用 Set-Cookie 响应标头让 API 控制所有令牌。作为回报,从后续请求中读取该 cookie。我做了一个简短的测试,它应该可以工作。另一个改进是在每次请求后重置该 cookie。
    • @10101,给我一些时间,我会在这里更新我的答案。
    猜你喜欢
    • 2011-04-19
    • 2017-05-12
    • 2012-02-28
    • 2015-04-04
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 2012-12-03
    • 2010-12-23
    相关资源
    最近更新 更多