Пользовательский вход Blazor на стороне сервера с использованием Identity теряет зарегистрированное состояние после перезапуска - PullRequest
0 голосов
/ 14 марта 2020

Я реализовал собственное решение, чтобы войти без ссылки на страницу sh. Однако не могу понять, почему я теряю состояние входа в систему при перезапуске приложения (новый запуск отладки).

Запуск

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);

        services.AddIdentity<User, Role>(options =>
        { // options...
        }).AddEntityFrameworkStores<DatabaseContext>().AddDefaultTokenProviders();
        services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("/keys")).SetApplicationName("App").SetDefaultKeyLifetime(TimeSpan.FromDays(90));
        services.AddRazorPages();
        services.AddServerSideBlazor().AddCircuitOptions(options =>
        {
            options.DetailedErrors = true;
        });

        services.AddSession();
        services.AddSignalR();
        services.AddBlazoredLocalStorage();
        services.AddHttpClient();

        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();

        services.AddAuthorization(options =>
        { // options...
        });

        // Add application services.
        services.AddScoped<AuthenticationStateProvider, IdentityAuthenticationStateProvider>();

        // .. services

        services.AddMvc(options =>
        {
            options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
        }).AddSessionStateTempDataProvider();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseSession();
        app.UseStaticFiles();
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

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

AuthorizeController

    [HttpPost("Login")]
    [AllowAnonymous]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

            Microsoft.AspNetCore.Identity.SignInResult result = await signInMgr.PasswordSignInAsync(model.LEmail, model.LPassword, model.RememberMe, lockoutOnFailure: false);

            if (result.Succeeded)
                return Ok();

            return BadRequest(string.Join(", ", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)));
        }

        return BadRequest();
    }

    [HttpGet("UserInfo")]
    public UserInfo UserInfo()
    {
        return new UserInfo
        {
            IsAuthenticated = User.Identity.IsAuthenticated,
            UserName = User.Identity.Name,
            ExposedClaims = User.Claims.ToDictionary(c => c.Type, c => c.Value)
        };
    }

I полагаю, проблема в моей реализации AuthenticationStateProvider

public class IdentityAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
    readonly AuthorizeService authorizeSvc;
    readonly IServiceScopeFactory scopeFactory;
    readonly IdentityOptions options;

    UserInfo userInfoCache;

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    public IdentityAuthenticationStateProvider(AuthorizeService authorizeService, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory)
    {
        authorizeSvc = authorizeService;
        scopeFactory = serviceScopeFactory;
        options = optionsAccessor.Value;
    }

    public async Task LoginAsync(LoginViewModel model)
    {
        await authorizeSvc.LoginAsync(model);

        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public async Task RegisterAsync(RegisterViewModel register)
    {
        await authorizeSvc.RegisterAsync(register);

        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public async Task LogoutAsync()
    {
        await authorizeSvc.LogoutAsync();

        userInfoCache = null;

        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    async Task<UserInfo> GetUserInfoAsync()
    {
        if (userInfoCache != null && userInfoCache.IsAuthenticated)
            return userInfoCache;

        userInfoCache = await authorizeSvc.GetUserInfo();

        return userInfoCache;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        ClaimsIdentity identity = new ClaimsIdentity();

        try
        {
            UserInfo userInfo = await GetUserInfoAsync();

            if (userInfo.IsAuthenticated)
            {
                IEnumerable<Claim> claims = new[] { new Claim(ClaimTypes.Name, userInfoCache.UserName) }.Concat(userInfoCache.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));

                identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            }
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine("Request failed:" + ex.ToString());
        }

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        // Get the user manager from a new scope to ensure it fetches fresh data
        IServiceScope scope = scopeFactory.CreateScope();
        try
        {
            UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();

            return await ValidateSecurityStampAsync(userManager, authenticationState.User);
        }
        finally
        {
            if (scope is IAsyncDisposable asyncDisposable)
                await asyncDisposable.DisposeAsync();
            else
                scope.Dispose();
        }
    }

    async Task<bool> ValidateSecurityStampAsync(UserManager<User> userManager, ClaimsPrincipal principal)
    {
        User user = await userManager.GetUserAsync(principal);

        if (user is null)
            return false;
        else if (!userManager.SupportsUserSecurityStamp)
            return true;

        string principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
        string userStamp = await userManager.GetSecurityStampAsync(user);

        return principalStamp == userStamp;
    }
}

AuthorizeService просто вызывает httprequests, как

    public async Task<UserInfo> GetUserInfo()
    {
        HttpContext context = contextAccessor.HttpContext;

        HttpClient client = clientFactory.CreateClient();
        client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");

        string json = await client.GetStringAsync("api/Authorize/UserInfo");

        return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
    }

В инструментах разработчика Chrome я заметил, что куки не изменяются после входа в систему. Это, наверное, главная проблема. Есть идеи как это исправить?

Спасибо

1 Ответ

0 голосов
/ 15 марта 2020

AuthorizeService должен передавать куки. Для серверной стороны при предварительном рендеринге передайте куки из HttpContext, во время выполнения передайте куки от javascript через IJSRuntime инъекцию.

Реализация выглядит как для пользовательского AuthenticationStateProvider

    async Task<string> GetCookiesAsync()
    {
        try
        {
            return $".AspNetCore.Identity.Application={await jsRuntime.InvokeAsync<string>("getLoginCookies")}";
        }
        catch
        {
            return $".AspNetCore.Identity.Application={httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"]}";
        }
    }

    public async Task<UserInfo> GetUserInfoAsync()
    {
        if (userInfoCache != null && userInfoCache.IsAuthenticated)
            return userInfoCache;

        userInfoCache = await authorizeSvc.GetUserInfo(await GetCookiesAsync());

        return userInfoCache;
    }

AuthorizeService

    public async Task<UserInfo> GetUserInfo(string cookie)
    {
        string json = await CreateClient(cookie).GetStringAsync("api/Authorize/UserInfo");

        return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
    }

    HttpClient CreateClient(string cookie = null)
    {
        HttpContext context = contextAccessor.HttpContext;

        HttpClient client = clientFactory.CreateClient();
        client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");

        if(!string.IsNullOrEmpty(cookie))
            client.DefaultRequestHeaders.Add("Cookie", cookie);

        return client;
    }

Также необходимо упомянуть необходимость выполнения следующих действий для следующих действий. Вход / Регистрация

  1. Вход
  2. GetUserInfo (включая файлы cookie)
  3. Написать повар ie через javascript

Выйти

  1. Удалить повара ie через javascript
  2. SignOut
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...