Я оставил грязную тайну, скрывающуюся в моем предыдущем посте о двухэтапной аутентификации с текстовыми сообщениями . Я надеюсь, что многие из вас чувствовали себя неловко, читая val secret = generateSecret
строки. generateSecret
Вещь не имеет места в чисто функциональном коде. Это делает некоторые странные побочные эффекты и вычисляет значения, которые всегда отличаются. Читайте дальше, чтобы узнать, как следить за грязными секретами (и другими операциями ввода-вывода) в вашем коде.
Проблема снова
Вспомните бит кода, который сгенерировал токен аутентификации, и секрет, который будет доставлен пользователю.
val token = UUID.randomUUID() val secret = generateSecret // save the token create(AuthenticationToken(UUID.randomUUID(), token, new Date(), true, 2, Some(secret))) // deliver the secret to the user val deliveryAddress = DeliveryAddress(Some("447*******"), None) second ! DeliverSecret(deliveryAddress, secret) // we're logged in partially sender ! Right(LoggedInPartially(token))
Строки, которые оскорбляют мое чувство правильного кода , val token = UUID.randomUUID()
и val secret = generateSecret
. Давайте выберем один из них и рассмотрим его. Он говорит , что secret
равно generateSecret
, следовательно, должно быть возможно заменить secret
с generateSecret
в коде ниже.
Ой, подожди. Мы не можем этого сделать. Эта функция generateSecret
вообще не является функцией. Это делает что-то и извергает секреты. Так же, как Daily Mail-o-matic . Если заменить оба вхождения secret
с generateSecret
в коде выше, она не будет выполнена. Мы должны содержать побочные эффекты.
Сдерживание
Нам все еще нужно выполнять эти странные побочные эффекты, но мы должны их сдерживать. Мы собираемся использовать систему типов с помощью компилятора, чтобы убедиться, что мы не делаем ничего прикольного. Вместо того, чтобы иметь функцию, которая возвращает секреты как String
s, у нас будет функция, которая возвращает ящики с секретом String
s. Мы также заметим, что мы можем составить эти коробки вместе, чтобы сформировать большие коробки.
Право-хо.
Thinking about this further, it turns out that it’s not just the secret that’s randomly generated, it is also the token number. In fact, we can expand the randomness to the entire AuthenticationToken
. And so, we’ll have the AuthenticationTokenGenerator
.
private[authentication] trait AuthenticationTokenGenerator { def generateAuthenticationToken(userRef: UUID): IO[AuthenticationToken] = IO(AuthenticationToken( userRef, UUID.randomUUID(), new Date(), false, 0, None)) def generateSecret(token: AuthenticationToken): IO[AuthenticationToken] = IO(token.copy( secret = Some(UUID.randomUUID().toString.substring(0, 5)), partial = true)) }
Notice the return types of the generateAuthenticationToken
and generateSecret
. They are no longer the dirty, random AuthenticationToken
values themselves, but boxes that carry the random values. This is rather important. Whenever we call generateAuthenticationToken
, we get a box that carries the generated AuthenticationToken
. And so, we cannot use it when we need just the AuthenticationToken
. This leads us quite nicely to being able to wire these boxes together. I will show you the LoginActor
again in its entirety:
/** * Login actor that supervises the actors in the login process. */ class LoginActor(secretDelivery: ActorRef) extends Actor with AuthenticationTokenGenerator with TokenOperations { import scalaz.syntax.monad._ /** * Saves the token in some persistent store * * @param at the token to be saved * @return the IO of the saved token */ def createToken(at: AuthenticationToken): IO[AuthenticationToken] = IO(create(at)) /** * Sends the secret to the user * * @param at the authentication token * @return the IO of the token */ def deliverSeret(at: AuthenticationToken): IO[AuthenticationToken] = { val deliveryAddress = DeliveryAddress(None, Some("[email protected]")) secretDelivery ! DeliverSecret(deliveryAddress, at.secret.get) IO(at) } def receive = { case FirstLogin(username, password, clientSignature) => // check that username & password are OK if (username == "root" && password == "p4ssw0rd") { val userRef = UUID.fromString("a3372060-2b3b-11e2-81c1-0800200c9a66") // the account is there, and needs 2nd phase auth val action = generateAuthenticationToken(userRef) >>= generateSecret >>= createToken >>= deliverSeret >>= { at => sender ! Right(LoggedInPartially(at.token)); IO () } action.unsafePerformIO() } else { // not hardcoded username or password, so... sender ! Left(BadUsernameOrPassword()) } case SecondLogin(token, secret) => find(token) match { case None => sender ! Left(BadPartialToken()) case Some(at) if !at.isValid(secret) && at.retries == 0 => // no more retries delete(at.token) sender ! Left(TooManyBadSecrets()) case Some(at) if !at.isValid(secret) && at.retries > 0 => // bad secret, but retries still allowed update(at.copy(retries = at.retries - 1)) sender ! Left(BadPartialToken()) case Some(at) if at.isValid(secret) => // delete the old one delete(at.token) val action = generateAuthenticationToken(at.userRef) >>= createToken >>= { at => sender ! Right(LoggedInFully(at.token)); IO () } action.unsafePerformIO() } } }
So, there you have it. We have carefully packaged up the side-effects into boxes and assembled the boxes together to get a bigger box. Together with the type checking we get from the compiler, we can ensure that we do not let dirty secrets escape into the rest of our codebase. And, I’m also happy that the =
symbol means just that I can replace the symbol with whatever is on the right hand side. And the world is a bit nicer place.