dagblog.
← Archiv

Jednoduchá ochrana webových formulářů pomocí JCaptcha a Spring MVC


Občas potřebujeme webový formulář ochránit před automatickým odesíláním, třeba kvůli spamu nebo útokům typu DoS. Nejbezpečnějším a běžně používaným způsobem, jak odlišit robota od člověka, je CAPTCHA. Zkratka znamená „Completely Automated Public Test to tell Computers and Humans Apart“, tedy plně automatizovaný veřejný test k rozlišení počítačů a lidí.

Ve webovém formuláři CAPTCHA tvoří dynamicky generovaný obrázek s úlohou a vstupní pole. Před každým odesláním formuláře uživatel přepíše text z obrázku do pole. Text se ověří na serveru. Pokud je správný, odeslání pokračuje. Jinak se znovu zobrazí původní formulář.

V Javě máme open-source řešení CAPTCHA s názvem JCaptcha. Do webové aplikace se dá snadno integrovat a nabízí řadu hotových modulů, například pro Struts nebo servletový filtr. Ukážeme si, jak JCaptcha propojit se Spring MVC.

Co budeme potřebovat?

Nejprve musíme dynamicky vygenerovat obrázek CAPTCHA a pak při odeslání formuláře ověřit text, který uživatel přepsal. K tomu můžeme použít základní modul JCaptcha. Poskytuje základní komponenty, z nichž se dá postavit specializovaný modul.

Jádrem JCaptcha je CaptchaService, přesněji ImageCaptchaService a její specializované implementace. Toto rozhraní definuje dvě hlavní metody, getImageChallengeForID a validateResponseForID.

  • getImageChallengeForID(String ID) — Vrátí obrázek s úlohou pro zadaný identifikátor.
  • validateResponseForID(String ID, Object response) — Ověří odpověď na obrázkovou úlohu pro zadaný identifikátor.

Jako identifikátor vygenerované CAPTCHA použijeme ID relace. Pro klienta je jedinečné a prohlížeč ho automaticky posílá s každým požadavkem. Nemusíme tedy generovat vlastní jedinečné číslo ani obsluhovat další parametr.

Generování obrázku bude z pohledu Spring MVC zapouzdřené v kontroleru.

code { font-family: Courier New, Courier; font-size: 10pt; margin: 0px; }

public class JCaptchaController implements Controller, InitializingBean{

 private ImageCaptchaService captchaService;



 /**

 * @see org.springframework.web.servlet.mvc.Controller#handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)

 */

 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {

 byte[] captchaChallengeAsJpeg = null;

 // the output stream to render the captcha image as jpeg into

 ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();

 

 // get the session id that will identify the generated captcha. 

 //the same id must be used to validate the response, the session id is a good candidate!

 String captchaId = request.getSession().getId();

 

 // call the ImageCaptchaService getChallenge method

 BufferedImage challenge =

 captchaService.getImageChallengeForID(captchaId,request.getLocale());

 

 // a jpeg encoder

 JPEGImageEncoder jpegEncoder =

 JPEGCodec.createJPEGEncoder(jpegOutputStream);

 jpegEncoder.encode(challenge);

 



 captchaChallengeAsJpeg = jpegOutputStream.toByteArray();



 // flush it in the response

 response.setHeader("Cache-Control", "no-store");

 response.setHeader("Pragma", "no-cache");

 response.setDateHeader("Expires", 0);

 response.setContentType("image/jpeg");

 ServletOutputStream responseOutputStream =

 response.getOutputStream();

 responseOutputStream.write(captchaChallengeAsJpeg);

 responseOutputStream.flush();

 responseOutputStream.close();

 return null;

 }



 /**

 * Set captcha service

 * @param captchaService The captchaService to set.

 */

 public void setCaptchaService(ImageCaptchaService captchaService) {

 this.captchaService = captchaService; 

 } 

 

 /**

 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()

 */

 public void afterPropertiesSet() throws Exception {

 if(captchaService == null){

 throw new RuntimeException("Image captcha service wasn`t set!");

 }

 }

}

 
 

Java2html

Jde o klasický kontroler, který zapisuje přímo do odpovědi. Libovolnou implementaci ImageCaptchaService lze nastavit metodou setCaptchaService. Teď potřebujeme zapojit ověřování CAPTCHA. Hodí se zdědit od třídy SimpleFormController ze Springu a přepsat metodu onBindAndValidate.

Tato metoda deleguje volání na novou metodu validateCaptcha. O samotné ověření se stará validateCaptcha. Pokud je odpověď na CAPTCHA neplatná, přidá se do objektu s chybami ověření nový ObjectError a odeslání formuláře se zruší.

