我有一个方法:
public ??? AuthManager.Login(Credentials credentials)
以下是此方法的一组有效输出值:
根据返回类型,向用户显示不同的视图(是的,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。
UPDATE:抛出异常也不是一个优雅的解决方案,因为我认为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)。
有更好的方法对此进行建模吗?我已经有了几个有效的解决方案 - 现在我想要一个有效的优雅解决方案。
答案 0 :(得分: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
,FailReason
和AttemptNumber
不仅是您需要的属性。在这种情况下,您需要向我们展示更具体的示例,如果需要,我们将尝试构建适合您逻辑的抽象。在这种特殊情况下 - 没有抽象意义。
答案 1 :(得分:1)
总结:不是返回一个值并解码它 - 给Login一组处理程序,以便Login
调用适当的回调(想想jQuery的ajax { success: ..., error: ... }
)
Login
方法的使用者必须使用基本上的switch语句解码响应。重构此代码以消除“switch”语句并消除自定义类型爆炸的一种方法是不要求Login方法返回一个有区别的联合 - 我们给Login方法一组thunks - 每个响应一个。
(细微之处)从技术上讲,我们不会删除自定义类,我们只需用泛型替换它们,即我们将InvalidPasswordFailedLogin { int failedAttemptCount }
替换为Action<int>
。这种方法也提供了一些有趣的机会,例如Login可以更自然地处理异步。另一方面,测试变得更加模糊。
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();
}
}
答案 2 :(得分:0)
您可以返回Tuple
public Tuple<T1,T2> AuthManager.Login(Credentials credentials){
//do your stuff here
return new Tuple<T1,T2>(valueOfT1,valueOfT2);
}
答案 3 :(得分: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
相关联,则可以将其定义为基类中的抽象方法,在子类中实现它,然后调用它完全一样。
答案 4 :(得分:0)
另一种可能的方法是创建一个封装Login进程及其结果的类,如下所示:
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具有自然的事务范围,您也可以使其实现IDisposable。
答案 5 :(得分: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.
答案 6 :(得分: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) {
...
}