Статьи

Интеграция с reCAPTCHA с помощью Spring Integration

Иногда нам просто нужна капча , это печальный факт. Сегодня мы узнаем, как интегрироваться с reCAPTCHA . Поскольку сама тема не особенно интересна и продвинута, мы немного переоценим (?) Использование Spring Integration для обработки низкоуровневых деталей. Решение использовать reCAPTCHA от Google было продиктовано двумя факторами: (1) это умеренно хорошая реализация CAPTCHA с приличными изображениями со встроенной поддержкой людей с нарушениями зрения и (2) аутсорсинг CAPTCHA позволяет нам оставаться без состояния на стороне сервера , Не говоря уже о том, что мы помогаем в оцифровке книг.

Вторая причина на самом деле очень важна. Обычно вы должны сгенерировать CAPTCHA на стороне сервера и сохранить ожидаемый результат, например, в пользовательском сеансе. Когда ответ возвращается, вы сравниваете ожидаемое и введенное решение CAPTCHA. Иногда мы не хотим хранить какое-либо состояние на стороне сервера, не говоря уже о том, что реализация CAPTCHA не является особенно полезной задачей. Так что приятно иметь что-то готовое и приемлемое. Полный исходный код как всегда доступен, мы начинаем с простого веб — приложения Spring MVC без каких — либо CAPTCHA. reCAPTCHA бесплатна, но требует регистрации, поэтому первым делом нужно подписаться и сгенерировать ваши открытые / закрытые ключи и заполнить app.properties

файл конфигурации в нашем примере проекта.

Чтобы отобразить и включить reCAPTCHA в форму, все, что вам нужно сделать, это добавить библиотеку JavaScript:

<div id="recaptcha"> </div>
...
<script src="http://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>

И разместите виджет reCAPTCHA где угодно: 

Recaptcha.create("${recaptcha_public_key}",
  "recaptcha",
  {
    theme: "white",
    lang : 'en'
  }
);

Официальная документация очень кратким и описательный характер, поэтому я не ныряя в детали этого. Когда вы включаете этот виджет в <form />, вы получите два дополнительных поля, когда пользователь отправляет: recaptcha_response_field и recaptcha_challenge_field. Первый — это текст, набранный пользователем, а второй — скрытый токен, сгенерированный для каждого запроса. Вероятно, он используется серверами reCAPTCHA в качестве ключа сеанса, но нам все равно, все, что нам нужно сделать, это передать эти поля дальше серверу reCAPTCHA . Я буду использовать HttpClient 4 для выполнения HTTP-запроса к внешнему серверу и некоторого умного сопоставления с образцом в Scala для анализа ответа:

trait ReCaptchaVerifier {
  def validate(reCaptchaRequest: ReCaptchaSecured): Boolean
 
}
 
@Service
class HttpClientReCaptchaVerifier @Autowired()(
                                                  httpClient: HttpClient,
                                                  servletRequest: HttpServletRequest,
                                                  @Value("${recaptcha_url}") recaptchaUrl: String,
                                                  @Value("${recaptcha_private_key}") recaptchaPrivateKey: String
                                                  ) extends ReCaptchaVerifier {
 
  def validate(reCaptchaRequest: ReCaptchaSecured): Boolean = {
    val post = new HttpPost(recaptchaUrl)
    post.setEntity(new UrlEncodedFormEntity(List(
      new BasicNameValuePair("privatekey", recaptchaPrivateKey),
      new BasicNameValuePair("remoteip", servletRequest.getRemoteAddr),
      new BasicNameValuePair("challenge", reCaptchaRequest.recaptchaChallenge),
      new BasicNameValuePair("response", reCaptchaRequest.recaptchaResponse)))
    )
    val response = httpClient.execute(post)
    isReCaptchaSuccess(response.getEntity.getContent)
  }
 
  private def isReCaptchaSuccess(response: InputStream) = {
    val responseLines = Option(response) map {
      Source.fromInputStream(_).getLines().toList
    } getOrElse Nil
    responseLines match {
      case "true" :: _ => true
      case "false" :: "incorrect-captcha-sol" :: _=> false
      case "false" :: msg :: _ => throw new ReCaptchaException(msg)
      case resp => throw new ReCaptchaException("Unrecognized response: " + resp.toList)
    }
  }
 
}
 
class ReCaptchaException(msg: String) extends RuntimeException(msg)

Единственный недостающий элемент — это признак ReCaptchaSecured, инкапсулирующий два поля reCAPTCHA, упомянутых ранее. Чтобы обеспечить безопасность любой веб-формы с помощью reCAPTCHA, я просто расширяю эту модель: 

trait ReCaptchaSecured {
  @BeanProperty var recaptchaChallenge = ""
  @BeanProperty var recaptchaResponse = ""
}
 
class NewComment extends ReCaptchaSecured {
  @BeanProperty var name = ""
  @BeanProperty var contents = ""
}

Весь CommentsController.scala не так уж актуален. Но результат есть!

Так что это работает, но, очевидно, это не было действительно впечатляющим. Что бы вы сказали о замене низкоуровневого вызова HttpClient на Spring Integration? Интерфейс ReCaptchaVerifier (черта) остается тем же самым, поэтому код клиента не должен быть изменен. Но мы реорганизовали HttpClientReCaptchaVerifier в два отдельных, небольших, относительно высокого уровня и абстрактных класса:

@Service
class ReCaptchaFormToHttpRequest @Autowired() (servletRequest: HttpServletRequest, @Value("${recaptcha_private_key}") recaptchaPrivateKey: String) {
 
