10

我有一个方法:

public ??? AuthManager.Login(Credentials credentials)

这是此方法的一组有效输出值:

  1. 成功 (+accountId)
  2. 失败:AccountLockedOut
  3. 失败:用户名未找到
  4. 失败:InvalidPassword(+失败尝试计数)

根据返回类型,向用户显示不同的视图(是的,AccountLockedOut 的视图与 InvalidPassword 不同)。

我可以选择:

public class LoginAttemptResult {
    public bool Succeeded { get; set; }
    public AccountId AccountId { get; set; } // for when success
    public LoginAttemptResultEnumType Result { get;set; } // Success, Lockedout, UsernameNotFound, InvalidPassword  
    public int FailedAttemptCount { get; set; } // only used for InvalidPassword
}

我不喜欢这样,正在寻找更好的解决方案。首先,这导致了一个部分初始化的对象,二是它违反了接口隔离原则,三是它违反了 SRP。

更新:抛出异常也不是一个优雅的解决方案,因为InvalidPassword我认为它不是异常。失败的数据库连接是一个例外。空参数是一个例外。InvalidPassword是一个有效的预期响应。

我认为更好的解决方案是创建类的层次结构:

abstract class LoginAttemptResult
    sealed class LoginSuccess : LoginAttemptResult { AccountId }
    abstract class LoginFailure : LoginAttemptResult
        sealed class InvalidPasswordLoginFailure : LoginFailure { FailedAttemptCount }
        sealed class AccountLockedoutLoginFailure : LoginFailure

然后方法的调用者Login必须执行以下操作:

if (result is LoginSuccess) { 
    ..."welcome back mr. account id #" + (result as LoginSuccess).AccountId
}
else if (result is InvalidPasswordLoginFailure ) { 
    ..."you failed " + (result as InvalidPasswordLoginFailure).FailedAttemptCount + " times"
}

我认为这种方法(概念上)没有任何问题(除了它附带的许多类)。

这种方法还有什么问题?

请注意,这种方法本质上是 F# 的可区分联合 (DU)

有没有更好的方法来建模这个?我已经有几个可行的解决方案 - 现在我想要一个优雅的解决方案。

4

7 回答 7

4

如果结果类差异很大并且您需要为每个类设置一个单独的类,我认为您的解决方案是可以的。但我不确定。为每个结果尝试这个类:

/// <summary>
/// Immutable, created by the server
/// </summary>
class LoginResult
{
    /// <summary>
    /// Null in the case of failure
    /// </summary>
    public int? Id { get; private set; }

    /// <summary>
    /// Null in the case of success
    /// </summary>
    public string FailReason { get; private set; }

    /// <summary>
    /// Always >= 1
    /// </summary>
    public int AttemptNumber { get; private set; }

    public LoginResult(int id, int attemptNumber)
    {
        Id = id;
        AttemptNumber = attemptNumber;
    }

    public LoginResult(string reason, int attemptNumber)
    {
        FailReason = reason;
        AttemptNumber = attemptNumber;
    }
}

我可以想象,您的身份验证逻辑可能非常复杂,并且,Id不仅是您需要的属性。在这种情况下,您需要向我们展示更具体的示例,如有必要,我们将尝试构建适合您的逻辑的抽象。在这种特殊情况下 - 抽象没有意义。FailReasonAttemptNumber

于 2013-05-03T15:45:34.117 回答
1

