ASP.NET成员资格更改密码无法工作

12

我有以下代码,用于在用户单击密码重置按钮时更改用户的密码(包含一些额外的代码来记录到 ELMAH 中,以便我可以尝试找出哪里出了问题)。

这是在 ASP.NET MVC 2 中使用标准 aspnet 成员资格提供程序,使用以下简单的视图:

New Password:     ______
Confirm Password: ______
[Reset] [Cancel]

这个视图的路由是/Account/Reset/guid,其中guid是aspnet会员数据库中用户的id。

代码的关键部分在于调用 user.ChangePassword()。你可以看到,当成功时,它会记录一条消息。问题在于对于某些用户,成功消息被记录了,但他们无法使用新密码登录。而对于其他用户,记录了成功消息并且他们可以登录。

if (user.ChangePassword(pwd, confirmPassword))
{
    ErrorSignal.FromCurrentContext().Raise(
        new Exception("ResetPassword - changed successfully!"));
    return Json(new { 
        Msg = "You have reset your password successfully." }, 
        JsonRequestBehavior.AllowGet);
 }

完整的代码清单如下:

[HttpPost]
public JsonResult ResetPassword(string id, string newPassword, string confirmPassword)
{
    ErrorSignal.FromCurrentContext().Raise(new Exception("ResetPassword started for " + id));

    ViewData["PasswordLength"] = Membership.MinRequiredPasswordLength;

    if (string.IsNullOrWhiteSpace(newPassword))
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - new password was blank."));
        ModelState.AddModelError("_FORM", "Please enter a new password.");
        return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
    }

    if (newPassword.Length < Membership.MinRequiredPasswordLength)
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - new password was less than minimum length."));
        ModelState.AddModelError("_FORM", 
            string.Format("The password must be at least {0} characters long.", 
            Membership.MinRequiredPasswordLength));
        return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
    }

    if (string.IsNullOrWhiteSpace(confirmPassword))
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - confirm password was blank."));
        ModelState.AddModelError("_FORM", 
            "Please enter the same new password in the confirm password textbox.");
        return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
    }

    if (confirmPassword.Length < Membership.MinRequiredPasswordLength)
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - confirm password was less than minimum length."));
        ModelState.AddModelError("_FORM", 
            string.Format("The password must be at least {0} characters long.", 
            Membership.MinRequiredPasswordLength));
        return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
    }

    if (confirmPassword != newPassword)
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - new password did not match the confirm password."));
        ModelState.AddModelError("_FORM", "Please enter the same password again.");
        return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
    }

    bool isMatch = ValidationHelper.IsGUID(id);
    if (string.IsNullOrWhiteSpace(id) || !isMatch)
    {
        ErrorSignal.FromCurrentContext().Raise(
            new Exception("ResetPassword - id was not a guid."));
        ModelState.AddModelError("_FORM", "An invalid ID value was passed in through the URL");
    }
    else
    {
        //ID exists and is kosher, see if this user is already approved
        //Get the ID sent in the querystring
        Guid userId = new Guid(id);

        try
        {
            //Get information about the user
            MembershipUser user = Membership.GetUser(userId);
            if (user == null)
            {
                //could not find the user
                ErrorSignal.FromCurrentContext().Raise(
                    new Exception("ResetPassword - could not find user by id " + id));
                ModelState.AddModelError("_FORM", 
                    "The user account can not be found in the system.");
            }
            else
            {
                ErrorSignal.FromCurrentContext().Raise(
                    new Exception("ResetPassword - user is " + user.UserName));
                string pwd = user.ResetPassword();

                if (user.ChangePassword(pwd, confirmPassword))
                {
                    ErrorSignal.FromCurrentContext().Raise(
                        new Exception("ResetPassword - changed successfully!"));
                    return Json(new { 
                        Msg = "You have reset your password successfully." }, 
                        JsonRequestBehavior.AllowGet);
                }
                ErrorSignal.FromCurrentContext().Raise(
                    new Exception("ResetPassword 
                    - failed to change the password, for an unknown reason"));
            }
        }
        catch (Exception ex)
        {
            ErrorSignal.FromCurrentContext().Raise(
                new Exception("ResetPassword: " + ex));
            return Json(new { Error = ex.Message + " -> " 
                + ex.InnerException.Message }, JsonRequestBehavior.AllowGet);
        }
    }

    return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
}

编辑:添加悬赏以尝试解决此问题。这是我的问题清单中最烦人的问题之一,我不知道该如何继续。


你亲眼见过这种情况吗?你百分之百确定用户不是只是输错了密码吗?在这个页面上,密码是否存在前导或尾随空格,可能会被登录页面去掉了? - Greg
另外,用户在尝试登录时收到了什么特定的错误消息?同时还有其他什么东西正在重置他们的密码吗?基本上,就目前而言,MembershipProvider 的工作是正常的,并且您发布的代码看起来不错,因此我的猜测是问题要么不是问题,要么存在于其他代码中。 - Greg
我已经尝试过将密码重置为"password1",然后立即尝试登录。但是我无法登录(用户名或密码不正确)。对于其他用户和我的账户,我可以更改密码。 - JK.
这是标准的 SQL Server ASP.NET 成员资格还是其他的? - Simon Mourier
你不应该向ResetPassword函数发送用户名吗?http://msdn.microsoft.com/en-us/library/system.web.security.membershipprovider.resetpassword.aspx - Tommy
看起来@adrift得到了答案 - user.IsApproved为false。虽然我认为这只能解释我用作示例的新用户。其他用户在密码问题之前已经使用该网站一段时间(因此他们已经被批准)。 - JK.
10个回答

15

如果用户需要重置密码,则有可能由于太多无效尝试而导致其帐户被锁定。 如果是这种情况,则密码已成功重置,但在锁定条件被清除之前,用户将无法登录。

尝试检查MembershipUser.IsLockedOut

当在PasswordAttemptWindow内达到MaxInvalidPasswordAttempts时,用户最常被锁定并且无法通过ValidateUser方法验证。

要将此属性设置为false并让用户再次尝试登录,可以使用UnlockUser方法。

编辑

您也检查了IsApproved吗?如果该用户的值为false,则身份验证会失败。

另外,假设默认成员资格提供程序是SqlMembershipProvider,请针对您的数据库运行以下查询并确保一切正确:

select IsApproved, IsLockedOut, FailedPasswordAttemptCount
from aspnet_Membership
where ApplicationId = @yourApplicationId and UserId = @userId

在尝试登录之前,请执行查询以验证IsApprovedIsLockedOut是否正常。还要注意FailedPasswordAttemptCount的值。

尝试登录,然后再次运行查询。如果登录失败,FailedPasswordAttemptCount的值是否增加了?

您还可以查看aspnet_Membership表中的PasswordFormat,并确保根据您正在使用的格式使用正确的值(0表示清除,1表示哈希,2表示加密)。


是的,抱歉我忘了提到 - 我已经检查了 IsLockedOut 属性,用户没有被锁定。 - JK.
@JK,请查看我上面的编辑。希望这些建议中的一个能帮助你找到有用的线索。我很好奇问题最终会是什么。 - Jeff Ogata
+1 IsApproved 为 false - 看起来问题已经找到了 :) - JK.