  def transform(form: ReCaptchaSecured) = Map(
    "privatekey" -> recaptchaPrivateKey,
    "remoteip" -> servletRequest.getRemoteAddr,
    "challenge" -> form.recaptchaChallenge,
    "response" -> form.recaptchaResponse).asJava
 
}
 
@Service
class ReCaptchaServerResponseToResult {
 
  def transform(response: String) = {
    val responseLines = response.split('\n').toList
    responseLines match {
      case "true" :: _ => true
      case "false" :: "incorrect-captcha-sol" :: _=> false
      case "false" :: msg :: _ => throw new ReCaptchaException(msg)
      case resp => throw new ReCaptchaException("Unrecognized response: " + resp.toList)
    }
  }
 
}

Обратите внимание, что нам больше не нужно реализовывать ReCaptchaVerifier, Spring Integration сделает это за нас. Мы только должны сказать, как фреймворк предполагает использовать строительные блоки, которые мы извлекли выше. Я думаю, что я еще не описал, что такое Spring Integration и как она работает. В нескольких словах это очень чистая реализация шаблонов корпоративной интеграции (некоторые могут назвать это ESB). Потоки сообщений описываются с использованием XML и могут быть встроены в стандартную конфигурацию Spring XML: 

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/integration"
       xmlns:http="http://www.springframework.org/schema/integration/http"
       xsi:schemaLocation="
           http://www.springframework.org/schema/integration
           http://www.springframework.org/schema/integration/spring-integration.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/integration/http
           http://www.springframework.org/schema/integration/http/spring-integration-http.xsd
           ">
 
       <!-- configuration here -->   
 
</beans:beans>

В нашем случае мы опишем поток сообщений от Java-интерфейса HttpClientReCaptchaVerifier / признака Scala к серверу reCAPTCHA и обратно. По пути объект ReCaptchaSecured должен быть преобразован в запрос HTTP, а ответ HTTP должен быть преобразован в значимый результат, прозрачно возвращаемый из интерфейса. 

<gateway id="ReCaptchaVerifier" service-interface="com.blogspot.nurkiewicz.recaptcha.ReCaptchaVerifier" default-request-channel="reCaptchaSecuredForm"/>
 
<channel id="reCaptchaSecuredForm" datatype="com.blogspot.nurkiewicz.web.ReCaptchaSecured"/>
 
<transformer input-channel="reCaptchaSecuredForm" output-channel="reCaptchaGoogleServerRequest" ref="reCaptchaFormToHttpRequest"/>
 
<channel id="reCaptchaGoogleServerRequest" datatype="java.util.Map"/>
 
<http:outbound-gateway
    request-channel="reCaptchaGoogleServerRequest"
    reply-channel="reCaptchaGoogleServerResponse"
    url="${recaptcha_url}"
    http-method="POST"
    extract-request-payload="true"
    expected-response-type="java.lang.String"/>
 
<channel id="reCaptchaGoogleServerResponse" datatype="java.lang.String"/>
 
<transformer input-channel="reCaptchaGoogleServerResponse" ref="reCaptchaServerResponseToResult"/>

Несмотря на объем XML, общий поток сообщений довольно прост. Сначала мы определим шлюз , который является мостом между интерфейсом Java и потоком сообщений Spring Integration. Аргумент ReCaptchaVerifier.validate () позже становится сообщением , которое отправляется на канал reCaptchaSecuredForm . Из этого канала объект ReCaptchaSecured передается преобразователю ReCaptchaFormToHttpRequest . Целью преобразователя является два преобразования из объекта ReCaptchaSecured в карту Java, представляющую набор пар ключ-значение. Позже эта карта передается (через канал reCaptchaGoogleServerRequest) в http: outbound-gateway. Ответственность этого компонента заключается в том, чтобы преобразовать ранее созданную карту в HTTP-запрос и отправить его по указанному адресу.

Когда ответ возвращается, он отправляется на канал reCaptchaGoogleServerResponse. Там преобразователь ReCaptchaServerResponseToResult выполняет действие, переводя HTTP-ответ на бизнес-результат (логическое значение). Наконец, результат преобразователя направляется обратно к шлюзу. По умолчанию все происходит синхронно, поэтому мы все еще можем использовать простой интерфейс Java для проверки reCAPTCHA.

Верьте или нет, все это работает. Мы больше не используем HttpClient (думаю, что все лучше по сравнению с HttpClient 4 API …), и вместо одного «огромного» класса у нас есть набор небольших, сфокусированных, легко тестируемых классов. Каркас обрабатывает подключение и детали низкого уровня. Замечательный?

Мечта архитектора или кошмар разработчика?

Позвольте мне обобщить наши усилия, приведя выводы из вышеприведенной презентации:
сбалансировать архитектурные преимущества с эффективностью разработки, Spring Integration может получать данные из различных разнородных источников, таких как JMS, реляционная база данных или даже FTP, объединять, разбивать, анализировать и фильтровать сообщения несколькими способами и, наконец, отправлять их дальше с помощью самых экзотических протоколов. Кодирование всего этого вручную — действительно утомительная и подверженная ошибкам задача. С другой стороны, иногда нам просто не нужны все изворотливости, и грязные руки (например, путем выполнения HTTP-запроса вручную и анализа ответа) намного проще и легче для понимания. Прежде чем слепо основывать всю свою архитектуру либо на абстракциях очень высокого уровня, либо на низкоуровневых процедурах, написанных вручную, подумайте о последствиях и балансе. Нет решения подходит для всех проблем. Какую версию интеграции reCAPTCHA вы считаете лучше?