spring security4 to add CAPTCHA sample code

  • 2021-01-18 06:28:44
  • OfStack

spring security is a large module, and this article only covers the authentication of custom parameters. The default validation parameters for spring and security are username and password, which are generally not sufficient. Because time is too long, some forget, there may be a little omission. All right, no nonsense.
spring and spring security are configured with javaConfig, versions 4.2.5 and 4.0.4 respectively
General idea: Customize EntryPoint, add custom parameters to extend AuthenticationToken and AuthenticationProvider for validation.

First, define EntryPoint:


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
  public MyAuthenticationEntryPoint(String loginFormUrl) {
    super(loginFormUrl);
  }
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    super.commence(request, response, authException);
  }
}

Next, token, validCode is the CAPTCHA parameter:


import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public class MyUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
  private String validCode;
  public MyUsernamePasswordAuthenticationToken(String principal, String credentials, String validCode) {
    super(principal, credentials);
    this.validCode = validCode;
  }
  public String getValidCode() {
    return validCode;
  }
  public void setValidCode(String validCode) {
    this.validCode = validCode;
  }
}

Continue to ProcessingFilter,


import com.core.shared.ValidateCodeHandle;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
public class MyValidCodeProcessingFilter extends AbstractAuthenticationProcessingFilter {
  private String usernameParam = "username";
  private String passwordParam = "password";
  private String validCodeParam = "validateCode";
  public MyValidCodeProcessingFilter() {
    super(new AntPathRequestMatcher("/user/login", "POST"));
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    String username = request.getParameter(usernameParam);
    String password = request.getParameter(passwordParam);
    String validCode = request.getParameter(validCodeParam);
    valid(validCode, request.getSession());
    MyUsernamePasswordAuthenticationToken token = new MyUsernamePasswordAuthenticationToken(username, password, validCode);
    return this.getAuthenticationManager().authenticate(token);
  }

  public void valid(String validCode, HttpSession session) {
    if (validCode == null) {
      throw new ValidCodeErrorException(" The verification code is empty !");
    }
    if (!ValidateCodeHandle.matchCode(session.getId(), validCode)) {
      throw new ValidCodeErrorException(" Captcha error !");
    }
  }
}

3 parameters are defined respectively, used to receive login form parameters, the construction method gives url login and the need for post mode

The next step is authentication, not the username and password, but the CAPTCHA

Here are the ValidateCodeHandle1 utility classes and ValidCodeErrorException:


import java.util.concurrent.ConcurrentHashMap;
public class ValidateCodeHandle {
  private static ConcurrentHashMap validateCode = new ConcurrentHashMap<>();
  public static ConcurrentHashMap getCode() {
    return validateCode;
  }

  public static void save(String sessionId, String code) {
    validateCode.put(sessionId, code);
  }

  public static String getValidateCode(String sessionId) {
    Object obj = validateCode.get(sessionId);
    if (obj != null) {
      return String.valueOf(obj);
    }
    return null;
  }

  public static boolean matchCode(String sessionId, String inputCode) {
    String saveCode = getValidateCode(sessionId);
    if (saveCode.equals(inputCode)) {
      return true;
    }
    return false;
  }
}

There is a need to inherit AuthenticationException to indicate that it is an authentication failure of security, so that the subsequent failure process can be followed


import org.springframework.security.core.AuthenticationException;
public class ValidCodeErrorException extends AuthenticationException {

  public ValidCodeErrorException(String msg) {
    super(msg);
  }
  public ValidCodeErrorException(String msg, Throwable t) {
    super(msg, t);
  }
}

Next up is Provider:


import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
  @Override
  public boolean supports(Class<?> authentication) {
    return MyUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
  }

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    Object salt = null;
    if (getSaltSource() != null) {
      salt = getSaltSource().getSalt(userDetails);
    }
    if (authentication.getCredentials() == null) {
      logger.debug("Authentication failed: no credentials provided");
      throw new BadCredentialsException(" User name or password error !");
    }
    String presentedPassword = authentication.getCredentials().toString();
    if (!this.getPasswordEncoder().isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
      logger.debug("Authentication failed: password does not match stored value");

      throw new BadCredentialsException(" User name or password error !");
    }

  }
}

Where the supports method specifies the logic of using custom token, additionalAuthenticationChecks method and superclass, I just changed the information returned by the exception.

Next up is handler, which handles authentication success and failure