public class ProtectedFormController extends SimpleFormController {
  /**
   * Default paramter name for CAPTCHA response in <code>{@link HttpServletRequest}</code>
   */
  private static final String DEFAULT_CAPTCHA_RESPONSE_PARAMETER_NAME = "j_captcha_response";
  
  protected ImageCaptchaService captchaService;
  protected String captchaResponseParameterName = DEFAULT_CAPTCHA_RESPONSE_PARAMETER_NAME;
        
  /**
   * Delegates request to CAPTCHA validation, subclasses which overrides this 
   * method must manually call <code>{@link #validateCaptcha(HttpServletRequest, BindException)}</code>
   * or explicitly call super method.
   * 
   * @see #validateCaptcha(HttpServletRequest, BindException)
   * @see org.springframework.web.servlet.mvc.BaseCommandController#onBindAndValidate(javax.servlet.http.HttpServletRequest, java.lang.Object, org.springframework.validation.BindException)
   */
  @Override
  protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors) throws Exception {        
        validateCaptcha(request, errors);
  }
  
  /**
   * Validate CAPTCHA response, if response isn`t valid creates new error object 
   * and put him to errors holder.
   * 
   * @param request current servlet request
   * @param errors errors holder
   */
  protected void validateCaptcha(HttpServletRequest request, BindException errors){
        boolean isResponseCorrect = false;
        
        //remenber that we need an id to validate!
        String captchaId = request.getSession().getId();
        //retrieve the response
        String response = request.getParameter(captchaResponseParameterName);
        //validate response
        try {          
          if(response != null){
                isResponseCorrect =
                  captchaService.validateResponseForID(captchaId, response);
          }
        } catch (CaptchaServiceException e) {
                //should not happen, may be thrown if the id is not valid          
        }
        
        if(!isResponseCorrect){
          //prepare object error, captcha response isn`t valid
                  String objectName = "Captcha";
          String[] codes = {"invalid"};
          Object[] arguments = {};
          String defaultMessage = "Wrong cotrol text!";
          ObjectError oe = new ObjectError(objectName, codes, arguments, defaultMessage);
          errors.addError(oe);
        }                 
  }

  /**
   * Set captcha service
   * @param captchaService the captchaService to set.
   */
  public void setCaptchaService(ImageCaptchaService captchaService) {
        this.captchaService = captchaService;
  }

  /**
   * Set paramter name for CAPTCHA response in <code>{@link HttpServletRequest}</code>
   * @param captchaResponseParameterName the captchaResponseParameterName to set.
   */
  public void setCaptchaResponseParameterName(String captchaResponseParameterName) {
        this.captchaResponseParameterName = captchaResponseParameterName;
  }
}

 
 

Java2html

Teď už můžeme Spring a JCaptcha použít. Jako ukázku jsem zvolil odeslání komentáře. Komentář je reprezentovaný jako JavaBean a kontroler formuláře je podtřídou ProtectedFormController, která ověřuje CAPTCHA.

public class Comment {

 private String email;

 private String subject;

 private String body;

 

 //Getters and Setters

}

 
 

Java2html

public class NewCommentForm extends ProtectedFormController {

 

 /**

 * @see org.springframework.web.servlet.mvc.SimpleFormController#doSubmitAction(java.lang.Object)

 */

 @Override

 protected void doSubmitAction(Object command) throws Exception {

 Comment comment = (Comment) command;

 //do something with new comment for example save to database.

 } 

}

 
 

Java2html

Oba kontrolery i ostatní součásti se nastavují v *-servlet.xml.


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> 
 <bean id="formController" class="cz.sweb.pichlik.springtutorial.captcha.NewCommentForm">     
     <property name="captchaService"><ref bean="captchaService"/></property>
     <property name="commandClass"><value>cz.sweb.pichlik.springtutorial.captcha.Comment</value></property>
     <property name="formView"><value>form</value></property>
  <property name="successView"><value>submit</value></property>
  <property name="commandName"><value>comment</value></property>
    </bean>   
&lt;!-- This controller generates CAPTCHA image --&gt;
&lt;bean id="captchaController" class="cz.sweb.pichlik.springtutorial.captcha.JCaptchaController"&gt;
 &lt;property name="captchaService"&gt;&lt;ref bean="captchaService"/&gt;&lt;/property&gt;
&lt;/bean&gt; 
   

