Custom image verification and verification code verification

Spring Security principle

Green: Check whether the request contains this information

Blue: Exception handling

Orange: Determines whether the request can access the service

Custom Login

The original Spring Security login method is not applicable in a project with a separate front and back end, so we need to customize the login method.

Custom validates successful processor

@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private UserMapper userMapper;


    @SneakyThrows
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        
        // After a successful login, you may need to return to the foreground what menu permissions the current user has, issue tokens, change database data, etc.
        // Then the foreground dynamic control menu display, specific according to their own business needs to expand
        // Return json data
        CommonReturnType result = CommonReturnType.success("Login successful");
        httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code

Custom validation failure handler

@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // Return json data
        CommonReturnType result = null;
        if (e instanceof AccountExpiredException) {
            // The account has expired
            result = CommonReturnType.fail("Account expired");
        } else if (e instanceof InternalAuthenticationServiceException) {
            // The password is incorrect
            result = CommonReturnType.fail("User does not exist");
        } else if(e instanceof BadCredentialsException) {
            // User does not exist
            result = CommonReturnType.fail(e.getMessage());
        } else if (e instanceof CredentialsExpiredException) {
            // Password expired
            result = CommonReturnType.fail("Password expired");
        } else if (e instanceof DisabledException) {
            // The account is unavailable
            result = CommonReturnType.fail("Account is disabled.");
        } else if (e instanceof LockedException) {
            // The account is locked
            result = CommonReturnType.fail("Account lock");
        } else if(e instanceof NonceExpiredException) {
            // Remote login
            result = CommonReturnType.fail("Remote Login");
        } else if(e instanceof SessionAuthenticationException) {
            / / the session
            result = CommonReturnType.fail("The session error");
        } else if(e instanceof ValidateCodeException) {
            // The verification code is abnormal
            result = CommonReturnType.fail(e.getMessage());
        } else {
            // Other unknown exceptions
            result = CommonReturnType.fail(e.getMessage());
        }
        httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code

Anonymous access (unlogged access) to the handler

@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        CommonReturnType result = CommonReturnType.fail("Login required to access the service");
        httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code

Access denied handler

@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        CommonReturnType result = CommonReturnType.fail("Access to services requires administrator identity");
        httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code

Logout successful processor