2
哦,我一直都用过
bool MembershipUser.ChangePassword(string oldPassword, string newPassword)

我从未遇到过返回true但密码未正确更改的问题。

就我所知,你的代码看起来没问题。其中有很多Elmah噪音,很难跟进(你可能想要删除它或将其替换为简单的日志调用,以便更容易跟进)。

请验证你传递的字符串id是否对应于预期用户的UserId。你可能会发送来自其他用户的userId并更改该用户的密码。


日志记录包括一个显示 user.UserName 的消息 - 每次都显示正确的名称。我将尝试使用 MembershipUser.ChangePassword 而不是 user.ChangePassword() - JK.
1
顺便提一下,由于您的ResetPassword操作仅处理Post请求,因此您不需要JsonRequestBehavior.AllowGet。请使用[HttpPost]属性。 - santiagoIT
@santiago 好的,那我就不改了。我很困惑,不明白为什么用户更改密码返回 true,但使用新密码无法登录。我自己试过,使用一个不能更改密码的用户的 GUID,证实了这一点。 - JK.
1
奇怪的情况。在您仍在跟踪问题时,请确保记录confirmPassword参数。您永远不知道,它可能不是您所期望的。例如,如果他们使用了一些特殊字符(<,>等),它可能会进行URL编码。 - santiagoIT
@santiago 我试着把它改成“password1”,但没有成功。 - JK.
显示剩余5条评论

1

编辑 - 下面的答案是错误的,请查看评论