<bean id=“simpleUrlMapping” class=“org.springframework.web.servlet.handler.SimpleUrlHandlerMapping”>
<property name=“mappings”> <props>
<prop key=“/newcomment.htm”>formController</prop> <prop key=“/captcha.htm”>captchaController</prop>
</props> </property> </bean>

<bean id=“viewResolver” class=“org.springframework.web.servlet.view.InternalResourceViewResolver”> <property name=“viewClass”><value>org.springframework.web.servlet.view.JstlView</value></property> <property name=“prefix”><value>/WEB-INF/jsp/</value></property> <property name=“suffix”><value>.jsp</value></property> </bean> </beans>

Konfigurace aplikačního kontextu obsahuje jediný bean, captchaService. Je nastavený jako singleton a jako konkrétní implementaci služby CAPTCHA používá výchozí třídu JCaptcha DefaultManageableImageCaptchaService>.


<?xml version=“1.0” encoding=“UTF-8”?>
<!DOCTYPE beans PUBLIC ”-//SPRING//DTD BEAN//EN” “http://www.springframework.org/dtd/spring-beans.dtd”>
<beans>
<!— CAPTCHA SERVICE DEFINITION —>
<bean id=“captchaService”
class=“com.octo.captcha.service.image.DefaultManageableImageCaptchaService”
singleton=“true”/>
</beans>

HTML kód je v souboru form.jsp. Všimněte si, jak URL v atributu src elementu img volá JCaptchaController. Smyčka forEach vypíše chyby z ověření včetně hlášky o neplatné odpovědi na CAPTCHA.


<%@ page session=“true” contentType=“text/html; charset=utf-8” pageEncoding=“utf-8” %>
<%@ taglib uri=“http://java.sun.com/jsp/jstl/core” prefix=“c” %>
<%@ taglib uri=“http://www.springframework.org/tags” prefix=“spring” %>
<html>
<head>
<title>New comment</title>
</head>
<body>
<form method=“post” action=“newcomment.htm”>
<spring:bind path=“comment”>
<ul>
<c:forEach items=”${status.errorMessages}” var=“errorMessage”>
<li style=“color:red”><c:out value=”${errorMessage}”/></li>
</font>
</c:forEach> </ul> <table> <tr> <td><label>Email</label></td><td><input type=“text” name=“email” value=“<c:out value=”${comment.email}”/>”/></td> </tr> <tr> <td><label>Subject</label></td><td><input type=“text” name=“subject” value=“<c:out value=”${comment.subject}”/>”/></td> </tr> <tr> <td><label>Body</label></td><td><textarea name=“body” cols=“15” rows=“5”><c:out value=”${comment.body}”/></textarea></td> </tr> <tr> <td><label>Control text</label></td><td><input type=“text” name=“j_captcha_response” /></td> </tr> <tr> <td colspan=“2”><img src=“captcha.htm” /></td> </tr> <tr> <td colspan=“2” align=“center”><input type=“submit” value=“submit” /></td> </tr>
</table> </spring:bind> </form>
</body> </html>

Nový formulář

Nový formulář komentáře s polem CAPTCHA
Obrázek se nepodařilo načíst. Nový formulář komentáře s polem CAPTCHA

Plná velikost historického snímku (Nový formulář komentáře s polem CAPTCHA)

↗ Medium

Platné odeslání

Platné odeslání formuláře s CAPTCHA, první snímek
Obrázek se nepodařilo načíst. Platné odeslání formuláře s CAPTCHA, první snímek

Plná velikost historického snímku (Platné odeslání formuláře s CAPTCHA, první snímek)

↗ Medium
Platné odeslání formuláře s CAPTCHA, druhý snímek
Obrázek se nepodařilo načíst. Platné odeslání formuláře s CAPTCHA, druhý snímek
↗ Medium

Neplatné odeslání

Neplatné odeslání formuláře s CAPTCHA, první snímek
Obrázek se nepodařilo načíst. Neplatné odeslání formuláře s CAPTCHA, první snímek

Plná velikost historického snímku (Neplatné odeslání formuláře s CAPTCHA, první snímek)

↗ Medium
Neplatné odeslání formuláře s CAPTCHA, druhý snímek
Obrázek se nepodařilo načíst. Neplatné odeslání formuláře s CAPTCHA, druhý snímek

Plná velikost historického snímku (Neplatné odeslání formuláře s CAPTCHA, druhý snímek)

↗ Medium

Stáhněte si zdrojový kód (80 KB) nebo zdrojový kód a knihovny (3,2 MB).

Verze na Medium · Archiv původní stránky

Související články

Související epizody CZ Podcastu