如何使用IdentityServer4实施.NET Core身份注册

时间:2018-12-23 11:42:28

标签: razor asp.net-core .net-core identityserver4 httpcontext

  

问题是,IdentityServer已经提供了用于使用Identity Core实现IS4的代码库。但是,该代码库确实   不包含任何用户注册码。底层.NET Core   身份用户注册码存在。您需要连接的所有内容   这样做是为了创建一个像.NET Core一样调用它的MVC API   Identity的样本项目。但是,我无法使其与   在 HttpContext 级别上存在IdentityServer4。

似乎实际上是开发人员未创建注册功能的原因...

我们得到的最接近的答案是在这里: User Registration Process with IdentityServer4

但是,这是没有任何代码示例的理论答案。这是我为了找到答案而可以提供的代码。

这是我的身份认证初创公司

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Nozomi.Base.Identity;
using Nozomi.Base.Identity.Debugging;
using Nozomi.Base.Identity.Models;
using Nozomi.Base.Identity.Models.Identity;
using Nozomi.Repo.Identity.Data;
using Nozomi.Service.Identity;
using Nozomi.Service.Identity.Factories;
using Nozomi.Service.Identity.Managers;
using Nozomi.Service.Identity.Stores;
using Nozomi.Service.Identity.Stores.Interfaces;

namespace Nozomi.Ticker.StartupExtensions
{
    public static class IdentityServerStartup
    {
        public static void ConfigureNozomiAuth(this IServiceCollection services, IConfiguration configuration)
        {
            var env = // Code omitted for brevity

            services.AddIdentity<User, Role>()
                .AddEntityFrameworkStores<NozomiAuthContext>()
                .AddUserManager<NozomiUserManager>()
                .AddSignInManager<NozomiSignInManager>()
                .AddUserStore<NozomiUserStore>()
                .AddRoleStore<NozomiRoleStore>()
                .AddClaimsPrincipalFactory<NozomiUserClaimsPrincipalFactory>()
                .AddDefaultTokenProviders();

            // Configure Authentication
            // https://github.com/aspnet/Security/issues/1414 Redundant to add in this
            //
            // Findings have shown this affects IdentityServerAuthenticationService. Removing this will 
            // default all constants to .NET Core Identity's defaults.
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = NozomiAuthConstants.ApplicationScheme;
                options.DefaultAuthenticateScheme = NozomiAuthConstants.ApplicationScheme;
                options.DefaultChallengeScheme = NozomiAuthConstants.ApplicationScheme;
                options.DefaultForbidScheme = NozomiAuthConstants.ApplicationScheme;
                options.DefaultSignInScheme = NozomiAuthConstants.ApplicationScheme;
                options.DefaultSignOutScheme = NozomiAuthConstants.ApplicationScheme;
            }).AddCookie(options =>
            {
                options.Cookie.Name = NozomiAuthConstants.ApplicationScheme;

                // Cookie settings
                options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

                // TODO: finish this
                options.LoginPath = "/account/login";
                options.LogoutPath = "/account/logout";
                options.AccessDeniedPath = "/account/accessdenied";
                options.SlidingExpiration = true;
            });

            // Configure Authorization
            services.AddAuthorization();

            services.Configure<IdentityOptions>(options =>
            {
                // Password settings.
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequiredLength = 6;
                options.Password.RequiredUniqueChars = 1;

                // User settings.
                options.User.AllowedUserNameCharacters =
                    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
                options.User.RequireUniqueEmail = true;
            });

            services.AddIdentityServer()
                .AddRequiredPlatformServices()
                //.AddSigningCredential(cert)
                .AddResourceStore<ResourceStore>()
                .AddClientStore<ClientStore>()
                .AddAspNetIdentity<User>()
                .AddProfileService<NozomiProfileService>();

            services.AddTransient<IClientStore, ClientStore>();
            services.AddTransient<IResourceStore, ResourceStore>();
        }

        public static void UseNozomiAuth(this IApplicationBuilder app)
        {
            app.UseIdentityServer();
        }
    }
}

因此,基本上,此注入计划告诉您我正在尝试结合.NET Core身份实现IS4。然后,我进一步注入AddAuthentication()方法,以确保正确放置了我的自定义方案名称。

请注意,您在下面看到的内容相当于一天的逐步调试。

仅供参考,由IS4完成的注入操作不会声明您自己的常量(方案),因此您必须确保“ AddAuthentication”

