使用 IdentityServer4 从多个 API 创建用户

User creation with IdentityServer4 from multiple API's

所以我已经为这个问题苦恼了一段时间。

我们有一个 Web 应用程序使用 IdentityServer4AspNetIdentity 来验证和注册用户(这是有效的如预期)。 此外,我们还有另一个 API(在同一解决方案中),它能够使用 IdentityServer4 对访问 API 的用户进行身份验证。

但是,问题是,除了身份验证,我们不能使用 API 来 创建新用户

例如,用户应该能够通过网络 API 而不仅仅是从网络应用程序创建其他用户,因为在我们的例子中,用户链接到其他用户(将其视为多个配置文件).

我不是很熟悉 .Net Core 框架提供的所有配置服务,我尝试了多种方法通过 API 访问 Web 应用程序的用户管理器来注册我的用户经典 POST 请求,但似乎没有任何效果。在线搜索很棘手,因为我们的问题非常具体,这就是我在这里发帖的原因。

API Startup.cs - 配置服务:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
     // base-address of your identityserver
     options.Authority = Configuration["IdentityServer:Url"];

     // name of the API resource
     options.ApiName = Configuration["IdentityServer:APIName"];
     options.ApiSecret = Configuration["IdentityServer:APISecret"];

     options.EnableCaching = true;
     options.CacheDuration = TimeSpan.FromMinutes(10); // that's the default

     options.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityServer:RequireHttpsMetadata"]);
});

API Startup.cs - 配置:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseCors("AllowAllOrigins");
    app.UseAuthentication();
    app.UseMvc();
}

API UsersController.cs - 构造函数:

private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _context;

public UsersController(IUserService service,
ApplicationDbContext context,
UserManager<ApplicationUser> userManager)
{
    _service = service;
    _userManager = userManager;
    _context = context;
}

现在的问题是,当我启动 API 并尝试访问 UsersController 时,出现以下错误:

System.InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager`1[XXXXX.Data.Models.ApplicationUser]' while attempting to activate 'XXXXXXX.Api.Controllers.UsersController'.

我真诚地希望至少能找到一些关于如何进行的建议。

如果有什么不清楚的地方请回复,我很乐意添加更多信息或弄清楚。

亲切的问候,

马里奥。

编辑: 谢谢大家的回复。下面由@Vidmantas 提供的代码片段可以解决问题。

由于我对 .net core 的了解有限,我在配置服务功能中进行了很多试验和错误,正如您所想象的那样,这些都没有奏效。我坚信使用 .net core 很容易(例如 API),但是当涉及到配置服务时,复杂性(主要是 puzzling/confusing)会爆炸。

至于架构,你给了我未来重构的好主意。做的笔记。

马里奥。

如果我对您的理解正确,那么您实际上不应该通过 API 创建用户——这就是您拥有 Identity Server 4 的原因——为您的用户群提供中央身份验证权限。您实际需要的是:

  • Identity Server 4 端的一组 API 端点,用于管理 AspNetIdentity
  • 全新的API,但与您的 AspNetIdentity 的 Identity Server 4 共享同一个数据库
  • 让您的 API 共享 AspNet Identity 的数据库

如果你选择最后一个选项,那么你可能需要像下面这样的东西来添加:

services.AddDbContext<IdentityContext>(); //make sure it's same database as IdentityServer4

services.AddIdentityCore<ApplicationUser>(options => { });
new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddSignInManager<SignInManager<ApplicationUser>>()
    .AddEntityFrameworkStores<IdentityContext>();

这将为您提供足够的服务来使用 UserManager,并且不会设置任何不必要的身份验证方案。

由于关注点分离,我不推荐最后一种方法 - 您的 API 应该关注提供资源,而不是创建用户和提供资源。我认为第一种和第二种方法都不错,但我总是倾向于为 AspNetIdentity 管理提供干净的独立服务。

来自我的一个项目的示例架构,我们在其中实现了这种方法:

  • auth.somedomain.com - 带有 AspNetIdentity 的 IdentityServer4 网络应用程序用于用户身份验证。
  • accounts.somedomain.com - 带有 AspNetIdentity(与 Identity Server 4 相同的数据库)的 AspNetCore 网络应用程序用于 AspNetIdentity 用户管理
  • webapp1.somedomain.com - 一个 Web 应用程序,您的所有前端逻辑都驻留在其中(如果 AspNetCore MVC 或类似的东西,当然也可以有后端)
  • api1.somedomain.com - 一个纯粹用于 API 目的的网络应用程序(如果你为前端和后端使用单个应用程序,那么你可以将最后两个结合起来)

我和你的情况差不多

  • 具有 asp .net 身份用户的身份服务器。 (数据库包含客户和用户数据)
  • API(数据库包含对应用程序数据的访问).net Framework
  • 应用程序 .net 框架。

我们的用例是通常会通过身份服务器创建新用户。但是,我们还希望应用程序能够邀请用户。所以我可以登录到应用程序,我想邀请我的朋友。这个想法是,邀请的行为就像用户自己创建一样。

所以它会向我的朋友发送一封附有代码的电子邮件,然后用户就可以提供他们的密码并拥有一个帐户。

为此,我在我的帐户控制器上创建了一个新操作。

[HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> Invited([FromQuery] InviteUserRequest request)
    {

        if (request.Code == null)
        {
            RedirectToAction(nameof(Login));
        }
        var user = await _userManager.FindByIdAsync(request.UserId.ToString());
        if (user == null)
         {
          return View("Error");
        }

        var validateCode = await _userManager.VerifyUserTokenAsync(user, _userManager.Options.Tokens.PasswordResetTokenProvider, "ResetPassword", Uri.UnescapeDataString(request.Code));
        if (!validateCode)
        {
         return RedirectToAction(nameof(Login), new { message = ManageMessageId.PasswordResetFailedError, messageAttachment = "Invalid code." });
        }

        await _userManager.EnsureEmailConfirmedAsync(user);
        await _userManager.EnsureLegacyNotSetAsync(user);

        return View(new InvitedViewModel { Error = string.Empty, Email = user.Email, Code = request.Code, UserId = user.Id });
    }

当用户接受电子邮件时,我们会添加他们。

[HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Invited([FromForm] InvitedViewModel model)
    {
        if (!ModelState.IsValid)
        {
            model.Error = "invalid model";
            return View(model);
        }

        if (!model.Password.Equals(model.ConfirmPassword))
        {

            model.Error = "Passwords must match";
            return View(model);
        }
        if (model.Terms != null && !model.Terms.All(t => t.Accept))
        {
            return View(model);
        }
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user == null)
        {             
            // Don't reveal that the user does not exist
            return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = "User Not invited please invite user again." });
        }

        var result = await _userManager.ResetPasswordAsync(user, Uri.UnescapeDataString(model.Code), model.Password);

        if (result.Succeeded)
        {            
            return Redirect(_settings.Settings.XenaPath);
        }

        var errors = AddErrors(result);
                    return RedirectToAction(nameof(Login), new { message = ManageMessageId.InvitedFailedError, messageAttachment = errors });
    }

这样做的原因是只有身份服务器应该读取和写入其数据库。 api 和第三方应用程序永远不需要直接更改由另一个应用程序控制的数据库。所以以这种方式,API 告诉身份服务器邀请用户,然后身份服务器自己控制其他一切。

此外,通过这种方式,您无需在 API 中设置用户管理器 :)

我不建议您在不同 API 之间使用共享数据库。 如果您需要使用额外的 API 扩展 Identity Server 4,您可以使用 LocalApiAuthentication 作为您的控制器。