import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class FrontAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
  public FrontAuthenticationSuccessHandler(String defaultTargetUrl) {
    super(defaultTargetUrl);
  }

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    super.onAuthenticationSuccess(request, response, authentication);
  }
}

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class FrontAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  public FrontAuthenticationFailureHandler(String defaultFailureUrl) {
    super(defaultFailureUrl);
  }

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    super.onAuthenticationFailure(request, response, exception);
  }
}

And finally, the most important security config:


import com.service.user.CustomerService;
import com.web.filter.SiteMeshFilter;
import com.web.mySecurity.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends AbstractSecurityWebApplicationInitializer {

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new StandardPasswordEncoder("MD5");
  }

  @Autowired
  private CustomerService customerService;

  @Configuration
  @Order(1)
  public static class FrontendWebSecurityConfigureAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyValidCodeProcessingFilter myValidCodeProcessingFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http.csrf().disable() 
          .authorizeRequests()
          .antMatchers("/user/login", "/user/logout").permitAll()
          .anyRequest().authenticated()
          .and()
          .addFilterBefore(myValidCodeProcessingFilter, UsernamePasswordAuthenticationFilter.class)
          .formLogin()
          .loginPage("/user/login")
          .and()
          .logout()
          .logoutUrl("/user/logout")
          .logoutSuccessUrl("/user/login");
    }

  }

  @Bean(name = "frontAuthenticationProvider")
  public MyAuthenticationProvider frontAuthenticationProvider() {
    MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider();
    myAuthenticationProvider.setUserDetailsService(customerService);
    myAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    return myAuthenticationProvider;
  }

  @Bean
  public AuthenticationManager authenticationManager() {
    List<AuthenticationProvider> list = new ArrayList<>();
    list.add(frontAuthenticationProvider());
    AuthenticationManager authenticationManager = new ProviderManager(list);
    return authenticationManager;
  }

  @Bean
  public MyValidCodeProcessingFilter myValidCodeProcessingFilter(AuthenticationManager authenticationManager) {
    MyValidCodeProcessingFilter filter = new MyValidCodeProcessingFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setAuthenticationSuccessHandler(frontAuthenticationSuccessHandler());
    filter.setAuthenticationFailureHandler(frontAuthenticationFailureHandler());
    return filter;
  }

  @Bean
  public FrontAuthenticationFailureHandler frontAuthenticationFailureHandler() {
    return new FrontAuthenticationFailureHandler("/user/login");
  }

  @Bean
  public FrontAuthenticationSuccessHandler frontAuthenticationSuccessHandler() {
    return new FrontAuthenticationSuccessHandler("/front/test");
  }

  @Bean
  public MyAuthenticationEntryPoint myAuthenticationEntryPoint() {
    return new MyAuthenticationEntryPoint("/user/login");
  }
}

The first is bean for an encryption class, and customerService is a simple query user


@Service("customerService")
public class CustomerServiceImpl implements CustomerService {

  @Autowired
  private UserDao userDao;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return userDao.findCustomerByUsername(username);
  }
}

FrontendWebSecurityConfigureAdapter () : disable csrf, enable authorization request authorizeRequests(), where "/user/login", "/user/logout" pass permission verification, other requests need to login authentication, then addFilterBefore(), add my custom myValidCodeProcessingFilter to security default UsernamePasswordAuthenticationFilter before, that is, my custom parameter authentication, then formLogin(), To configure url, url, url, controller mapping is required, that is, to write controller by yourself.
Next is the bean display statement for AuthenticationProvider, AuthenticationManager, ProcessingFilter, AuthenticationFailureHandler, AuthenticationSuccessHandler, EntryPoint.

Below is the login jsp


import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public class MyUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
  private String validCode;
  public MyUsernamePasswordAuthenticationToken(String principal, String credentials, String validCode) {
    super(principal, credentials);
    this.validCode = validCode;
  }
  public String getValidCode() {
    return validCode;
  }
  public void setValidCode(String validCode) {
    this.validCode = validCode;
  }
}

0

login.jsp {SPRING_SECURITY_LAST_EXCEPTION.message} {SPRING_SECURITY_LAST_EXCEPTION.message} {SPRING_SECURITY_LAST_EXCEPTION.message} {SPRING_SECURITY_EXCEPTION.message} {SPRING_SECURITY_EXCEPTION.message}} {SPRING_SECURITY_EXCEPTION.message}} {SPRING_SECURITY_EXCEPTION.message}}} {SPRING_SECURITY_EXCEPTION.message}}};

The entire custom security validation process is complete


Related articles: