是否可以将 ASP.NET 成员身份与在 ASP.NET Identity 2.0 中创建的表一起用于身份验证?

Is it possible to use ASP.NET membership with tables created in ASP.NET Identity 2.0 for authentication?

我有一个基于 MVC4 的 Web 门户,它使用成员身份仅对用户进行身份验证(不创建新用户,不验证密码)。

这是用于验证用户身份的表格的屏幕截图:

以下是用户尝试验证时触发的操作方法:

    [HttpPost]
    public ActionResult Login(string username, string password, string[] rememberMe, string returnUrl)
    {
        if(Membership.ValidateUser(username, password))
        {
            if (rememberMe != null)
            {
                FormsAuthentication.SetAuthCookie(username, true);
            }
            Response.Redirect(returnUrl);
        }
        ViewBag.ErrorMessage = "wrong credentials";
        return View();
    }
    

这是网络配置:

<membership defaultProvider="AspNetSqlMembershipProvider" userIsOnlineTimeWindow="15">
  <providers>
    <clear />
    <add name="AspNetSqlMembershipProvider" 
         connectionStringName="SiteSqlServer" 
         enablePasswordRetrieval="false" 
         enablePasswordReset="true" 
         requiresQuestionAndAnswer="false"
         applicationName="/" 
         requiresUniqueEmail="true" 
         passwordFormat="Hashed" 
         maxInvalidPasswordAttempts="8" 
         minRequiredPasswordLength="4" 
         minRequiredNonalphanumericCharacters="0" 
         passwordAttemptWindow="10" 
         passwordStrengthRegularExpression="" 
         type="System.Web.Security.SqlMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d43e4e" />
  </providers>
</membership>

我还有一个由 asp.net identity 2.0 创建的新数据库:

我的任务是针对新表进行用户身份验证 (即我需要成员身份用于身份验证,由 asp.net identity 2.0 创建的表)。

因此,为此目的,我想我需要自定义会员才能阅读新表格。

我的问题是,是否可以对成员身份进行更改,使其读取 asp.net identity 2.0 以进行身份​​验证?

