SignInManager.SignInAsync 不会让我登录 (Vue.js SPA)



我在由dotnet core 2.1 API后端和Vue.js SPA前端组成的应用程序上遇到了一些奇怪的身份验证问题。

我已经准备好了所有关键元素和层,可以使用身份验证 API 控制器在后端登录和注销,并且在 Vue 应用程序上有一个基本的登录页面,我可以使用它登录。

前端应用程序由 3 个关键页面组成,目前"登陆"(单击此处登录行),登录用户的"主页"和登录页面本身。

如果我登录,那么登录 API 调用会返回我的基本用户数据和角色,所以我将其存储在 Vuex 状态并使用它来加载主页。但是,如果我尝试通过将页面刷新回根 URL 来返回登录页面,那么 Vue 应用程序应该触发一个例程,该例程将评估用户状态并相应地进行调整(它首先通过检查 Vuex 状态来执行此操作,如果为空,则调用身份验证控制器以获取用户和角色数据并重新填充自身)所以, 当我登录时,我应该被重定向回登录的"主页"页面。这不会发生 - 相反,会显示"登陆"(未登录)页面。

我有一些涓涓细流的一般日志记录,并且随着我的使用,我添加了更多日志,很明显,当我进行第二次调用时,由于某种原因,我不再获得授权,尽管我一生都无法弄清楚原因。

我之前已经非常成功地使用了这个设置 - 唯一的区别是开发API后端是 ASP.Net 核心而不是.net Framework。

我对从哪里开始看这个的任何想法持开放态度?

这是我的启动配置...

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
ApplicationDbContext context, RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
if (env.IsDevelopment())
{
app
.UseDeveloperExceptionPage()
.UseDatabaseErrorPage();
}
else
{
app
.UseExceptionHandler("/Home/Error")
.UseHsts();
}
app
.UseAuthentication()
.UseHttpsRedirection()
.UseStaticFiles()
.UseSpaStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "VueApp";
if (env.IsDevelopment())
{
spa.UseVueCliServer("serve");
}
});
DbInitializer.Initialize(context, roleManager, userManager, env, loggerFactory);
}

和服务...

public void ConfigureServices(IServiceCollection services)
{
services
.AddLogging(builder => builder
.AddConsole()
.AddDebug());
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
// In production, the Vue files will be served from this directory
services
.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "wwwroot";
});
services
.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyConnection")))
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// User settings
options.User.RequireUniqueEmail = true;
})
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>() // Not sure I need this - added to see if it made things better but it didn't
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services
.ConfigureEntityServices()
.ConfigureIdentityDependencies()
.ConfigureDomainServices();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = cxt =>
{
cxt.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = cxt =>
{
cxt.Response.StatusCode = 403;
return Task.CompletedTask;
};
options.Events.OnRedirectToLogout = cxt => Task.CompletedTask;
});
}

我还尝试将仅开发 CORS 策略添加到启动,以允许开发模式下的任何内容排除这种情况 - 这没有区别。

我的登录方法看起来像这样...

public async Task<ActionResult<CurrentUserJsonModel>> LogIn(LoginJsonModel model)
{
if (model == null) return BadRequest();
if (!ModelState.IsValid) return BadRequest(ModelState);
var result = await authService.LogInAsync(model.UserName, model.Password);
if (!result.Succeeded)
return BadRequest(result.Errors.Select(e => new ErrorJsonModel(e)).ToArray());
var principal = User;
return new CurrentUserJsonModel
{
UserName = result.UserName,
Roles = await authService.GetRolesAsync(model.UserName, model.Password)
};
}

