├── .gitignore ├── lib ├── simplecaptcha-1.2.jar └── simplecaptcha-1.2-sources.jar ├── src └── main │ ├── resources │ ├── logback.xml │ └── spring │ │ ├── mvc-config.xml │ │ └── security-config.xml │ ├── java │ └── com │ │ └── example │ │ ├── security │ │ └── captcha │ │ │ ├── CaptchaAuthenticationDetailsSource.java │ │ │ ├── CaptchaUtils.java │ │ │ ├── CaptchaAuthenticationDetails.java │ │ │ ├── CaptchaDaoAuthenticationProvider.java │ │ │ └── CaptchaGenerator.java │ │ └── web │ │ ├── AuthenticationResultListener.java │ │ └── LoginController.java │ └── webapp │ ├── WEB-INF │ ├── jsp │ │ ├── secure.jsp │ │ └── login.jsp │ └── web.xml │ └── index.jsp ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | -------------------------------------------------------------------------------- /lib/simplecaptcha-1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaljemala/spring-captcha/HEAD/lib/simplecaptcha-1.2.jar -------------------------------------------------------------------------------- /lib/simplecaptcha-1.2-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaljemala/spring-captcha/HEAD/lib/simplecaptcha-1.2-sources.jar -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/example/security/captcha/CaptchaAuthenticationDetailsSource.java: -------------------------------------------------------------------------------- 1 | package com.example.security.captcha; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.security.authentication.AuthenticationDetailsSource; 6 | 7 | public class CaptchaAuthenticationDetailsSource implements 8 | AuthenticationDetailsSource { 9 | 10 | @Override 11 | public CaptchaAuthenticationDetails buildDetails(HttpServletRequest context) { 12 | return new CaptchaAuthenticationDetails(context); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spring Captcha 2 | ============== 3 | 4 | Spring Security SimpleCaptcha integration sample. 5 | 6 | It demonstrates a way how to introduce a CAPTCHA verification into the form logins. After 3 unsuccessful login attempts a CAPTCHA will required to be provided. 7 | 8 | To generate CAPTCHAs the [SimpleCaptcha library](http://simplecaptcha.sourceforge.net/) is used. 9 | 10 | How to install SimpleCaptcha to your local Maven repo 11 | ----------------------------------------------------- 12 | ```shell 13 | mvn install:install-file -Dfile=./lib/simplecaptcha-1.2.jar -DgroupId=nl.captcha -DartifactId=simplecaptcha -Dversion=1.2 -Dpackaging=jar 14 | ``` 15 | -------------------------------------------------------------------------------- /src/main/java/com/example/security/captcha/CaptchaUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.security.captcha; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | 6 | import javax.imageio.ImageIO; 7 | import javax.xml.bind.DatatypeConverter; 8 | 9 | import nl.captcha.Captcha; 10 | 11 | public abstract class CaptchaUtils { 12 | 13 | public static String encodeBase64(Captcha captcha) { 14 | try { 15 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 16 | ImageIO.write(captcha.getImage(), "png", bos); 17 | return DatatypeConverter.printBase64Binary(bos.toByteArray()); 18 | } catch (IOException e) { 19 | return ""; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/secure.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> 2 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | Secured Page 10 | 11 | 12 |
13 |

Secure Page

14 |

This is a protected page and it can be viewed only after successfully autheticated.

15 |

Go back

16 |

Logout

17 |
18 | 19 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page session="false" %> 2 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 3 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 4 | <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> 5 | 6 | 7 | 8 | 9 | Home Page 10 | 11 | 12 |
13 |

Home Page

14 |

Anyone can view this page.

15 | <% if (request.getUserPrincipal() != null) { %> 16 |

Your are logged as: <%= request.getUserPrincipal().getName() %>

17 | <% } %> 18 | 19 |

You can currently access "/secure" URLs.

20 |
21 |

Go to a secured page (bob/secret)

22 |
23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/example/security/captcha/CaptchaAuthenticationDetails.java: -------------------------------------------------------------------------------- 1 | package com.example.security.captcha; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | import nl.captcha.Captcha; 8 | 9 | import org.springframework.web.util.WebUtils; 10 | 11 | public class CaptchaAuthenticationDetails implements Serializable { 12 | 13 | private static final long serialVersionUID = 8047091036777813803L; 14 | 15 | private final String answer; 16 | private final Captcha captcha; 17 | 18 | public CaptchaAuthenticationDetails(HttpServletRequest req) { 19 | this.answer = req.getParameter("j_captcha"); 20 | this.captcha = (Captcha) WebUtils.getSessionAttribute(req, "captcha"); 21 | } 22 | 23 | public String getAnswer() { 24 | return answer; 25 | } 26 | 27 | public Captcha getCaptcha() { 28 | return captcha; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/example/web/AuthenticationResultListener.java: -------------------------------------------------------------------------------- 1 | package com.example.web; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import javax.servlet.http.HttpSession; 6 | 7 | import org.springframework.context.ApplicationListener; 8 | import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; 9 | import org.springframework.web.context.request.RequestContextHolder; 10 | import org.springframework.web.context.request.ServletRequestAttributes; 11 | 12 | public class AuthenticationResultListener implements 13 | ApplicationListener { 14 | 15 | @Override 16 | public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { 17 | HttpSession session = getSession(); 18 | AtomicInteger counter = (AtomicInteger) session.getAttribute("counter"); 19 | counter.incrementAndGet(); 20 | } 21 | 22 | private HttpSession getSession() { 23 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 24 | return attributes.getRequest().getSession(false); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/spring/mvc-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 2 | 3 | 4 | 5 | 6 | Login Page 7 | 8 | 9 |

Login

10 | 11 |

12 | Login attempt was not successful: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} 13 |

14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
User:
Password:
Captcha: 30 |
31 | 32 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | contextConfigLocation 8 | classpath:spring/security-config.xml 9 | 10 | 11 | 12 | org.springframework.web.context.ContextLoaderListener 13 | 14 | 15 | org.springframework.web.context.request.RequestContextListener 16 | 17 | 18 | 19 | springSecurityFilterChain 20 | org.springframework.web.filter.DelegatingFilterProxy 21 | 22 | 23 | springSecurityFilterChain 24 | /* 25 | 26 | 27 | 28 | captcha 29 | org.springframework.web.servlet.DispatcherServlet 30 | 31 | contextConfigLocation 32 | classpath:spring/mvc-config.xml 33 | 34 | 1 35 | 36 | 37 | captcha 38 | / 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/com/example/security/captcha/CaptchaDaoAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.example.security.captcha; 2 | 3 | import nl.captcha.Captcha; 4 | 5 | import org.springframework.security.authentication.BadCredentialsException; 6 | import org.springframework.security.authentication.InsufficientAuthenticationException; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | 12 | public class CaptchaDaoAuthenticationProvider extends DaoAuthenticationProvider { 13 | 14 | @Override 15 | protected void additionalAuthenticationChecks(UserDetails userDetails, 16 | UsernamePasswordAuthenticationToken token) 17 | throws AuthenticationException { 18 | super.additionalAuthenticationChecks(userDetails, token); 19 | 20 | Object obj = token.getDetails(); 21 | if (!(obj instanceof CaptchaAuthenticationDetails)) { 22 | throw new InsufficientAuthenticationException( 23 | "Captcha details not found."); 24 | } 25 | 26 | CaptchaAuthenticationDetails captchaDetails = (CaptchaAuthenticationDetails) obj; 27 | Captcha captcha = captchaDetails.getCaptcha(); 28 | if (captcha != null) { 29 | String expected = captcha.getAnswer(); 30 | String actual = captchaDetails.getAnswer(); 31 | if (!expected.equals(actual)) { 32 | throw new BadCredentialsException("Captcha does not match."); 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/web/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.example.web; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import javax.servlet.http.HttpSession; 6 | 7 | import nl.captcha.Captcha; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.ModelMap; 12 | import org.springframework.web.bind.annotation.ModelAttribute; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.SessionAttributes; 16 | 17 | import com.example.security.captcha.CaptchaGenerator; 18 | import com.example.security.captcha.CaptchaUtils; 19 | 20 | @Controller 21 | @SessionAttributes("counter") 22 | public class LoginController { 23 | 24 | @Autowired 25 | private CaptchaGenerator captchaGenerator; 26 | 27 | private static final int MAX_NOCAPTCHA_TRIES = 3; 28 | 29 | // @ModelAttribute("captcha") 30 | // public Captcha captcha() { 31 | // return captchaGenerator.createCaptcha(200, 50); 32 | // } 33 | 34 | @ModelAttribute("counter") 35 | public AtomicInteger failureCounter() { 36 | return new AtomicInteger(0); 37 | } 38 | 39 | @RequestMapping(value = "/login", method = RequestMethod.GET) 40 | public String login(ModelMap model, HttpSession session) { 41 | AtomicInteger counter = (AtomicInteger) model.get("counter"); 42 | if (counter.intValue() >= MAX_NOCAPTCHA_TRIES) { 43 | Captcha captcha = captchaGenerator.createCaptcha(200, 50); 44 | session.setAttribute("captcha", captcha); 45 | model.addAttribute("captchaEnc", CaptchaUtils.encodeBase64(captcha)); 46 | } 47 | return "login"; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/resources/spring/security-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/security/captcha/CaptchaGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.security.captcha; 2 | 3 | import nl.captcha.Captcha; 4 | import nl.captcha.backgrounds.BackgroundProducer; 5 | import nl.captcha.backgrounds.TransparentBackgroundProducer; 6 | import nl.captcha.gimpy.GimpyRenderer; 7 | import nl.captcha.gimpy.RippleGimpyRenderer; 8 | import nl.captcha.noise.CurvedLineNoiseProducer; 9 | import nl.captcha.noise.NoiseProducer; 10 | import nl.captcha.text.producer.DefaultTextProducer; 11 | import nl.captcha.text.producer.TextProducer; 12 | import nl.captcha.text.renderer.DefaultWordRenderer; 13 | import nl.captcha.text.renderer.WordRenderer; 14 | 15 | import org.springframework.beans.factory.InitializingBean; 16 | 17 | public class CaptchaGenerator implements InitializingBean { 18 | 19 | private BackgroundProducer backgroundProducer; 20 | private TextProducer textProducer; 21 | private WordRenderer wordRenderer; 22 | private NoiseProducer noiseProducer; 23 | private GimpyRenderer gimpyRenderer; 24 | boolean isBorder; 25 | 26 | public Captcha createCaptcha(int width, int height) { 27 | Captcha.Builder builder = new Captcha.Builder(width, height) 28 | .addBackground(backgroundProducer) 29 | .addText(textProducer, wordRenderer) 30 | .addNoise(noiseProducer); 31 | 32 | if (isBorder) { 33 | builder.addBorder(); 34 | } 35 | 36 | return builder.build(); 37 | } 38 | 39 | public void setBackgroundProducer(BackgroundProducer backgroundProducer) { 40 | this.backgroundProducer = backgroundProducer; 41 | } 42 | 43 | public void setTextProducer(TextProducer textProducer) { 44 | this.textProducer = textProducer; 45 | } 46 | 47 | public void setWordRenderer(WordRenderer wordRenderer) { 48 | this.wordRenderer = wordRenderer; 49 | } 50 | 51 | public void setNoiseProducer(NoiseProducer noiseProducer) { 52 | this.noiseProducer = noiseProducer; 53 | } 54 | 55 | public void setGimpyRenderer(GimpyRenderer gimpyRenderer) { 56 | this.gimpyRenderer = gimpyRenderer; 57 | } 58 | 59 | public void setBorder(boolean isBorder) { 60 | this.isBorder = isBorder; 61 | } 62 | 63 | @Override 64 | public void afterPropertiesSet() throws Exception { 65 | if (this.backgroundProducer == null) { 66 | this.backgroundProducer = new TransparentBackgroundProducer(); 67 | } 68 | 69 | if (this.textProducer == null) { 70 | this.textProducer = new DefaultTextProducer(); 71 | } 72 | 73 | if (this.wordRenderer == null) { 74 | this.wordRenderer = new DefaultWordRenderer(); 75 | } 76 | 77 | if (this.noiseProducer == null) { 78 | this.noiseProducer = new CurvedLineNoiseProducer(); 79 | } 80 | 81 | if (this.gimpyRenderer == null) { 82 | this.gimpyRenderer = new RippleGimpyRenderer(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | com.example 6 | captcha 7 | war 8 | 1.0.0 9 | Captcha 10 | 11 | 12 | 3.2.6.RELEASE 13 | 3.2.0.RELEASE 14 | 2.5 15 | 1.2 16 | 1.1.0 17 | 1.7.5 18 | 19 | 20 | 21 | 22 | org.springframework 23 | spring-webmvc 24 | ${spring-framework.version} 25 | 26 | 27 | org.springframework.security 28 | spring-security-web 29 | ${spring-security.version} 30 | 31 | 32 | org.springframework.security 33 | spring-security-config 34 | ${spring-security.version} 35 | 36 | 37 | org.springframework.security 38 | spring-security-taglibs 39 | ${spring-security.version} 40 | 41 | 42 | nl.captcha 43 | simplecaptcha 44 | 1.2 45 | 46 | 47 | javax.servlet 48 | servlet-api 49 | ${servlet-api.version} 50 | provided 51 | 52 | 53 | javax.servlet 54 | jstl 55 | ${jstl.version} 56 | runtime 57 | 58 | 59 | org.slf4j 60 | slf4j-api 61 | ${slf4j.version} 62 | 63 | 64 | org.slf4j 65 | jcl-over-slf4j 66 | ${slf4j.version} 67 | runtime 68 | 69 | 70 | ch.qos.logback 71 | logback-classic 72 | ${logback.version} 73 | runtime 74 | 75 | 76 | 77 | 78 | 79 | io.spring.repo.maven.release 80 | http://repo.spring.io/release/ 81 | 82 | false 83 | 84 | 85 | 86 | 87 | 88 | captcha 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-compiler-plugin 93 | 3.1 94 | 95 | 1.7 96 | 1.7 97 | 98 | 99 | 100 | 101 | 102 | --------------------------------------------------------------------------------