Introduction
In this tutorial we'll integrate Spring Security 3 with the popular reCAPTCHA service. For the Spring Security module we'll based the application from the one we built for Spring Security 3 - MVC: Using a Simple User-Service Tutorial. For the reCAPTCHA module we'll based the implementation from the Using reCAPTCHA with Java/JSP article. Our design goal here is to integrate the reCAPTCHA service unobtrusively. To realize that we'll be relying on Spring Security filters.Here's what we will do:
1. Declare two CAPTCHA filters
2. Modify a JSP file to include reCAPTCHA login
3. Add two filters in the spring-security.xml configuration
Requirements:
1. A basic understanding of how to setup a simple Spring Security 3 application (See here)
2. A basic understanding of how to setup reCAPTCHA with JSP (See here)
3. An account with the reCAPTCHA service (See here). For the account, make sure you assigned the host that matches your web application. For example http://localhost.
What is reCAPTCHA?
reCAPTCHA is a free CAPTCHA service that helps to digitize books, newspapers and old time radio shows....Here's a screenshot of the reCAPTCHA service:
A CAPTCHA is a program that can tell whether its user is a human or a computer. You've probably seen them รณ colorful images with distorted text at the bottom of Web registration forms. CAPTCHAs are used by many websites to prevent abuse from "bots," or automated programs usually written to generate spam. No computer program can read distorted text as well as humans can, so bots cannot navigate sites protected by CAPTCHAs.
About 200 million CAPTCHAs are solved by humans around the world every day. In each case, roughly ten seconds of human time are being spent. Individually, that's not a lot of time, but in aggregate these little puzzles consume more than 150,000 hours of work each day. What if we could make positive use of this human effort? reCAPTCHA does exactly that by channeling the effort spent solving CAPTCHAs online into "reading" books.
Source: http://www.google.com/recaptcha/learnmore
Architecture
Scenario 1: Spring SecurityA form-based login has two input fields: username and password. To authenticate a user, the user must enter valid information. After submitting the values, the web application will query the database if the submitted values are present. The web application performs the validation.
Scenario 2: reCAPTCHA
A reCAPTCHA form has one input field. To authenticate a user, the user must match the text images shown on the form. After submitting the values, the web application will send a POST request to the reCAPTCHA service. The service performs the validation and returns the result as a booblean value. The web application can use this result to determine if the user should be allowed access or not.
Spring Security and reCAPTCHA
With Spring Security and reCAPTCHA the same logic still applies. To authenticate a user, the user must enter his username, password, and the two words that matches the CAPTCHA text images.
Here's a screenshot of our new login page:
After submitting the values, here's what should happen:
1. A custom filter stores the entered values from the CAPTCHA form for later use. (Scenario 2)
2. The web application queries the database to check if the entered username and password exist. If invalid, deny access. If valid, continue with the remaining filters. (Scenario 1)
3. A second custom filter retrieves the stored values from the CAPTCHA form and sends a POST request to the CAPTCHA service. (Scenario 2)
Development
The Filters
Our application has two custom filters:1. CAPTCHA Capture filter 2. CAPTCHA Verifier filterThe purpose of the Capture filter is to store the information entered by the user in the CAPTCHA form. Whereas the Verifier filter's purpose is to send a POST request to the CAPTCHA service and wait for the result. If the result is valid, allow the user to proceed; otherwise, it will show the login page again.
We'll place these two filters in-between the FORM_LOGIN_FILTER alias which is the one responsible for the form-based login for Spring Security. Why do we need to place these two filters in-between this alias?
<security:http auto-config="true" > ... <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/> <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/> </security:http>
Issue 1:
Placing the capture and verifier filters after the FORM_LOGIN_FILTER resets the requests parameters which means they will always be null. The verifier has nothing to verify resulting to denied access!
Issue 2:
Placing the capture and verifier filter before the alias works but you won't be able to redirect the user to the login page if in case the user has entered an invalid information. You can throw an Exception like BadCredentialsException but the exception will be shown on the web page! Again, you can't redirect to a JSP page because the response stream has been already written.
Here's the capture filter:
CaptchaCaptureFilter.java
package org.krams.tutorial.filter; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.springframework.web.filter.OncePerRequestFilter; /** * Filter for capturing Captcha fields. * It's purpose is to store these values internally */ public class CaptchaCaptureFilter extends OncePerRequestFilter { protected Logger logger = Logger.getLogger("filter"); private String recaptcha_response; private String recaptcha_challenge; private String remoteAddr; @Override public void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.debug("Captcha capture filter"); // Assign values only when user has submitted a Captcha value. // Without this condition the values will be reset due to redirection // and CaptchaVerifierFilter will enter an infinite loop if (req.getParameter("recaptcha_response_field") != null) { recaptcha_response = req.getParameter("recaptcha_response_field"); recaptcha_challenge = req.getParameter("recaptcha_challenge_field"); remoteAddr = req.getRemoteAddr(); } logger.debug("challenge: " + recaptcha_challenge); logger.debug("response: " + recaptcha_response); logger.debug("remoteAddr: " + remoteAddr); // Proceed with the remaining filters chain.doFilter(req, res); } public String getRecaptcha_response() { return recaptcha_response; } public void setRecaptcha_response(String recaptchaResponse) { recaptcha_response = recaptchaResponse; } public String getRecaptcha_challenge() { return recaptcha_challenge; } public void setRecaptcha_challenge(String recaptchaChallenge) { recaptcha_challenge = recaptchaChallenge; } public String getRemoteAddr() { return remoteAddr; } public void setRemoteAddr(String remoteAddr) { this.remoteAddr = remoteAddr; } }
Here's the verifier filter:
CaptchaVerifierFilter.java
package org.krams.tutorial.filter; import java.io.IOException; import java.util.Properties; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.tanesha.recaptcha.ReCaptchaImpl; import net.tanesha.recaptcha.ReCaptchaResponse; import org.apache.log4j.Logger; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.web.filter.OncePerRequestFilter; /** * Filter for verifying if the submitted Captcha fields * are valid. * <p> * This filter also allows you to set a proxy if needed */ public class CaptchaVerifierFilter extends OncePerRequestFilter { protected Logger logger = Logger.getLogger("filter"); private Boolean useProxy = false; private String proxyPort; private String proxyHost; private String failureUrl; private CaptchaCaptureFilter captchaCaptureFilter; private String privateKey; // Inspired by log output: AbstractAuthenticationProcessingFilter.java:unsuccessfulAuthentication:320) // Delegating to authentication failure handlerorg.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@15d4273 private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); @Override public void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.debug("Captcha verifier filter"); logger.debug("challenge: " + captchaCaptureFilter.getRecaptcha_challenge()); logger.debug("response: " + captchaCaptureFilter.getRecaptcha_response()); logger.debug("remoteAddr: " + captchaCaptureFilter.getRemoteAddr()); // Assign values only when user has submitted a Captcha value if (captchaCaptureFilter.getRecaptcha_response() != null) { // Create a new recaptcha (by Soren Davidsen) ReCaptchaImpl reCaptcha = new ReCaptchaImpl(); // Set the private key (assigned by Google) reCaptcha.setPrivateKey(privateKey); // Assign proxy if needed if (useProxy) { Properties systemSettings = System.getProperties(); systemSettings.put("http.proxyPort",proxyPort); systemSettings.put("http.proxyHost",proxyHost); } // Send HTTP request to validate user's Captcha ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(captchaCaptureFilter.getRemoteAddr(), captchaCaptureFilter.getRecaptcha_challenge(), captchaCaptureFilter.getRecaptcha_response()); // Check if valid if (!reCaptchaResponse.isValid()) { logger.debug("Captcha is invalid!"); // Redirect user to login page failureHandler.setDefaultFailureUrl(failureUrl); failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid!")); } else { logger.debug("Captcha is valid!"); } // Reset Captcha fields after processing // If this method is skipped, everytime we access a page // CaptchaVerifierFilter will infinitely send a request to the Google Captcha service! resetCaptchaFields(); } // Proceed with the remaining filters chain.doFilter(req, res); } /** * Reset Captcha fields */ public void resetCaptchaFields() { captchaCaptureFilter.setRemoteAddr(null); captchaCaptureFilter.setRecaptcha_challenge(null); captchaCaptureFilter.setRecaptcha_response(null); } public Boolean getUseProxy() { return useProxy; } public void setUseProxy(Boolean useProxy) { this.useProxy = useProxy; } public String getProxyPort() { return proxyPort; } public void setProxyPort(String proxyPort) { this.proxyPort = proxyPort; } public String getProxyHost() { return proxyHost; } public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; } public String getFailureUrl() { return failureUrl; } public void setFailureUrl(String failureUrl) { this.failureUrl = failureUrl; } public CaptchaCaptureFilter getCaptchaCaptureFilter() { return captchaCaptureFilter; } public void setCaptchaCaptureFilter(CaptchaCaptureFilter captchaCaptureFilter) { this.captchaCaptureFilter = captchaCaptureFilter; } public String getPrivateKey() { return privateKey; } public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } }
Configuration
We've already seen earlier the required configuration to activate the CAPTCHA filters. Here's the full spring-security.xml configuration file:spring-security.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <security:http auto-config="true" use-expressions="true" access-denied-page="/krams/auth/denied" > <security:intercept-url pattern="/krams/auth/login" access="permitAll"/> <security:intercept-url pattern="/krams/main/admin" access="hasRole('ROLE_ADMIN')"/> <security:intercept-url pattern="/krams/main/common" access="hasRole('ROLE_USER')"/> <security:form-login login-page="/krams/auth/login" authentication-failure-url="/krams/auth/login?error=true" default-target-url="/krams/main/common"/> <security:logout invalidate-session="true" logout-success-url="/krams/auth/login" logout-url="/krams/auth/logout"/> <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/> <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/> </security:http> <!-- For capturing CAPTCHA fields --> <bean id="captchaCaptureFilter" class="org.krams.tutorial.filter.CaptchaCaptureFilter" /> <!-- For verifying CAPTCHA fields --> <!-- Private key is assigned by the reCATPCHA service --> <bean id="captchaVerifierFilter" class="org.krams.tutorial.filter.CaptchaVerifierFilter" p:useProxy="false" p:proxyPort="" p:proxyHost="" p:failureUrl="/krams/auth/login?error=true" p:captchaCaptureFilter-ref="captchaCaptureFilter" p:privateKey="ADD-YOUR-PRIVATE-KEY-HERE"/> <!-- Declare an authentication-manager to use a custom userDetailsService --> <security:authentication-manager> <security:authentication-provider user-service-ref="userDetailsService"> <security:password-encoder ref="passwordEncoder"/> </security:authentication-provider> </security:authentication-manager> <!-- Use a Md5 encoder since the user's passwords are stored as Md5 in the database --> <bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/> <!-- An in-memory list of users. No need to access an external database layer. See Spring Security 3.1 Reference 5.2.1 In-Memory Authentication --> <!-- john's password is admin, while jane;s password is user --> <security:user-service id="userDetailsService"> <security:user name="john" password="21232f297a57a5a743894a0e4a801fc3" authorities="ROLE_USER, ROLE_ADMIN" /> <security:user name="jane" password="ee11cbb19052e40b07aac0ca060c23ee" authorities="ROLE_USER" /> </security:user-service> </beans>Notice the only changes we made here is add the two filters:
<security:http auto-config="true" > ... <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/> <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/> </security:http>
And declare the beans:
<!-- For capturing CAPTCHA fields --> <bean id="captchaCaptureFilter" class="org.krams.tutorial.filter.CaptchaCaptureFilter" /> <!-- For verifying CAPTCHA fields --> <!-- Private key is assigned by the reCATPCHA service --> <bean id="captchaVerifierFilter" class="org.krams.tutorial.filter.CaptchaVerifierFilter" p:useProxy="false" p:proxyPort="" p:proxyHost="" p:failureUrl="/krams/auth/login?error=true" p:captchaCaptureFilter-ref="captchaCaptureFilter" p:privateKey="ADD-YOUR-PRIVATE-KEY-HERE"/>The verifier filter is configurable. You can enable proxy if needed and assign a failure URL which is usually the same URL declared in your form-login tag. Don't forget to add your private key which is provided freely by reCAPTCHA!
The Login Page
Our last task is modify the login page so that the CAPTCHA form is shown along with the username and password fields. To implement the login page we just mix directly the contents from the original loginpage.jsp and the one from http://code.google.com/apis/recaptcha/docs/java.htmlHere's the login page:
loginpage.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> <%@ page import="net.tanesha.recaptcha.ReCaptcha" %> <%@ page import="net.tanesha.recaptcha.ReCaptchaFactory" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h1>Login</h1> <div id="login-error">${error}</div> <c:url value="/j_spring_security_check" var="secureUrl"/> <form action="${secureUrl}" method="post" > <% ReCaptcha c = ReCaptchaFactory.newReCaptcha("ADD-YOUR-PUBLIC-KEY-HERE", "ADD-YOUR-PRIVATE-KEY-HERE", false); out.print(c.createRecaptchaHtml(null, null)); %> <p> <label for="j_username">Username</label> <input id="j_username" name="j_username" type="text" /> </p> <p> <label for="j_password">Password</label> <input id="j_password" name="j_password" type="password" /> </p> <input type="submit" value="Login"/> </form> </body> </html>The main changes here are the addition of the ReCaptcha:
<% ReCaptcha c = ReCaptchaFactory.newReCaptcha("ADD-YOUR-PUBLIC-KEY-HERE", "ADD-YOUR-PRIVATE-KEY-HERE", false); out.print(c.createRecaptchaHtml(null, null)); %>Upon running the application, this will automatically build and show the CAPTCHA form.
Run the Application
To run the application, use the following URL:http://localhost:8080/spring-security-recaptcha/or to go directly to the login page:
http://localhost:8080/spring-security-recaptcha/krams/auth/login
Reminders:
1. Make sure you've signed-up for a reCAPTCHA account!
2. Don't forget to add your Public and Private keys in the JSP page
3. Don't forget to add the Private key in the spring-security.xml
4. Enable the proxy setting if you have one
The application has two built-in users:
username: john / password: admin username: jane / password: user
Conclusion
That's it. We've successfully integrated reCAPTCHA with an existing Spring Security application without changing any of the classes. We've modified our configuration by just adding two simple custom filters. We've also successfully followed the guidelines presented in Using reCAPTCHA with Java/JSP article.Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-security-recaptcha/
You can download the project as a Maven build. Look for the spring-security-recaptcha.zip in the Download sections.
You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run
If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
Share the joy:
|
Subscribe by reader Subscribe by email Share