@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        CommonReturnType result = CommonReturnType.success("Logout successful.");
        httpServletResponse.setContentType("text/json; charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); }}Copy the code

Security configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // The database queries the user service
    @Autowired
    private UserNameDetailService userdetailservice;

    // Do not log in to the processor (anonymous access no permission processing)
    @Autowired
    private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;

    // Session expiration policy handler (remote login)
    @Autowired
    private CustomizeSessionInformationExpiredStrategy customizeSessionInformationExpiredStrategy;

    // Log in to the processor successfully
    @Autowired
    private CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;

    // Failed to log in to the handler
    @Autowired
    private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;

    // The handler is denied permission
    @Autowired
    private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
	
    // Log out the successful handler
    @Autowired
    private CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;

    // Image captcha filter
    @Autowired
    private ValidateImageCodeFilter validateImageCodeFilter;
    
	// SMS verification code filter
    @Autowired
    private SmsFilter smsFilter;
    
	// Configure the SMS verification code
    @Autowired
    private SmsAuthenticationConfig smsAuthenticationConfig;


    /** * Custom database search authentication *@param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userdetailservice).passwordEncoder(passwordEncoder());
    }

    /** * Set encryption mode *@return* /
    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }
    
    /** * Configure login *@param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Enable cross-domain and disable protection
        http.csrf().disable().cors();
        // Register a custom image captcha filter
        http.addFilterBefore(validateImageCodeFilter, UsernamePasswordAuthenticationFilter.class);
        // SMS validator
        http.addFilterBefore(smsFilter, ValidateImageCodeFilter.class);
        // Configure SMS verification code authentication to Spring Security
        http.apply(smsAuthenticationConfig);
        // Change the default jump when no login or login expired
        http.exceptionHandling().authenticationEntryPoint(customizeAuthenticationEntryPoint);
        // Path permission
        http.authorizeRequests()
            .antMatchers("/api/v1/user/login"."/doc.html"
                    ,"/aip/v1/qrs/cc"."/api/v1/user/mobile"
                    ,"/api/v1/user/sms"."/api/v1/user/image")
            .permitAll()
            .antMatchers("/usr/add").hasAnyAuthority("admin")
            .anyRequest().authenticated();
        // Log out
        http.logout()
            .logoutUrl("/logout").logoutSuccessUrl("/test/hello").deleteCookies("JSESSIONID")
            .logoutSuccessHandler(customizeLogoutSuccessHandler) // Logout successful logic processing
        .and()
            .formLogin()
            .successHandler(customizeAuthenticationSuccessHandler) // Logon success logic processing
            .failureHandler(customizeAuthenticationFailureHandler) // Logon failure logic processing
        .and()
            .exceptionHandling()
            .accessDeniedHandler(customizeAccessDeniedHandler) // Logical processing of permission denial
            .authenticationEntryPoint(customizeAuthenticationEntryPoint) // Anonymous Access No permission to access resources
        // Session management
        .and()
            .sessionManagement()
            .maximumSessions(1) // Maximum number of logins for the same user
            .expiredSessionStrategy(customizeSessionInformationExpiredStrategy); // Remote login (session failure) processing logic
    }

    public SmsAuthenticationConfig getSmsAuthenticationConfig(a) {
        returnsmsAuthenticationConfig; }}Copy the code

Querying user services in the database

@Service("userdetailservice")
public class UserNameDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserRoleRelationService userRoleRelationService;

    @Autowired
    private RolePermissionRelationService rolePermissionRelationService;

    @Autowired
    private SysPermissionService sysPermissionService;

    @SneakyThrows
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(null == username|| "".equals(username)) {
            throw new UsernameNotFoundException("Username cannot be empty.");
        }
        // Query the user
        // Look for user permissions
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(s1);
        return new User(user.getUsername(), newBCryptPasswordEncoder().encode(user.getPassword()), user.getEnabled(),user.getAccountNotExpired(),user.getCredentialsNotExpired(),user.getAccountNotLocked(),auths); }}Copy the code

Custom image verification code

Principle:

First of all, we through an interface for image authentication code, at the same time to the server to save image authentication code, and then we add filter to the authentication code in front of the UsernamePasswordAuthenticationFilter validation

Image verification code

public class ImageCode implements Serializable {
    // Image verification code
    private BufferedImage image;
    / / verification code
    private String code;
    // Expiration time
    private LocalDateTime expireTime;

    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }

    public boolean isExpire(a) {
        return LocalDateTime.now().isAfter(expireTime);
    }

    public BufferedImage getImage(a) {
        return image;
    }

    public void setImage(BufferedImage image) {
        this.image = image;
    }

    public String getCode(a) {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public LocalDateTime getExpireTime(a) {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime; }}Copy the code

Custom validation exception

You need to throw a defined exception during the check

public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = 5022575393500654458L;
    public ValidateCodeException(String message) {
        super(message); }}Copy the code

Randomly generate verification codes

public class ImageCodeUtil {
    /** * Create image verification code *@return* /
    public static ImageCode createImageCode(a) {
        int width = 100; // Captcha image width
        int height = 36; // Captcha image length
        int length = 4; // Verification code bits
        int expireIn = 120; // The validity period of the verification code is 120s

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();
        g.setColor(getRandColor(200.250));
        g.fillRect(0.0,width,height);
        g.setFont(new Font("Times New Roman",Font.ITALIC, 35));
        g.setColor(getRandColor(160.200));
        for(int i = 0; i< 155; i++){
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        StringBuilder sRand = new StringBuilder();
        String rand = null;
        for(int i = 0; i<length; i++){
            int anInt = random.nextInt(57);
            if(anInt  >= 10) {
                if(anInt + 65> =91 && anInt + 65< =96) {
                    anInt += 6;
                }
                char ch = (char) (anInt + 65);
                rand = String.valueOf(ch);
            } else {
                rand =  String.valueOf(anInt);
            }
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 15 * i + 15.28);
        }
            g.dispose();
            return new ImageCode(image, sRand.toString(),expireIn);
    }

    private static Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if(fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return newColor(r, g, b); }}Copy the code

Image verification code filter

Inherit to the OncePerRequestFilter and BasicAuthenticationFilter here is the same, because BasicAuthenticationFilter also inherited OncePerRequestFilter.

@Slf4j
@Component
public class ValidateImageCodeFilter extends OncePerRequestFilter {

    @Autowired
    // Custom validation failure handler
    private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;

    / / here I choose the authentication code in HttpSessionSessionStrategy redis (often used for storage)
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Whether the request path contains the keyword login && The request sent must be POST
        if (StringUtils.contains(request.getRequestURI(), "login")
                && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
            try {
                // Start validation
                validateCode(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                // If validation fails, a custom validation handler is used
                customizeAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
    // Verify the implementation
    private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
        // Take the verification code from SessionStrategy
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
        // Pull the verification code from the request path
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
        // The verification code is null
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("Captcha cannot be empty.");
        }
        // Verification code issuer verification
        if (codeInSession == null) {
            throw new ValidateCodeException("The captcha does not exist!");
        }
        // Whether the verification code is expired
        if (codeInSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
            throw new ValidateCodeException("The verification code has expired!");
        }
        // The verification code is correct
        if(! StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {throw new ValidateCodeException("The captcha is not correct!");
        }
        // Remove the server verification code store
        sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE"); }}Copy the code

Access to check the picture certificate code interface

@RequestMapping("/image")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
    ImageCode imageCode = ImageCodeUtil.createImageCode();
    ImageCode codeInRedis = new ImageCode(null,imageCode.getCode(),imageCode.getExpireTime());
    new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request), "SESSION_KEY_IMAGE_CODE", codeInRedis);
    response.setContentType("image/jpeg; charset=utf-8");
    response.setStatus(HttpStatus.OK.value());
    ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
Copy the code

Customize SMS verification codes

Different from the image verification code, the SMS verification code is a login method, while the image verification code is a parameter for login.

Here, we need to define a new method of login authentication. Let’s use the authentication method of account password to write.

SMS verification code filter

Intercepts the SMS verification code login request, forms a verification token, and then performs authentication. Finally, register the entire process with Spring Security

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String MOBILE_KEY = "mobile";

    private String mobileParameter = MOBILE_KEY;

    private boolean postOnly = true;


    public SmsAuthenticationFilter(a) {
        super(new AntPathRequestMatcher("/api/v1/user/mobile"."POST"));
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if(postOnly && ! request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();
        // Generate an authentication token, but it is not authenticated
        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter(a) {
        returnmobileParameter; }}Copy the code

SmsAuthenticationToken

In the interceptor in the previous step, we intercepted the SMS captcha login request, and we need to assemble an AuthenticationToken

public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    public SmsAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials(a) {
        return null;
    }

    @Override
    public Object getPrincipal(a) {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials(a) {
        super.eraseCredentials(); }}Copy the code

SmsAuthenticationProvider

Verify the validation token assembled above.

public class SmsAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private MobileDetailService mobileDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        UserDetails userDetails = mobileDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (null == userDetails) {
            throw new InternalAuthenticationServiceException("No user corresponding to this phone number was found");
        }
        // Mark the validation result as validated
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class
        aClass) {
        return SmsAuthenticationToken.class.isAssignableFrom(aClass);
    }

    public UserDetailsService getUserDetailService(a) {
        return mobileDetailService;
    }

    public void setUserDetailService(MobileDetailService mobileDetailService) {
        this.mobileDetailService = mobileDetailService; }}Copy the code

Configure the SMS verification code process to Spring Security

@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private MobileDetailService mobileDetailService;

    @Override
    public void configure(HttpSecurity http) {
        // A validation interceptor
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        // Set a manager for the validation interceptor
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // Set the handler whose validation was successful
        smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        // Set the handler whose validation failed
        smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        // A validation provider implements validation (adding permissions, etc.)
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        // Give the provider my login account information to get the service
        smsAuthenticationProvider.setUserDetailService(mobileDetailService);
        // Add this validator to the end of the user name loginhttp.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Interface for obtaining verification codes

Direct return through the interface instead of SMS service, here still uses the sessionStrategy storage, according to the need to use redis and other third-party database access.

@RequestMapping("/sms")
public void createSms(HttpServletRequest request,HttpServletResponse response,String mobile) throws IOException {
    SmsCode smsCode = RandomSmsUtil.createSMSCode();
    new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request),"SESSION_KEY_SMS_CODE" + mobile,smsCode);
    response.getWriter().write(smsCode.getCode());
    System.out.println("Your verification code is:" + smsCode.getCode() + "The valid time is:" + smsCode.getExpireTime());
}
Copy the code

Finally, remember that both of these customizations need to be registered in the configuration class. I’ve already configured this in advance when I configured autologon up front.

So far, the whole custom SMS verification code login, as well as the picture verification code, has been completed! This is sufficient for most login scenarios. If there is any mistake, please correct me!