在您调用“ AddIdentityServer”时,这只是IS4的一小部分

        /// <summary>
        /// Adds the default cookie handlers and corresponding configuration
        /// </summary>
        /// <param name="builder">The builder.</param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddCookieAuthentication(this IIdentityServerBuilder builder)
        {
            builder.Services.AddAuthentication(IdentityServerConstants.DefaultCookieAuthenticationScheme)
                .AddCookie(IdentityServerConstants.DefaultCookieAuthenticationScheme)
                .AddCookie(IdentityServerConstants.ExternalCookieAuthenticationScheme);

            builder.Services.AddSingleton<IConfigureOptions<CookieAuthenticationOptions>, ConfigureInternalCookieOptions>();
            builder.Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureInternalCookieOptions>();
            builder.Services.AddTransientDecorator<IAuthenticationService, IdentityServerAuthenticationService>();
            builder.Services.AddTransientDecorator<IAuthenticationHandlerProvider, FederatedSignoutAuthenticationHandlerProvider>();

            return builder;
        }

如果您真的想知道,它实际上就是这样做的。

        /// <summary>
        /// Adds IdentityServer.
        /// </summary>
        /// <param name="services">The services.</param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddIdentityServer(this IServiceCollection services)
        {
            var builder = services.AddIdentityServerBuilder();

            builder
                .AddRequiredPlatformServices()
                .AddCookieAuthentication()
                .AddCoreServices()
                .AddDefaultEndpoints()
                .AddPluggableServices()
                .AddValidators()
                .AddResponseGenerators()
                .AddDefaultSecretParsers()
                .AddDefaultSecretValidators();

            // provide default in-memory implementation, not suitable for most production scenarios
            builder.AddInMemoryPersistedGrants();

            return builder;
        }

最初的实现无效。我必须创建自己的用户存储。但是只有一种方法需要更新,而这就是更新。

        /// <summary>
        /// Gets the user identifier for the specified <paramref name="user" />.
        /// </summary>
        /// <param name="user">The user whose identifier should be retrieved.</param>
        /// <param name="cancellationToken">The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation, containing the identifier for the specified <paramref name="user" />.</returns>
        public override Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken = default (CancellationToken))
        {
            try
            {
                if (cancellationToken != null)
                    cancellationToken.ThrowIfCancellationRequested();

                if (user == null)
                    throw new ArgumentException(nameof(user));

                var res = _unitOfWork.GetRepository<User>().Get(u =>
                        u.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)
                        || user.UserName.Equals(user.UserName, StringComparison.InvariantCultureIgnoreCase))
                    .Select(u => u.Id)
                    .SingleOrDefault();

                if (res == null)
                    throw new ArgumentOutOfRangeException(nameof(user));

                return Task.FromResult(res.ToString());
            }
            catch (Exception ex)
            {
                return Task.FromResult("-1");
            }
        }

您还必须覆盖现有的SignInManager以支持您的自定义方案。

        /// <summary>
        /// Returns true if the principal has an identity with the application cookie identity
        /// </summary>
        /// <param name="principal">The <see cref="ClaimsPrincipal"/> instance.</param>
        /// <returns>True if the user is logged in with identity.</returns>
        public override bool IsSignedIn(ClaimsPrincipal principal)
        {
            if (principal == null)
            {
                throw new ArgumentNullException(nameof(principal));
            }
            return principal?.Identities != null &&
                   principal.Identities.Any(i => i.AuthenticationType == NozomiAuthConstants.ApplicationScheme);
        }

        /// <summary>
        /// Regenerates the user's application cookie, whilst preserving the existing
        /// AuthenticationProperties like rememberMe, as an asynchronous operation.
        /// </summary>
        /// <param name="user">The user whose sign-in cookie should be refreshed.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public override async Task RefreshSignInAsync(User user)
        {
            var auth = await Context.AuthenticateAsync(NozomiAuthConstants.ApplicationScheme);
            var authenticationMethod = auth?.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod);
            await SignInAsync(user, auth?.Properties, authenticationMethod);
        }

        /// <summary>
        /// Signs in the specified <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user to sign-in.</param>
        /// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
        /// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public override Task SignInAsync(User user, bool isPersistent, string authenticationMethod = null)
        {
            return SignInAsync(user, new AuthenticationProperties { IsPersistent = isPersistent }, authenticationMethod);
        }

        /// <summary>
        /// Signs in the specified <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user to sign-in.</param>
        /// <param name="authenticationProperties">Properties applied to the login and authentication cookie.</param>
        /// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public override async Task SignInAsync(User user, AuthenticationProperties authenticationProperties, string authenticationMethod = null)
        {
            try
            {
                var userPrincipal = await CreateUserPrincipalAsync(user);
                // Review: should we guard against CreateUserPrincipal returning null?
                if (authenticationMethod != null)
                {
                    userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod));
                }

                await Context.SignInAsync(NozomiAuthConstants.ApplicationScheme,
                    userPrincipal,
                    authenticationProperties ?? new AuthenticationProperties());
                // https://github.com/aspnet/Security/issues/1131#issuecomment-280896191
                // Context.User = userPrincipal;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        /// <summary>
        /// Signs the current user out of the application.
        /// </summary>
        public override async Task SignOutAsync()
        {
            await Context.SignOutAsync(NozomiAuthConstants.ApplicationScheme);
            await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
            await Context.SignOutAsync(IdentityConstants.ExternalScheme);
            await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
        }

