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><!-- This controller generates CAPTCHA image --> <bean id="captchaController" class="cz.sweb.pichlik.springtutorial.captcha.JCaptchaController"> <property name="captchaService"><ref bean="captchaService"/></property> </bean><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ář

Platné odeslání


Neplatné odeslání


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
- #3 · Java 6 SE, Vývoj webových aplikací
Dagi a Filemon o novinkách v Javě 6 a webových frameworcích.
- #1 · Vývojová prostředí v Javě
První díl CZ Podcastu se věnuje vývojovým prostředím pro Javu.