切线:(var 主线是在单步执行时测试当前状态 - 虽然不为空,但在调用 LogInAsync 后,当我单步执行时,用户在标识对象中没有用户名或声明(我认为它应该在这一点上这样做......

它调用一个看起来像这样的authService.LogInAsync方法...

public async Task<AuthResult> LogInAsync(ApplicationUser user)
{
if (user != null)
{
await identityService.SignOutAsync();
await identityService.SignInAsync(user);
}
return user != null
? new AuthResult(user)
: new AuthResult("Password or Email address incorrect.");
}

它使用身份服务来包装对用户管理器、登录管理器和角色管理器功能的访问...

public async Task SignInAsync(ApplicationUser user, bool isPersistent = true, string authenticationMethod = null)
=> await signInManager.SignInAsync(user, isPersistent, authenticationMethod);

当我在登录后端但清除 Vuex 状态(通过刷新页面)后加载应用程序时,应用程序会尝试调用此方法......

[Authorize]
[HttpGet]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<CurrentUserJsonModel>> Get()
{
if (HttpContextAccessor.HttpContext.User != null)
{
if (HttpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
return new CurrentUserJsonModel
{
UserName = HttpContextAccessor.HttpContext.User.Identity.Name,
Roles = await authService.GetRolesAsync(HttpContextAccessor.HttpContext.User)
};
return Forbid();
}
return Unauthorized();
}

当调试该方法甚至没有进入时 - 我在浏览器控制台上得到了一个 401 (如果授权失败,这将是有意义的)。

为了完整起见,这是我的 vue.config.js

const baseUrl = ''
module.exports = {
publicPath: baseUrl + '/',
// place our built files into the parent project where they will be copied
// into the distribution for deployment
outputDir: '../wwwroot',
filenameHashing: true, //process.env.NODE_ENV === 'production',
lintOnSave: 'error',
css: {
modules: false,
sourceMap: process.env.NODE_ENV !== 'production',
loaderOptions: {
sass: {
data: `
$fa-font-path: ${process.env.NODE_ENV !== 'production' ? '"~/fonts"' : '"' + baseUrl + '/fonts"'};
@import "@/scss/base/index.scss";
@import "@/scss/helpers/index.scss";
`
}
}
},
devServer: {
host: 'localhost',
port: 8080,
hot: true,
open: true,
openPage: '',
overlay: true,
disableHostCheck: true,
proxy: {
// Proxy services to a backend API
'/api': {
target: process.env.PROXY || 'https://localhost:44368',
secure: false,
changeOrigin: true
}
}
},
// these node_modules use es6 so need transpiling for IE
transpileDependencies: [
]
}

我在Visual Studio中的控制台输出看起来像这样...

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:44368/api/auth  
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Get", controller = "Auth"}. Executing action Ddm.Lcp.Iptn.Dashboard.Web.Api.AuthController.Get (Ddm.Lcp.Iptn.Dashboard.Web)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: Cookies was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action Ddm.Lcp.Iptn.Dashboard.Web.Api.AuthController.Get (Ddm.Lcp.Iptn.Dashboard.Web) in 10.8469ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 18.1495ms 401 

这表明 cookie 正在受到挑战,因为它们应该给我设置 - 确实感觉好像创建的实际登录实际上不起作用。

问题是...为什么会失败?

更新:根据@Kirk,它可能与Cookie和Identity.Application之间的方案不匹配有关。我已经通过注释掉 Startup 中的 cookie 元素进行了测试,在这种情况下,我发现自己处于递归循环中 - 我将 cookie 元素添加到 Startup 中.cs以使我能够从授权结果中获得合理的响应。

我需要能够执行此操作,但不会被重定向到(在我的应用程序中)不存在且不会存在的登录页面,因为它是 SPA。

signInManager.SignInAsync(...)使用Identity.Application方案,而不是已注册并配置为默认值的Cookies方案。要使用身份并自定义其使用的 Cookie,请使用ConfigureApplicationCookie

替换以下内容:

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = cxt =>
{
cxt.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = cxt =>
{
cxt.Response.StatusCode = 403;
return Task.CompletedTask;
};
options.Events.OnRedirectToLogout = cxt => Task.CompletedTask;
});

-跟-:

services.ConfigureApplicationCookie(options =>
{
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = cxt =>
{
cxt.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = cxt =>
{
cxt.Response.StatusCode = 403;
return Task.CompletedTask;
};
options.Events.OnRedirectToLogout = cxt => Task.CompletedTask;
});

最新更新