本文正在参与「金石计划 . 瓜分6万现金大奖」

铢积寸累,水滴石穿

前语

在前面的几篇文章中,登录时都是使用用户名 + 暗码进行登录的,但是在实践项目当中,登录时,还需求输入图形验证码。那如安在 Spring Security 现有的认证体系中,参加自己的认证逻辑呢?这便是本文的内容,本文会介绍两种完成方案,一是根据过滤器完成;二是根据认证器完成。

验证码生成

已然需求输入图形验证码,那先来生成验证码吧。

参加验证码依靠

<!--验证码生成器-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Kaptcha 依靠是谷歌的验证码东西。

验证码装备

@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha captchaProducer() {
        Properties properties = new Properties();
        // 是否显示边框
        properties.setProperty("kaptcha.border","yes");
        // 边框色彩
        properties.setProperty("kaptcha.border.color","105,179,90");
        // 字体色彩
        properties.setProperty("kaptcha.textproducer.font.color","blue");
        // 字体大小
        properties.setProperty("kaptcha.textproducer.font.size","35");
        // 图片宽度
        properties.setProperty("kaptcha.image.width","300");
        // 图片高度
        properties.setProperty("kaptcha.image.height","100");
        // 文字个数
        properties.setProperty("kaptcha.textproducer.char.length","4");
        //文字大小
        properties.setProperty("kaptcha.textproducer.font.size","100");
        //文字随机字体
        properties.setProperty("kaptcha.textproducer.font.names", "宋体");
        //文字间隔
        properties.setProperty("kaptcha.textproducer.char.space","16");
        //干扰线色彩
        properties.setProperty("kaptcha.noise.color","blue");
        // 文本内容 从设置字符中随机抽取
        properties.setProperty("kaptcha.textproducer.char.string","0123456789");
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        kaptcha.setConfig(new Config(properties));
        return kaptcha;
    }
}

验证码接口

/**
 * 生成验证码
 */
@GetMapping("/verify-code")
public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException {
    resp.setContentType("image/jpeg");
    // 生成图形校验码内容
    String text = producer.createText();
    // 将验证码内容存入HttpSession
    session.setAttribute("verify_code", text);
    // 生成图形校验码图片
    BufferedImage image = producer.createImage(text);
    // 使用try-with-resources 方式,能够主动关闭流
    try(ServletOutputStream out = resp.getOutputStream()) {
        // 将校验码图片信息输出到浏览器
        ImageIO.write(image, "jpeg", out);
    }
}

代码注释写的很清楚,就不过多的介绍。归于固定的装备,已然装备完了,那就看看生成的效果吧!

Spring Security 在登录时如何添加图形验证码

接下来就看看怎么集成到 Spring Security 中的认证逻辑吧!

参加依靠

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

根据过滤器

编写自定义认证逻辑

这儿承继的过滤器为 UsernamePasswordAuthenticationFilter,并重写attemptAuthentication办法。用户登录的用户名/暗码是在UsernamePasswordAuthenticationFilter类中处理,那我们就承继这个类,增加对验证码的处理。当然也能够完成其他类型的过滤器,比如:GenericFilterBeanOncePerRequestFilter,不过处理起来会比承继UsernamePasswordAuthenticationFilter麻烦一点。

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 需求是 POST 恳求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 获得恳求验证码值
        String code = request.getParameter("code");
        HttpSession session = request.getSession();
        // 获得 session 中的 验证码值
        String sessionVerifyCode = (String) session.getAttribute("verify_code");
        if (StringUtils.isEmpty(code)){
            throw new AuthenticationServiceException("验证码不能为空!");
        }
        if(StringUtils.isEmpty(sessionVerifyCode)){
            throw new AuthenticationServiceException("请从头恳求验证码!");
        }
        if (!sessionVerifyCode.equalsIgnoreCase(code)) {
            throw new AuthenticationServiceException("验证码过错!");
        }
        // 验证码验证成功,铲除 session 中的验证码
        session.removeAttribute("verify_code");
        // 验证码验证成功,走本来父类认证逻辑
        return super.attemptAuthentication(request, response);
    }
}

代码逻辑很简单,验证验证码是否正确,正确则走父类本来逻辑,去验证用户名暗码是否正确。 过滤器定义完成后,接下来便是用我们自定义的过滤器替代默认的UsernamePasswordAuthenticationFilter

  • SecurityConfig
import cn.cxyxj.study04.Authentication.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study04.Authentication.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 用自定义的 VerifyCodeFilter 实例替代 UsernamePasswordAuthenticationFilter
        http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()  //敞开装备
                // 验证码、登录接口放行
                .antMatchers("/verify-code","/auth/login").permitAll()
                .anyRequest() //其他恳求
                .authenticated().and()//验证   表示其他恳求需求登录才干访问
                .csrf().disable();  // 禁用 csrf 维护
    }
    @Bean
    VerifyCodeFilter loginFilter() throws Exception {
        VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
        verifyCodeFilter.setFilterProcessesUrl("/auth/login");
        verifyCodeFilter.setUsernameParameter("account");
        verifyCodeFilter.setPasswordParameter("pwd");
        verifyCodeFilter.setAuthenticationManager(authenticationManagerBean());
        verifyCodeFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        verifyCodeFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return verifyCodeFilter;
    }
}