然后,我更新了索赔工厂以正确支持我制定的方案。

namespace Nozomi.Service.Identity.Factories
{
    public class NozomiUserClaimsPrincipalFactory: UserClaimsPrincipalFactory<User, Role>
    {
        public new NozomiUserManager UserManager;
        public new RoleManager<Role> RoleManager;

        public NozomiUserClaimsPrincipalFactory(NozomiUserManager userManager, RoleManager<Role> roleManager, 
            IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
        {
            UserManager = userManager;
            RoleManager = roleManager;
        }

        /// <summary>
        /// Creates a <see cref="T:System.Security.Claims.ClaimsPrincipal" /> from an user asynchronously.
        /// </summary>
        /// <param name="user">The user to create a <see cref="T:System.Security.Claims.ClaimsPrincipal" /> from.</param>
        /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous creation
        /// operation, containing the created <see cref="T:System.Security.Claims.ClaimsPrincipal" />.</returns>
        public override async Task<ClaimsPrincipal> CreateAsync(User user)
        {
            if ((object) user == null)
                throw new ArgumentNullException(nameof (user));
            return new ClaimsPrincipal(await GenerateClaimsAsync(user));
        }

        /// <summary>Generate the claims for a user.</summary>
        /// <param name="user">The user to create a <see cref="T:System.Security.Claims.ClaimsIdentity" /> from.</param>
        /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous creation operation, containing the created <see cref="T:System.Security.Claims.ClaimsIdentity" />.</returns>
        protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
        {
            var userId = await UserManager.GetUserIdAsync(user);
            var userNameAsync = await UserManager.GetUserNameAsync(user);

            var id = new ClaimsIdentity(NozomiAuthConstants.ApplicationScheme, 
                this.Options.ClaimsIdentity.UserNameClaimType, this.Options.ClaimsIdentity.RoleClaimType);
            id.AddClaim(new Claim(this.Options.ClaimsIdentity.UserIdClaimType, userId));
            id.AddClaim(new Claim(this.Options.ClaimsIdentity.UserNameClaimType, userNameAsync));

            ClaimsIdentity claimsIdentity;
            if (this.UserManager.SupportsUserSecurityStamp)
            {
                claimsIdentity = id;
                string type = this.Options.ClaimsIdentity.SecurityStampClaimType;
                claimsIdentity.AddClaim(new Claim(type, await this.UserManager.GetSecurityStampAsync(user)));
                claimsIdentity = (ClaimsIdentity) null;
                type = (string) null;
            }
            if (this.UserManager.SupportsUserClaim)
            {
                claimsIdentity = id;
                claimsIdentity.AddClaims((IEnumerable<Claim>) await this.UserManager.GetClaimsAsync(user));
                claimsIdentity = (ClaimsIdentity) null;
            }
            return id;
        }
    }
}

我们应该能够通过创建一个注册API来立即登录,该API在成功注册后自动登录。

        //
        // POST: /Account/Register
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var user = new User
                {
                    UserName = model.Username, 
                    Email = model.Email
                };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    #if DEBUG
                    await _userManager.ForceConfirmEmail(user);
                    await _signInManager.SignInAsync(user, false, NozomiAuthConstants.ApplicationScheme);
                    _logger.LogInformation(3, "User created a new account with password.");
                    #else
                    // For more information on how to enable account confirmation and password reset
                    // please visit http://go.microsoft.com/fwlink/?LinkID=532713
                    // Send an email with this link
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code },
                        protocol: HttpContext.Request.Scheme);
                    await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
                        "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");
                    #endif

                    return RedirectToLocal(returnUrl);
                }
                AddErrors(result);
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