所以你是想通过 GUID 查找某个人吗?通过执行以下操作:

Guid userId = new Guid(id);

你实际上正在创建一个保证唯一的ID。所以我猜你永远找不到用户,并且成功地为没有人重置了密码。你不能通过传递的id参数直接找到他们吗?


不,调用 new Guid(some string) 只是将该字符串转换为 Guid。它正在正确地定位用户。 - JK.
1
Guid userId = new Guid(id) 将字符串 id 参数转换为 Guid。它不会生成全新的 Guid。由于 Membership.GetUser 需要一个 Guid,因此您必须使用此转换。 - BJ Safdie
对不起,今晚我没有清醒的头脑,在编辑帖子时已经更正了,以免有人误会为真。除此之外,我认为代码看起来没问题,当然它并没有引发任何异常,所以我猜测这是数据库方面的错误。那么你还有其他密码方法的问题吗? - Will Ayd
@JK,看了你对Santiago的评论,你有没有确认过可用的样本密码和不可用的样本密码?或者你是遇到了这样的情况:某个密码在某些提交中有效,但在其他提交中无效? - Will Ayd
是的,我可以将一些用户的密码更改为“password1”,而其他用户则无法更改为相同的“password1”。 - JK.
非常奇怪。默认的成员资格提供程序对所有用户都是相同的吗? - Will Ayd

1
这对我有效:
<%@ Page Title="Change Password" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="ChangePassword.aspx.cs" Inherits="WebPages.Account.ChangePassword" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Change Password
    </h2>
    <p>
        Use the form below to change your password.
    </p>
    <p>
        New passwords are required to be a minimum of <%= Membership.MinRequiredPasswordLength %> characters in length.
    </p>
    <asp:ChangePassword ID="ChangeUserPassword" runat="server" CancelDestinationPageUrl="~/" EnableViewState="false" RenderOuterTable="false" 
        OnChangedPassword="ChangeUserPassword_ChangedPassword">
        <ChangePasswordTemplate>
            <span class="failureNotification">
                <asp:Literal ID="FailureText" runat="server"></asp:Literal>
            </span>
            <asp:ValidationSummary ID="ChangeUserPasswordValidationSummary" runat="server" CssClass="failureNotification" 
                 ValidationGroup="ChangeUserPasswordValidationGroup"/>
            <div class="accountInfo">
                <fieldset class="changePassword">
                    <legend>Account Information</legend>
                    <p>
                        <asp:Label ID="CurrentPasswordLabel" runat="server" AssociatedControlID="CurrentPassword">Old Password:</asp:Label>
                        <asp:TextBox ID="CurrentPassword" runat="server" CssClass="passwordEntry" TextMode="Password"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="CurrentPasswordRequired" runat="server" ControlToValidate="CurrentPassword" 
                             CssClass="failureNotification" ErrorMessage="Password is required." ToolTip="Old Password is required." 
                             ValidationGroup="ChangeUserPasswordValidationGroup">*</asp:RequiredFieldValidator>
                    </p>
                    <p>
                        <asp:Label ID="NewPasswordLabel" runat="server" AssociatedControlID="NewPassword">New Password:</asp:Label>
                        <asp:TextBox ID="NewPassword" runat="server" CssClass="passwordEntry" TextMode="Password"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="NewPasswordRequired" runat="server" ControlToValidate="NewPassword" 
                             CssClass="failureNotification" ErrorMessage="New Password is required." ToolTip="New Password is required." 
                             ValidationGroup="ChangeUserPasswordValidationGroup">*</asp:RequiredFieldValidator>
                    </p>
                    <p>
                        <asp:Label ID="ConfirmNewPasswordLabel" runat="server" AssociatedControlID="ConfirmNewPassword">Confirm New Password:</asp:Label>
                        <asp:TextBox ID="ConfirmNewPassword" runat="server" CssClass="passwordEntry" TextMode="Password"></asp:TextBox>
                        <asp:RequiredFieldValidator ID="ConfirmNewPasswordRequired" runat="server" ControlToValidate="ConfirmNewPassword" 
                             CssClass="failureNotification" Display="Dynamic" ErrorMessage="Confirm New Password is required."
                             ToolTip="Confirm New Password is required." ValidationGroup="ChangeUserPasswordValidationGroup">*</asp:RequiredFieldValidator>
                        <asp:CompareValidator ID="NewPasswordCompare" runat="server" ControlToCompare="NewPassword" ControlToValidate="ConfirmNewPassword" 
                             CssClass="failureNotification" Display="Dynamic" ErrorMessage="The Confirm New Password must match the New Password entry."
                             ValidationGroup="ChangeUserPasswordValidationGroup">*</asp:CompareValidator>
                    </p>
                </fieldset>
                <p class="submitButton">
                    <asp:Button ID="CancelPushButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel"/>
                    <asp:Button ID="ChangePasswordPushButton" runat="server" CommandName="ChangePassword" Text="Change Password" 
                         ValidationGroup="ChangeUserPasswordValidationGroup"/>
                </p>
            </div>
        </ChangePasswordTemplate>
        <SuccessTemplate>
            <div class="accountInfo">
                <fieldset class="changePassword">
                    <legend>Password changed</legend>
                        <p>
                            Your password has been changed. A confirmation e-mail has been sent to you.
                        </p>
                </fieldset>
            </div>
        </SuccessTemplate>
    </asp:ChangePassword>