当我们替换了UsernamePasswordAuthenticationFilter之后,本来在 SecurityConfig#configure 办法中关于 form 表单的装备就会失效,那些失效的特点,都能够在装备 VerifyCodeFilter 实例的时分装备;还需求记住装备AuthenticationManager,否则发动时会报错。

  • MyAuthenticationFailureHandler
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 登录失败回调
 */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        String msg = "";
        if (e instanceof LockedException) {
            msg = "账户被确定,请联系管理员!";
        }
       else if (e instanceof BadCredentialsException) {
            msg = "用户名或许暗码输入过错,请从头输入!";
        }
        out.write(e.getMessage());
        out.flush();
        out.close();
    }
}
  • MyAuthenticationSuccessHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 登录成功回调
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    }
}

测验

  • 不传入验证码建议恳求。

Spring Security 在登录时如何添加图形验证码

  • 恳求获取验证码接口

Spring Security 在登录时如何添加图形验证码

  • 输入过错的验证码

Spring Security 在登录时如何添加图形验证码

  • 输入正确的验证码

Spring Security 在登录时如何添加图形验证码

  • 输入现已使用过的验证码
    Spring Security 在登录时如何添加图形验证码
    各位读者是不是会觉得已然承继了 Filter,那是不是每个接口都会进入到我们的自定义办法中呀!如果是承继了 GenericFilterBean、OncePerRequestFilter 那是肯定会的,需求手动处理。 但我们承继的是 UsernamePasswordAuthenticationFilter,security 现已帮忙处理了。处理逻辑在其父类 AbstractAuthenticationProcessingFilter#doFilter 中。

Spring Security 在登录时如何添加图形验证码

根据认证器

编写自定义认证逻辑

验证码逻辑编写完成,那接下来就自定义一个 VerifyCodeAuthenticationProvider 承继自 DaoAuthenticationProvider,并重写 authenticate 办法。

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
 * 验证码验证器
 */
public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获得恳求验证码值
        String code = req.getParameter("code");
        // 获得 session 中的 验证码值
        HttpSession session = req.getSession();
        String sessionVerifyCode = (String) session.getAttribute("verify_code");
        if (StringUtils.isEmpty(code)){
            throw new AuthenticationServiceException("验证码不能为空!");
        }
        if(StringUtils.isEmpty(sessionVerifyCode)){
            throw new AuthenticationServiceException("请从头恳求验证码!");
        }
        if (!code.toLowerCase().equals(sessionVerifyCode.toLowerCase())) {
            throw new AuthenticationServiceException("验证码过错!");
        }
        // 验证码验证成功,铲除 session 中的验证码
        session.removeAttribute("verify_code");
        // 验证码验证成功,走本来父类认证逻辑
        return super.authenticate(authentication);
    }
}

自定义的认证逻辑完成了,剩下的问题便是怎么让 security 走我们的认证逻辑了。

在 security 中,一切的 AuthenticationProvider 都是放在 ProviderManager 中统一管理的,所以接下来我们就要自己提供 ProviderManager,然后注入自定义的 VerifyCodeAuthenticationProvider。

  • SecurityConfig
import cn.cxyxj.study02.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study02.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }
    @Bean
    VerifyCodeAuthenticationProvider verifyCodeAuthenticationProvider() {
        VerifyCodeAuthenticationProvider provider = new VerifyCodeAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService());
        return provider;
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        ProviderManager manager = new ProviderManager(verifyCodeAuthenticationProvider());
        return manager;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()  //敞开装备
                // 验证码接口放行
                .antMatchers("/verify-code").permitAll()
                .anyRequest() //其他恳求
                .authenticated()//验证   表示其他恳求需求登录才干访问
                .and()
                .formLogin()
                .loginPage("/login.html") //登录页面
                .loginProcessingUrl("/auth/login") //登录接口,此地址能够不真实存在
                .usernameParameter("account") //用户名字段
                .passwordParameter("pwd") //暗码字段
                .successHandler(new MyAuthenticationSuccessHandler())
                .failureHandler(new MyAuthenticationFailureHandler())
                .permitAll() // 上述 login.html 页面、/auth/login接口放行
                .and()
                .csrf().disable();  // 禁用 csrf 维护
        ;
    }
}

测验

  • 不传入验证码建议恳求。
    Spring Security 在登录时如何添加图形验证码
  • 恳求获取验证码接口

Spring Security 在登录时如何添加图形验证码

  • 输入过错的验证码

Spring Security 在登录时如何添加图形验证码

  • 输入正确的验证码

Spring Security 在登录时如何添加图形验证码

  • 输入现已使用过的验证码

Spring Security 在登录时如何添加图形验证码

  • 如你对本文有疑问或本文有过错之处,欢迎谈论留言指出。如觉得本文对你有所协助,欢迎点赞和重视。