我正在开发 SPA Web 应用程序,并且使用 IdentityServer4 代码流来处理授权。所以我有以下组件:
https://localhost:5001
https://localhost:5001
http://localhost:8100
现在我正在尝试对移动应用程序用户进行身份验证,但是用户交互登录屏幕不断重定向,我得到了
login_required
。
追踪电话,这就是我得到的:
/connect/authorize
端点 - 检查https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256
- 检查login
- Check中调用
AccountController.cs
https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256
- 检查https://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpbx8alT61z%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DrGappKbnVpUNzlNHst4t5RlHephWFfJTVXuwtpQ8tZI%26code_challenge_method%3DS256
-问题在这里IdentityServer无法感知用户现在已登录。我在调试终端中不断收到login_required
错误.这是我的设置:
Startup.cs
namespace Charla
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityCore<ApplicationUser>(options => { });
new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<ConverseContext>();
/*services.AddIdentity<ApplicationUser, IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<ConverseContext>()
.AddDefaultTokenProviders();*/
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var builder = services
.AddIdentityServer(SetupIdentityServer)
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
sqloptions => {
sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
sqloptions.MigrationsAssembly(migrationsAssembly);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
sqloptions => {
sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
sqloptions.MigrationsAssembly(migrationsAssembly);
});
});
/*services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => {
options.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
options.RequireHttpsMetadata = false;
options.ApiName = "charla-api";
});*/
services.AddAuthentication(opt => {
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(jwt =>{
jwt.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
jwt.RequireHttpsMetadata = false;
jwt.TokenValidationParameters.ValidateAudience = false;
jwt.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
//endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/hub");
});
}
private static void SetupIdentityServer(IdentityServerOptions options)
{
options.UserInteraction.LoginUrl = "/auth/login";
options.UserInteraction.LoginReturnUrlParameter = "returnUrl";
options.UserInteraction.LogoutUrl = "/logout";
options.UserInteraction.ErrorUrl= "/error/identity";
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
//options.EmitStaticAudienceClaim = true;
//identityServerOptions.Authentication.CookieLifetime = TimeSpan.FromDays(1);
}
}
}
}
AccountController.cs
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class AccountController : ControllerBase
{
[HttpPost("login")]
public async Task<IActionResult> Login(UserResource model) {
var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);
var context = await interaction.GetAuthorizationContextAsync(model.return_url);
if (result.Succeeded) {
var uo = db.Users.Include(q => q.UserOrganization).Single( q => q.Email == model.email ).UserOrganization.First();
uo.LastLogin = DateTime.UtcNow;
await db.SaveChangesAsync();
// let identity server know that we loggedin
await identityEvents.RaiseAsync(new UserLoginSuccessEvent(
model.email, uo.UserId, model.email, clientId: context?.Client.ClientId
));
//return Redirect(model.return_url);
return Ok( new{
email = model.email,
return_url = context.RedirectUri
} );
}
await identityEvents.RaiseAsync(new UserLoginFailureEvent(model.email, "invalid credentials", clientId:context?.Client.ClientId));
return NotFound(new {});
}
IdentityConfig.cs - 我正在使用 EF 表,但它是从下面播种的:
using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;
namespace Charla
{
public static class IdentityConfig
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("charla-api", "Charla API Resource")
};
}
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("charla-api", "Charla API Scope")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// charla web app
new Client
{
ClientId = "charla-spa",
ClientName = "Charla Web App",
RequireClientSecret = false,
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = {
"https://localhost:5001/authentication/login-callback",
"https://app-dev.getcharla.com/authentication/login-callback",
"https://app.getcharla.com/authentication/login-callback"
},
//FrontChannelLogoutUri = "https://localhost:5001/authentication/logout-callback",
PostLogoutRedirectUris = {
"https://localhost:5001/authentication/logout-callback",
"https://app-dev.getcharla.com/authentication/logout-callback",
"https://app.getcharla.com/authentication/logout-callback"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"charla-api"
}
},
// mobile client
new Client
{
ClientId = "charla-mobile",
ClientName = "Charla Mobile Apps",
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
RedirectUris = {
"https://getcharla.com/ios_redirect",
"http://localhost:8100/auth/callback"
},
PostLogoutRedirectUris = {
"https://getcharla.com/ios_redirect_endsession",
"http://localhost:8100/auth/endsession"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"charla-api"
},
AllowedCorsOrigins = { "http://localhost:8100", "https://localhost:5001", "http://localhost:5000" }
},
};
}
}
login.component.ts - Web 组件中的函数,触发对
login
端点的调用并执行重定向:
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.returnUrl = params['returnUrl'] || '/';
});
}
/**
* Form Submit
*/
submit() {
const controls = this.loginForm.controls;
const authData = {
email: controls['email'].value,
password: controls['password'].value
};
this.auth
.login(authData.email, authData.password, this.returnUrl)
.pipe(
finalize(() => {
this.loading = false;
this.cdr.markForCheck();
})
)
.subscribe(
result => {
window.location = this.returnUrl;
this.router.navigateByUrl(this.returnUrl); // Main page
},
err => {
console.log(err);
if (( 'status' in err) && ( err.status === 404)){
this.authNoticeService.setNotice(this.translate.instant('AUTH.VALIDATION.INVALID_LOGIN'), 'danger');
}
}
);
}
不知道为什么它不起作用。我读到我应该在
HttpContent.SignInAsync
Web api 端点中使用 login
,但我已经在使用 var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);
,所以我认为这就足够了。
我不确定是否正确的其他选择,例如使用
AddIdentityCore
而不是 AddIdentity
。我应该添加AddAspNetIdentity<ApplicationUser>()
吗?
按照您提供的跟踪,由于浏览器上有关 Cookie 的新规则,可能会发生这种情况。
在新的 ASP.NET Core 应用程序上,会自动添加
samesite=none
属性,但浏览器要求您也指定 Secure
属性,否则 Set-Cookie 将被阻止。
要配置 IdentityServer,请在
Configure
类的 Startup
方法中添加以下代码块:
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = HttpOnlyPolicy.None,
MinimumSameSitePolicy = SameSiteMode.None,
Secure = CookieSecurePolicy.Always
});
使用安全属性时必须使用HTTPS,否则会失败
关于该主题的另一个问题:Session cookie set `SameSite=None;安全;`不起作用
以及 Microsoft 关于 ASP.NET Core 的一些信息:https://learn.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.0
注销并重新登录时,我的 SPA 也遇到了类似的问题。该问题似乎是由 Visual Studio 更新引起的。如果您使用命令提示符导航到项目文件夹并使用
dotnet run
' 或什至更好的 dotnet watch run
(这样您就可以在 .net core 代码运行时对其进行更改),您可以解决该问题;购买时不要在 Visual Studio 中通过 IIS 运行代码。
如果这对您有用,请将属性启动更新为项目(也使用 dotnet run),然后您也可以进行调试。
为了同时使用 http 和 https,我配置了 cookie 策略,如下所示:
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = HttpOnlyPolicy.None,
MinimumSameSitePolicy = SameSiteMode.Lax,
Secure = CookieSecurePolicy.SameAsRequest
});
此配置更正了 Chrome/Edge 到登录页面的重定向。