</asp:Content>

1
如果您正在使用内置的基于SQLServer的提供程序,请查看您的SQL存储过程。这是我的默认存储过程的样子:
ALTER PROCEDURE dbo.aspnet_Membership_SetPassword
    @ApplicationName  nvarchar(256),
    @UserName         nvarchar(256),
    @NewPassword      nvarchar(128),
    @PasswordSalt     nvarchar(128),
    @CurrentTimeUtc   datetime,
    @PasswordFormat   int = 0
AS
BEGIN
    DECLARE @UserId uniqueidentifier
    SELECT  @UserId = NULL
    SELECT  @UserId = u.UserId
    FROM    dbo.aspnet_Users u, dbo.aspnet_Applications a, dbo.aspnet_Membership m
    WHERE   LoweredUserName = LOWER(@UserName) AND
            u.ApplicationId = a.ApplicationId  AND
            LOWER(@ApplicationName) = a.LoweredApplicationName AND
            u.UserId = m.UserId

    IF (@UserId IS NULL)
        RETURN(1)

    UPDATE dbo.aspnet_Membership
    SET Password = @NewPassword, PasswordFormat = @PasswordFormat, PasswordSalt = @PasswordSalt,
        LastPasswordChangedDate = @CurrentTimeUtc
    WHERE @UserId = UserId
    RETURN(0)
END

正如您所看到的,更新语句可能完全失败,而存储过程可能会返回 true。我认为这可能是您遇到错误的原因所在。可能是锁定问题...


1

这确实是一个有趣的问题。"它对某些人有效,对其他人无效"的部分真的很奇怪。

这是一个间歇性的问题,还是对某些用户始终发生,而对其他用户始终不发生?

这里的另一个人建议运行ValidateUser(username, newPassword)来确认用户能够正确验证身份,然后再假定成功。

你试过这个吗?你可以不断循环,重置+更改密码,直到ValidateUser成功为止,也许在N次失败后退出。

bool success = false;
int numAttempts = 0;
do
{
    string pwd = user.ResetPassword();
    if (user.ChangePassword(pwd, confirmPassword))
    {
        success = Membership.ValidateUser(user.UserName, pwd);
    }
    numAttempts++;
} while(numAttempts < 5 && !success);

注意:此操作仅用于测试,不适用于生产环境,只是为了检查是否解决了问题。


1

我在想问题可能是你在修改密码之前重置了密码。不需要深入了解Membership类的所有内部细节,你可以尝试在这两个命令之间加入一些延迟吗?


1
你正在使用哪个MemberShipProvider?对于每个用户来说都是一样的吗?例如,如果你使用SqlMembershipProvider并将enablePasswordReset设置为false,则会悄悄地无法更新密码。在这种情况下,ChangePassword会返回true,就好像一切都进行得很顺利。

1
你是使用一个 Web 服务器还是多个 Web 服务器?如果使用多个服务器,可能会导致用于加密密码的机器密钥在所有服务器上不同。

0

你的主要catch块是否会抛出异常,而你没有注意到呢?

catch (Exception ex)
{
    ErrorSignal.FromCurrentContext().Raise(new Exception("ResetPassword: " + ex));
    return Json(new { Error = ex.Message + " -> " 
            + ex.InnerException.Message }, JsonRequestBehavior.AllowGet);
}

ex.InnerException.Message语句不安全,因为它可能会抛出NullReferenceException异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接