2013-05-03 123 views
10

我有一个方法:从C#方法返回不同类型

public ??? AuthManager.Login(Credentials credentials) 

下面是一组这种方法的有效输出值:

  1. 成功(+帐户ID)
  2. 失败: AccountLockedOut
  3. 失败:UsernameNotFound
  4. 失败:无效密码(+失败的尝试次数)

根据返回类型,向用户显示不同的视图(是的,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#的discriminated union (DU)

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

+5

会投掷你的项目失败的例外情况? – Dirk 2013-05-03 15:28:47

+0

Dirks的建议很有意义 - 当一切正常时返回登录信息 - 当事情出错并引发任何需要的信息时抛出异常。代码看起来并不关键,但问题是......“登录失败是一种特殊情况?”。 ASP.NET成员资格有什么作用? AD如何? – Charleh 2013-05-03 15:30:51

+3

想到这一点,但我不认为失败的登录是一个例外情况。如果我无法连接到数据库,或者凭据为空,则会抛出异常 - 这些异常。我可以将方法重命名为'TryLogin',以清楚地表明它尝试登录,这种失败尝试是预期的响应之一。 – 2013-05-03 15:31:34

回答

0

你可以做返回Tuple

public Tuple<T1,T2> AuthManager.Login(Credentials credentials){ 
//do your stuff here 
return new Tuple<T1,T2>(valueOfT1,valueOfT2); 
} 
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; 
    } 
} 

我可以想像,你的验证逻辑可能会非常复杂,IdFailReasonAttemptNumber不仅是你需要的属性。在这种情况下,您需要向我们展示更具体的示例,如果需要,我们将尝试构建适合您的逻辑的抽象。在这个特定的情况下 - 没有抽象意义。

+1

ISP违规,SRP违规,以及部分初始化的对象异味。此解决方案有效。我正在寻找一个可行的优雅解决方案。 – 2013-05-03 16:07:24

+5

@ THX-1138这些原则的使命是打击复杂性。你的情况是微不足道的,所以不需要花费时间(你的和其他读者的代码),代码行。并记住奥卡姆的剃刀原理 – astef 2013-05-03 16:16:22

+0

@astef它解决了具体问题,但OP的建议也是如此。问题是关于如何做到正确的方式,并且对于其他情况也可能非常有价值。 – 2013-05-03 17:00:29

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"; 
     } 
    } 
} 

就这样,你的登录方法可以只返回一个LoginAttemptResult

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

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

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

同样,如果你需要与你的LoginAttemptResult根据孩子类型相关的一些其他的方法,你可以把它定义为一个抽象的方法在您的基类中,在您的子类中实现它,然后以完全相同的方式调用它。

+0

1.消息将不会在SuccessLogin中使用。 2.本地化成为AuthManager关注的焦点。 3.由于我对不同的回应有不同的看法 - 只是回复信息可能不够。 – 2013-05-03 16:08:43

0

另一种可能的方法是创建一个封装了登录过程及其结果,像这样一类:

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  
      } 
      //... 
     } 
    } 

基本上就是这样的设计意味着的是,登录已经成为一个足够复杂的操作,一个单一的方法不再是一个合适的封装。我们需要返回一个复杂的结果,并可能需要包含更复杂的参数。因为班级现在对行为负责,而不只是表示数据,所以不太可能被视为违反SRP;对于一个稍微复杂的操作来说,这只是一个稍微复杂的类。

请注意,如果LoginContext具有自然的事务范围,您也可以使LoginContext实现IDisposable。

1

摘要:而不是返回值,并对其进行解码 - 给登录了一套处理程序,以便Login将调用适当的回调(认为jQuery的ajax { success: ..., error: ... }

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

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

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(); 
    } 
} 
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没有登录尝试。客户端不应该关心与登录有关的规则。接口的实现者应该保持登录尝试的状态,并且在成功传递一组凭据时返回失败,但尝试的次数已被优先考虑。

失败一个简单的消息应该沿着线读的东西:

Could not log you on at this time. Check that your username and/or password are correct, or please try again later. 
+0

你错过了这个问题的关键。此外,如果登录attmpt由于帐户尚未激活而失败,我想告诉用户并提供重新发送激活电子邮件的选项。与此类似,我想在输入无效密码时显示一条消息 - 类似于LogMeIn的操作 - “无效的密码。在您的帐户被锁定之前,您还有3次尝试。” – 2013-05-03 17:48:48

+0

如果用户尚未被激活,他们仍然应该进行身份验证。你可以添加一个'bool ISecurity.HasBeenActivated(AuthenticationToken token)'方法。用户已通过身份验证,但用户无权访问任何内容。对'HasBeenActivated'的调用可以将它们重定向到一个页面,或者显示一条消息说明。您甚至可以将这些信息存储在加密数据中,以便您不必再次检索。关键在于实现“ISecurity”接口的代码负责处理这个问题。仅仅因为LogMeIn这样做并不意味着它是安全的。 – 2013-05-03 18:35:37

+0

如果您坚持尝试计数,您可以简单地将'LoginAttempts'属性添加到'AuthenticationResponse'类。如果“成功”是真或假,它将具有有效值。 – 2013-05-03 18:36:48

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) { 
    ... 
} 
+0

部分初始化对象的气味? – astef 2013-12-16 08:22:43

+0

@astef:我不这么认为。构造后的任何点上的对象都处于完全初始化状态,并且在完全初始化状态下,只有一个属性可以具有值。例如。一旦设置了'AuthSuccess',你就不能'进一步'初始化对象并设置'AuthFailure'。 – 2013-12-17 05:08:16