总结:而不是返回一个值并对其进行解码 - 给 Login 一组处理程序,以便Login调用适当的回调(想想 jQuery 的ajax { success: ..., error: ... }

Login方法的使用者必须使用实质上的 switch 语句来解码响应。重构此代码以消除“switch”语句并消除自定义类型的爆炸的一种方法是,而不是要求 Login 方法返回可区分的联合 - 我们为 Login 方法提供一组 thunk - 每个响应一个。

(微妙的一点)从技术上讲,我们并没有摆脱自定义类,我们只是用泛型替换它们,即我们替换InvalidPasswordFailedLogin { int failedAttemptCount }Action<int>. 这种方法也提供了一些有趣的机会,例如登录可以更自然地异步处理。另一方面,测试变得更加模糊。

public class LoginResultHandlers {
    public Action<int> InvalidPassword { get; set; }
    public Action AccountLockedout { get; set; }
    public Action<AccountId> Success { get; set; }
}

public class AccountId {}

public class AuthManager {
    public void Login(string username, string password, LoginResultHandlers handler) {
        // if (...
            handler.Success(new AccountId());
        // if (...
            handler.AccountLockedout();
        // if (...
            handler.InvalidPassword(2);
    }
}

public class Application {
    public void Login() {
        var loginResultHandlers = new LoginResultHandlers {
                AccountLockedout = ShowLockedoutView,
                InvalidPassword = (failedAttemptCount) => ShowInvalidPassword(failedAttemptCount),
                Success = (accountId) => RedirectToDashboard(accountId)
        };
        new AuthManager().Login("bob", "password", loginResultHandlers);
    }

    private void RedirectToDashboard(AccountId accountId) {
        throw new NotImplementedException();
    }

    private void ShowInvalidPassword(int failedAttemptCount) {
        throw new NotImplementedException();
    }

    private void ShowLockedoutView() {
        throw new NotImplementedException();
    }
}
于 2013-05-03T16:33:43.920 回答
0

Another possible approach is to create a class that encapsulates the Login process and its results, like this:

    public interface ILoginContext
    {
        //Expose whatever properties you need to describe the login process, such as parameters and results

        void Login(Credentials credentials);
    }

    public sealed class AuthManager
    {
        public ILoginContext GetLoginContext()
        {
            return new LoginContext(this);
        }

        private sealed class LoginContext : ILoginContext
        {
            public LoginContext(AuthManager manager)
            {
                //We pass in manager so that the context can use whatever it needs from the manager to do its job    
            }
            //...
        }
    }

Basically what this design implies is that logging in has become a complex enough operation that a single method is no longer an appropriate encapsulation. We need to return a complex result and might want to include more complex parameters. Because the class is now responsible for the behavior and not just representing data, it's less likely to be considered a violation of SRP; it's just a somewhat complex class for a somewhat complex operation.

Note that you might also make the LoginContext implement IDisposable if it has a natural transactional scope.

于 2013-05-03T15:59:50.400 回答
0

你可以返回一个元组

public Tuple<T1,T2> AuthManager.Login(Credentials credentials){
//do your stuff here
return new Tuple<T1,T2>(valueOfT1,valueOfT2);
}
于 2013-05-03T15:42:16.353 回答
0

如果你让你的LoginAttemptResult类抽象,那么你可以添加一个抽象属性Message来强制你的子类实现它。

public abstract class LoginAttemptResult
{        
    public abstract string Message { get; }

    // any other base methods/properties and abstract methods/properties here
}

然后,您的孩子可能看起来像这样:

public class LoginSuccess : LoginAttemptResult
{
    public override string Message 
    { 
        get
        {
            return "whatever you use for your login success message";
        }
    }
}

这样,您的 Login 方法就可以返回一个LoginAttemptResult

public LoginAttemptResult AuthManager.Login(Credentials credentials)
{
    // do some stuff
}

然后你的来电者只会打电话给你的LoginAttemptResult.Message(或你需要它做的任何其他事情):

var loginResult = AuthManager.Login(credentials);
var output = loginResult.Message;

同样,如果您需要LoginAttemptResult基于子类型与您关联的其他方法,您可以将其定义为基类中的抽象方法,在子类中实现它,然后以完全相同的方式调用它。

于 2013-05-03T15:47:40.987 回答
0

这是一个满足我所有要求(可读性、可测试性、可发现性和美观性)的解决方案。

代码(注意实现与原始任务略有不同,但概念仍然存在):

public class AuthResult {
    // Note: impossible to create empty result (where both success and failure are nulls).
    // Note: impossible to create an invalid result where both success and failure exist.
    private AuthResult() {}
    public AuthResult(AuthSuccess success) {
        if (success == null) throw new ArgumentNullException("success");
        this.Success = success;
    }
    public AuthResult(AuthFailure failure) {
        if (failure == null) throw new ArgumentNullException("failure");
        this.Failure = failure;
    }
    public AuthSuccess Success { get; private set; }
    public AuthFailure Failure { get; private set; }
}

public class AuthSuccess {
    public string AccountId { get; set; }
}

public class AuthFailure {
    public UserNotFoundFailure UserNotFound { get; set; }
    public IncorrectPasswordFailure IncorrectPassword { get; set; }
}

public class IncorrectPasswordFailure : AuthResultBase {
    public int AttemptCount { get; set; }
}

public class UserNotFoundFailure : AuthResultBase {
    public string Username { get; set; }
}

请注意如何AuthResult正确建模函数范围的异构和分层性质。

如果您添加以下隐式运算符:

public static implicit operator bool(AuthResultBase result) {
    return result != null;
}

您可以按如下方式使用结果:

var result = authService.Auth(credentials);
if (result.Success) {
    ...
}

读起来(可以说)比:

if (result.Success != null) {
    ...
}
于 2013-09-10T16:23:40.720 回答
0

您的安全 API 不应该暴露太多信息。您发布的 API 没有向客户提供任何有用的信息,只是帮助攻击者尝试劫持帐户。您的登录方法应该只提供通过/失败信息和可以传递给您需要的任何授权机制的令牌。

// used by clients needing to authenticate
public interfac ISecurity {
  AuthenticationResponse Login(Credentials credentials);
}

// the response from calling ISecurity.Login
public class AuthenticationResponse {

  internal AuthenticationResponse(bool succeeded, AuthenticationToken token, string accountId) {
    Succeeded = succeeded;
    Token = token;
  }

  // if true then there will be a valid token, if false token is undefined
  public bool Succeeded { get; private set; }

  // token representing the authenticated user.
  // document the fact that if Succeeded is false, then this value is undefined
  public AuthenticationToken Token { get; private set; }

}

// token representing the authenticated user. simply contains the user name/id
// for convenience, and a base64 encoded string that represents encrypted bytes, can
// contain any information you want.
public class AuthenticationToken {

  internal AuthenticationToken(string base64EncodedEncryptedString, string accountId) {
    Contents = base64EncodedEncryptedString;
    AccountId = accountId;
  }

  // secure, and user can serialize it
  public string Contents { get; private set; }

  // used to identify the user for systems that aren't related to security
  // (e.g. customers this user has)
  public string AccountId { get; private set; }

}


// simplified, but I hope you get the idea. It is what is used to authenticate
// the user for actions (i.e. read, write, modify, etc.)
public interface IAuthorization {
  bool HasPermission(AuthenticationToken token, string permission); 
}

您会注意到此 API 没有登录尝试。客户端不应该关心登录所涉及的规则。ISecurity接口的实现者应该跟上登录尝试,并在成功传递一组凭据但超过尝试次数时返回失败。

一条关于失败的简单消息应该类似于以下内容:

Could not log you on at this time. Check that your username and/or password are correct, or please try again later.
于 2013-05-03T17:34:53.653 回答