Before start answering I know there is ReCaptcha which is simpler and easier, but I can’t use that. The production server is not online. So here we go.
I’m using Spring mvc 3 with spring security on maven Project and weblogic as the web server (jetty while developing). I’ll be very specific on this one.
Before seeing my configurations and files, I’d like to show you the list of my problems:
- I’ve tried ReCaptcha before JCaptcha with the same coding structure and it works fine.
- logger.debug doesn’t appear at all in CaptchaCaptureFilter class and/or CaptchaVerifierFilter class (while it appears in ArtajasaAuthenticationProvider class).
- I can see the captcha image, but whatever the answer is, it always invalid.
-
With the current state, it doesn’t work in jetty nor weblogic, but if I change the custom filter position to the one below, it works only in jetty.
<custom-filter ref="captchaCaptureFilter" position="FIRST"/> <custom-filter ref="captchaVerifierFilter" after="FIRST"/>
Thanks for viewing and many thanks for answering my question.
Below are the details.
The repository for JCaptcha is this one:
<repository>
<id>sourceforge-releases</id>
<name>Sourceforge Releases</name>
<url>https://oss.sonatype.org/content/repositories/sourceforge-releases</url>
</repository>
<dependency>
<groupId>com.octo.captcha</groupId>
<artifactId>jcaptcha-integration-simple-servlet</artifactId>
<version>2.0-alpha-1</version>
</dependency>
Here are some configuration I made in .xml files:
web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/spring/spring-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet>
<servlet-name>jcaptcha</servlet-name>
<servlet-class>com.octo.captcha.module.servlet.image.SimpleImageCaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>jcaptcha</servlet-name>
<url-pattern>/jcaptcha.jpg</url-pattern>
</servlet-mapping>
spring-security.xml
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/resources/**" access="permitAll()" />
<intercept-url pattern="/jcaptcha.jpg" access="permitAll()" />
<intercept-url pattern="/**" access="isAuthenticated()" />
<form-login login-page="/session/login/" default-target-url="/"
authentication-failure-url="/session/loginfailed/" />
<logout logout-success-url="/session/logout/" />
<access-denied-handler error-page="/session/403/" />
<!--JCaptcha Filtering-->
<custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
<custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
<anonymous />
</http>
<!-- For capturing CAPTCHA fields -->
<beans:bean id="captchaCaptureFilter" class="com.util.CaptchaCaptureFilter" />
<!-- For verifying CAPTCHA fields -->
<!-- Private key is assigned by the JCaptcha service -->
<beans:bean id="captchaVerifierFilter" class="com.util.CaptchaVerifierFilter"
p:failureUrl="/session/loginfailed/"
p:captchaCaptureFilter-ref="captchaCaptureFilter"/>
<beans:bean id="customAuthFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas"/>
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="allowSessionCreation" value="true" />
</beans:bean>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
<beans:property name="maximumSessions" value="1" />
</beans:bean>
<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
<beans:bean id="userService" class="com.service.mybatis.UserManager" />
<beans:bean id="customAuthenticationProvider" class="com.util.ArtajasaAuthenticationProvider" />
<authentication-manager alias="authenticationManager">
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>
<beans:bean id="accessDeniedHandler" class="com.util.ThouShaltNoPass">
<beans:property name="accessDeniedURL" value="/session/403/" />
</beans:bean>
And these are the java classes:
ArtajasaAuthenticationProvider.java
public class ArtajasaAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserService userService;
private Logger logger = LoggerFactory.getLogger(ArtajasaAuthenticationProvider.class);
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = String.valueOf(authentication.getPrincipal());
String password = String.valueOf(authentication.getCredentials());
logger.debug("Checking authentication for user {}", username);
if (StringUtils.isBlank(username)
|| StringUtils.isBlank(password)) {
throw new BadCredentialsException("No Username and/or Password Provided.");
} else {
Pengguna user = userService.select(username);
if (user == null) {
throw new BadCredentialsException("Invalid Username and/or Password.");
}
if (user.getPassword().equals(new PasswordUtil().generateHash(password, user.getSalt()))) {
List<GrantedAuthority> authorityList = (List<GrantedAuthority>) userService.getAuthorities(user);
return new UsernamePasswordAuthenticationToken(username, password, authorityList);
} else {
throw new BadCredentialsException("Invalid Username and/or Password.");
}
}
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
CaptchaCaptureFilter.java
public class CaptchaCaptureFilter extends OncePerRequestFilter {
protected Logger logger = Logger.getLogger(CaptchaCaptureFilter.class);
private String userCaptchaResponse;
private HttpServletRequest request;
@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("jcaptcha") != null) {
request = req;
userCaptchaResponse = req.getParameter("jcaptcha");
}
logger.debug("userResponse: " + userCaptchaResponse);
// Proceed with the remaining filters
chain.doFilter(req, res);
}
public String getUserCaptchaResponse() {
return userCaptchaResponse;
}
public void setUserCaptchaResponse(String userCaptchaResponse) {
this.userCaptchaResponse = userCaptchaResponse;
}
public HttpServletRequest getRequest() {
return request;
}
public void setRequest(HttpServletRequest request) {
this.request = request;
}
}
CaptchaVerifierFilter.java
public class CaptchaVerifierFilter extends OncePerRequestFilter {
protected Logger logger = Logger.getLogger(CaptchaVerifierFilter.class);
private String failureUrl;
private CaptchaCaptureFilter captchaCaptureFilter;
// 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("userResponse: " + captchaCaptureFilter.getUserCaptchaResponse());
// Assign values only when user has submitted a Captcha value
if (captchaCaptureFilter.getUserCaptchaResponse() != null) {
// Send HTTP request to validate user's Captcha
boolean captchaPassed = SimpleImageCaptchaServlet.validateResponse(captchaCaptureFilter.getRequest(), captchaCaptureFilter.getUserCaptchaResponse());
// Check if valid
if (!captchaPassed) {
logger.debug("Captcha is invalid!");
// Redirect user to login page
failureHandler.setDefaultFailureUrl(failureUrl);
failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid! " + captchaCaptureFilter.getRequest() + " " + captchaCaptureFilter.getUserCaptchaResponse()));
} 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.setUserCaptchaResponse(null);
}
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;
}
}
Last but not least, login.jsp
<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core_rt' %>
<form id="login" name="f" action="<c:url value='/j_spring_security_check'/>" method="POST">
<div class="container">
<div class="content">
<div class="row">
<div class="login-form">
<h3>Login</h3>
<br />
<fieldset>
<div class="clearfix">
username: ecr
<input type="text" name='j_username' value='<c:if test="${not empty param.login_error}"><c:out value="${SPRING_SECURITY_LAST_USERNAME}"/></c:if>' placeholder="username@artajasa.co.id">
</div>
<div class="clearfix">
password: ecr123
<input type="password" name='j_password' placeholder="password">
</div>
<div class="clearfix">
<img src="../../jcaptcha.jpg" />
<br />
<input type="text" name="jcaptcha" placeholder="masukkan captcha" />
</div>
<br />
<button class="btn btn-primary" type="submit"><i class="icon-lock"></i> Sign in</button>
</fieldset>
</div>
</div>
</div>
<br />
<c:if test="${not empty error}">
<div class="alert alert-error">
<button type="button" class="close" data-dismiss="alert"><i class="icon-remove"></i></button>
Login Failed, try again.<br />
<c:out value="${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}"/>
</div>
</c:if>
</div>
Problem SOLVED! I’ve found the answer. So we don’t need the CaptchaVerifierFilter after all. I validate the captcha inside AuthenticationProvider.
these are the list of changes:
in spring-security.xml, this one
become this one
remove
and validate the captcha in here