ForceConfirmEmail是一种自定义方法,用于默认情况下手动将EmailConfirmed布尔值设置为true。

注册后,所有内容都可以顺利通过数据库(您的用户帐户已保存在数据库中)。但是,尽管cookie中存在注册的剩余内容,但您无法从剃刀视图中得到任何东西。

enter image description here

如果您已经注意到,从一开始我就一直在使用NozomiAuthConstants.ApplicationScheme。就像您知道的那样,它实际上等于'idsrv'。

我在下面附加了一张图片,向您显示登录后会得到什么。您一无所获,甚至没有索偿。声明的长度= 0,任何IsAuthenticated =假。

enter image description here

再次运行整个mvc应用程序后,前端仍将其显示为未经身份验证。我回到绘图板上,找出什么为空,然后我意识到获取HttpContext的代码中断了,并为此修复了DI,最终在我的自定义SignInManager中实现了这一点,以使将来的调试过程更轻松。 ,(我还添加了私有变量/对象和构造函数以供进一步参考):

        private readonly IHttpContextAccessor _contextAccessor;
        private HttpContext _context;
        private new readonly NozomiUserManager UserManager;
        private new readonly NozomiUserClaimsPrincipalFactory ClaimsFactory;

        public NozomiSignInManager(NozomiUserManager userManager, IHttpContextAccessor contextAccessor, 
            NozomiUserClaimsPrincipalFactory claimsFactory, IOptions<IdentityOptions> optionsAccessor, 
            ILogger<NozomiSignInManager> logger, IAuthenticationSchemeProvider schemes) : 
            base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
        {
            _contextAccessor = contextAccessor;
            UserManager = userManager;
            ClaimsFactory = claimsFactory;
        }

        /// <summary>
        /// The <see cref="T:Microsoft.AspNetCore.Http.HttpContext" /> used.
        /// </summary>
        public new HttpContext Context
        {
            get
            {
                var httpContext = _context ?? _contextAccessor?.HttpContext;
                if (httpContext != null)
                    return httpContext;
                throw new InvalidOperationException("HttpContext must not be null.");
            }
            set => _context = value;
        }

这是我正确注入HttpContextAccessor的方式:

services.AddIdentityServer()
                .AddRequiredPlatformServices()
                //.AddSigningCredential(cert)
                .AddResourceStore<ResourceStore>()
                .AddClientStore<ClientStore>()
                .AddAspNetIdentity<User>()
                .AddProfileService<NozomiProfileService>();

此行

.AddRequiredPlatformServices()

确实

builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();            
            builder.Services.AddOptions();
            builder.Services.AddSingleton(
                resolver => resolver.GetRequiredService<IOptions<IdentityServerOptions>>().Value);

            return builder;

这是一个值得每周工作的工作(我在工作日没有太多时间,因此请考虑2-3天的工作。如果您有任何疑问,请告诉我,因为我或多或少地调试了步骤Identity Core代码的每个部分。)

现在,我所缺少的是找出为什么ClaimsPrincipal没有正确地注入到HttpContext中。

Source code is available here

1 个答案:

答案 0 :(得分:0)

您需要使用

  • AspIdentity +

  • 身份服务器+

  • 身份快速入门.UI +

  • 数据库

这是一个工作示例

步骤01

我们将打开一个运行窗口

在键盘上按

Windoes Key + R

等待

步骤02

我们将打开一个cmd窗口

在“运行”窗口中输入文字输入

cmd

在键盘上按

Enter

步骤03

确保App1 / appsettings.json具有此连接字符串,默认情况下将存在该连接字符串(如果未添加的话)

DataSource=app.db

步骤04

我们将创建一个目录并将其作为cmd的工作目录

在CMD窗口上写

mkdir D:\Projects\P.IT.Support\asp.net

cd /d D:\Projects\P.IT.Support\asp.net

步骤05

我们将下载并运行脚本

在CMD窗口上写

set scriptUrl="https://gist.githubusercontent.com/Elrashid/133d308902b453c280725379dda02684/raw/Flow4-AspIdentity-Sqlite-RequireConsent-Two-App-For-API-And-Web.bat"

PowerShell -Command "(new-object System.Net.WebClient).DownloadFile('%scriptUrl%','Flow4-AspIdentity-Sqlite-RequireConsent-Two-App-For-API-And-Web.bat')"

Flow4-AspIdentity-Sqlite-RequireConsent-Two-App-For-API-And-Web.bat