Reputation: 109
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:
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="[email protected]">
</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>
Upvotes: 4
Views: 12493
Reputation: 31
I am not sure if it is correct way of doing but its works for me perfectly. I have created same classes as yours with few changes in the filter class code and a small change in security-context.xml.
public class CaptchaCaptureFilter extends OncePerRequestFilter {
private String captchaResponse;
private boolean iscaptchaPassed;
//setters and getters
@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,FilterChain chain) throws IOException, ServletException {
logger.info("Captcha capture filter");
String captcha_Response=req.getParameter("jcaptcha");
logger.info("response captcha captured : " +captcha_Response);
if(captcha_Response!=null)
{
iscaptchaPassed = SimpleImageCaptchaServlet.validateResponse(req, req.getParameter("jcaptcha"));
captchaResponse=captcha_Response;
logger.info("isCaptchaPassed value is "+iscaptchaPassed);
}
// Proceed with the remaining filters
chain.doFilter(req, res);
}
public class CaptchaVerifierFilter extends OncePerRequestFilter {
protected Logger logger = LoggerFactory.getLogger(Filter.class);
private CaptchaCaptureFilter captchaCaptureFilter;
private String failureUrl;
//getters and setters**strong text**
private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,FilterChain chain) throws IOException, ServletException {
//logger.info("Captcha verifier filter");
boolean captchaPassed=captchaCaptureFilter.getIscaptchaPassed();
String captchaResponse=captchaCaptureFilter.getCaptchaResponse();
//logger.info("captcha captured :"+captchaResponse+" validation result of captcha : " +captchaPassed);
if(captchaResponse!=null)
{
if(captchaPassed)
{
logger.info("Captcha is valid!");
}
else
{
logger.info("Captcha is invalid!");
failureHandler.setDefaultFailureUrl(failureUrl);
failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid!"));
}
resetCaptchaFields();
}
chain.doFilter(req, res);
}
/**
* Reset Captcha fields
*/
public void resetCaptchaFields() {
captchaCaptureFilter.setCaptchaResponse(null);
captchaCaptureFilter.setIscaptchaPassed(false);;
}
security:custom-filter ref="captchaCaptureFilter" before="FIRST"
security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"
I am adding captchaCaptureFilter before all the filters where captcha is validated. the result of validation is used after UserNameAndPasswordAuthFilter that is FORM_LOGIN_FILTER.
Upvotes: 1
Reputation: 1018
I tried one more method of verification of JCaptcha i.e. before the Spring Security Authentication. First, JCaptcha will get verified and if thats correct, control will go to Spring Security Authentication. I did not add CaptchaCaptureFilter also in security-context.xml.
Below is what I tried. It worked correctly.
public String login() {
this.captchaPassed = false;
// Check if captcha entered is correct. If yes then only proceed with
// Spring Security Authentication
this.captchaPassed = checkLoginCaptcha();
if (captchaPassed) {
boolean success = authenticationService.login(userName, password);
if (success) {
StringBuilder userNameBuilder = new StringBuilder();
userNameBuilder.append(userName);
FacesContext.getCurrentInstance().getExternalContext()
.getSessionMap()
.put("USER_NAME_PARAM", userNameBuilder.toString());
return ApplicationConstants.HOME_PAGE;
} else {
this.message = "Wrong Username or Password Entered. Please LOGIN again.";
this.userName = null;
this.password = null;
this.captchaString = null;
return ApplicationConstants.LOGIN_PAGE;
}
} else {
this.message = "Invalid captcha entered. Please LOGIN again.";
return ApplicationConstants.LOGIN_PAGE;
}
}
public boolean checkLoginCaptcha() {
HttpServletRequest req = (HttpServletRequest) FacesContext
.getCurrentInstance().getExternalContext().getRequest();
String str = null;
boolean flag = false;
try {
str = req.getParameter("loginForm:jcaptchaString");
if (str != null) {
flag = SimpleImageCaptchaServlet.validateResponse(req, str);
}
} catch (Exception e) {
e.printStackTrace();
flag = false;
}
return flag;
}
Upvotes: 0
Reputation: 109
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
<!--JCaptcha Filtering-->
<custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
<custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
become this one
<!--JCaptcha Filtering-->
<custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
remove
<!-- 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"/>
and validate the captcha in here
<beans:bean id="customAuthenticationProvider" class="com.pusilkom.artajasa.ecr.backend.util.MyAuthenticationProvider"
p:captchaCaptureFilter-ref="captchaCaptureFilter"/>
Upvotes: 3