是的,这是可能的,但这是解决方案将取决于您当前的具体实施的问题之一。立即想到三种解决方案:

  1. 用对新系统的调用替换所有身份验证逻辑
    从长远来看,这更易于管理,因为您只需要在一个地方维护身份验证逻辑。

    • 如果您部署了可以通过 HTTP 访问的 API,这是最简单的解决方案,但它可以通过包含您的 dll 来工作,只要它们是针对兼容的 .Net 版本编译的。
    • 如果您有其他应用程序 (Desktop/Web/Mobile) 也使用新的 API,这个旧站点将成为新站点的客户端。

    This is the lowest code solution, and the only one where we do not have to be concerned with correctly hashing passwords for comparison.

    如果其他系统是在线的API,那么你可以使用HttpClient简单地调用其他站点进行身份验证,你如何做到这一点将取决于所支持的身份验证协议类型。 API

    using System.Web.Security;
    
    [HttpPost]
    public async Task<ActionResult> Login(string username, string password, string[] rememberMe, string returnUrl)
    {
        if(await ValidateUser(username, password))
        {
            if (rememberMe != null)
            {
                FormsAuthentication.SetAuthCookie(username, true);
            }
            Response.Redirect(returnUrl);
        }
        ViewBag.ErrorMessage = "wrong credentials";
        return View();
    }
    
    public async Task<bool> Login(string username, string password)
    {
        var client = new System.Net.Http.HttpClient();
        client.DefaultRequestHeaders.Clear();
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded");
        Dictionary<string, string> data = new Dictionary<string, string>();
        data.Add("grant_type", "password");
        data.Add("username", username);
        data.Add("password", password);
    
        // Example, replace this with your own.
        string apiAuthUrl = "http://localhost:3000/token";
    
        var response = await client.PostAsync(apiAuthUrl, new System.Net.Http.FormUrlEncodedContent(data));
        return response.IsSuccessStatusCode;
    }
    

    如果其他系统未发布为 API,即桌面风格的应用程序,那么您可以通过直接引用和调用 dll 中的 Auth 方法来做类似的事情。

  2. 创建自定义会员提供程序
    这是一个基于代码的解决方案,类似于它在 2.0 中的工作方式,但涉及更多。

    • Scott Gu 已发布源代码,曾经是此类活动的有用参考:Source Code for the Built-in ASP.NET 2.0 Providers Now Available for Download

    • 但是链接好像失效了!

    • MS Docs 仍有一些阅读,但没有来源:Implementing a Membership Provider

    • 以下文章来自第一原则方法,您的表已经创建,因此只需专注于访问数据,而不是创建新表

      (MVC) Custom Membership Providers

      It's not neccessary to implement all aspects of the providers, only those methods that your site code will call.

      This solution is good because you don't have to change any existing code, just new code and a change to the web.config

      兴趣点

      更改 web.config 引用以使用您的自定义提供程序,将 MyNamespace.Providers 替换为您的实际名称空间!:

      <membership defaultProvider="CustomMembershipProvider" userIsOnlineTimeWindow="15">
        <providers>
          <clear/>
          <add name="CustomMembershipProvider" 
              type="MyNamespace.Providers.CustomMembershipProvider"
              connectionStringName="SiteSqlServer"
              enablePasswordRetrieval="false"
              enablePasswordReset="true"
              requiresQuestionAndAnswer="false"
              requiresUniqueEmail="true"
              maxInvalidPasswordAttempts="8"
              minRequiredPasswordLength="4"
              minRequiredNonalphanumericCharacters="0"
              passwordAttemptWindow="10"
              applicationName="DotNetNuke" />
        </providers>
      </membership>
      

      然后实施会员提供程序class:

      using System.Web.Security;
      
      public class CustomMembershipProvider : MembershipProvider
      {
          public override MembershipUser CreateUser(string username, string password, 
                 string email, string passwordQuestion, string passwordAnswer, 
                 bool isApproved, object providerUserKey, out MembershipCreateStatus status)
          {
              /// we're not creating new users in this site!
              throw new NotImplementedException();             
          }
      
          public override MembershipUser GetUser(string username, bool userIsOnline)
          {
              var user = /* Your User Repo Lookup */;
      
              if (user != null)
              {
                  MembershipUser memUser = new MembershipUser("CustomMembershipProvider", 
                                                 username, user.UserID, user.UserEmailAddress,
                                                 string.Empty, string.Empty,
                                                 true, false, DateTime.MinValue,
                                                 DateTime.MinValue,
                                                 DateTime.MinValue,
                                                 DateTime.Now, DateTime.Now);
                  return memUser;
              }
              return null;
          }
      
          public override bool ValidateUser(string username, string password)
          {
              // This is highly dependent on how passwords are encrypted in the new system
              // Assuming it is a simple MD5 Hash, we just need to create the same Hash              
              string sha1Pswd = GetMD5Hash(password);
      
              // TODO: Now use this hash to verify that the username and password match a user in your repo
              var user = /*Lookup a user that matches the username and password.*/;
              if (user != null)
                  return true;
              return false;
          }
      
          public override int MinRequiredPasswordLength
          {
              get { return 4; }
          }
      
          /// <summary> Simple MD5 Hash algorithm, you will need to match this against the process used in the other system </summary>
          public static string GetMD5Hash(string value)
          {
              MD5 md5Hasher = MD5.Create();
              byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value));
              StringBuilder sBuilder = new StringBuilder();
              for (int i = 0; i < data.Length; i++)
              {
                  sBuilder.Append(data[i].ToString("x2"));
              }
      
              return sBuilder.ToString();
          }
      }
      
      
      
  3. 自定义 AspNetSqlMembershipProvider 使用的存储过程
    取决于你的实力,这可能更简单并且是一个选项,因为 MVC 站点已经配置了这个提供者,我们需要做的就是修改提供者用来执行身份验证请求的 SQL 存储过程。

    • 下面是关于如何实现提供者的好读物

      Using AspNetSqlMembershipProvider in MVC5
      Configuring web application to utilise ASP.NET Application Services database

      • 如前一个选项中所强调的,这仅在新站点使用相同的加密方法时才有效,它可能还需要使用 web.config 中设置的相同机器密钥。

      您可以设置一个空白数据库并为其配置 aspnet_regsql 以查看存储过程(它们不会在您的新数据库中!)。

      • 这将成为一个位或反复试验来确定将需要的程序,我怀疑你只需要GetUserByName,如下截图显示了一些其他存储过程,命名约定使得它很容易匹配成员资格 API 方法:

        /****** Object:  StoredProcedure [dbo].[aspnet_Membership_GetUserByName]    Script Date: 25/06/2020 11:19:04 AM ******/
        SET ANSI_NULLS ON
        GO
        SET QUOTED_IDENTIFIER OFF
        GO
        CREATE OR ALTER PROCEDURE [dbo].[aspnet_Membership_GetUserByName]
            @ApplicationName      nvarchar(256),
            @UserName             nvarchar(256),
            @CurrentTimeUtc       datetime,
            @UpdateLastActivity   bit = 0
        AS
        BEGIN
            DECLARE @UserId uniqueidentifier
      
            IF (@UpdateLastActivity = 1)
            BEGIN
                -- select user ID from AspnetUsers table
                -- Ignore ApplicationIDs here, assume this is a single Application schema
                SELECT TOP 1 @UserId = u.Id
                FROM    dbo.AspNetUsers u
                WHERE   LOWER(@UserName) = LOWER(u.UserName)
      
                IF (@@ROWCOUNT = 0) -- Username not found
                    RETURN -1
      
                -- We don't have 'Activity' per-se, instead we reset the AccessFailedCount to zero
                -- Your implementation might be different, so think it through :)
                UPDATE   dbo.AspNetUsers
                SET      AccessFailedCount = 0
                WHERE    Id = @UserId
      
                -- Your Schema might be different, here we just map back to the old Schema in the projected response
                -- Make sure the data types match in your response, here we are injected GetDate() for most dates by default
                -- NOTE: LockOutEnabled DOES NOT mean the user is locked out, only that lockout logic should be evaluated
                SELECT Email, '' as PasswordQuestion, '' as Comment, Cast(1 as BIT) as IsApproved,
                        CAST(null as datetime) as CreateDate, GetDate() as LastLoginDate, GetDate() as LastActivityDate, GetDate() as LastPasswordChangedDate,
                        Id as UserId, CASE WHEN LockoutEnabled = 1 AND LockoutEndDateUtc IS NOT NULL THEN 1 ELSE 0 END as IsLockedOut, LockoutEndDateUtc as LastLockoutDate
                FROM    dbo.AspNetUsers
                WHERE  Id = @UserId
            END
            ELSE
            BEGIN
                -- Your Schema might be different, here we just map back to the old Schema in the projected response
                -- Make sure the data types match in your response, here we are injected GetDate() for most dates by default
                -- NOTE: LockOutEnabled DOES NOT mean the user is locked out, only that lockout logic should be evaluated
                SELECT TOP 1 Email, '' as PasswordQuestion, '' as Comment, Cast(1 as BIT) as IsApproved,
                        CAST(null as datetime) as CreateDate, GetDate() as LastLoginDate, GetDate() as LastActivityDate, GetDate() as LastPasswordChangedDate,
                        Id as UserId, CASE WHEN LockoutEnabled = 1 AND LockoutEndDateUtc IS NOT NULL THEN 1 ELSE 0 END as IsLockedOut, LockoutEndDateUtc as LastLockoutDate
                FROM    dbo.AspNetUsers
                WHERE   LOWER(@UserName) = LOWER(UserName)
      
                IF (@@ROWCOUNT = 0) -- Username not found
                    RETURN -1
            END
      
            RETURN 0
        END
      

      您可能还需要实现部分或全部视图,但这在很大程度上取决于您的 MVC 站点实际使用的成员资格 API 中的方法。