2013-05-03 168 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