Spring Security---验证码详解

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security---验证码详解相关的知识,希望对你有一定的参考价值。


验证码实现的三种方式

  • session存储验证码,不适用于集群应用
  • 共享session存储验证码,适用于集群应用
  • 基于对称算法的验证码,适用于集群应用

验证码的组成部分

验证码实际上和谜语有点像,分为谜面和谜底。谜面通常是图片,谜底通常为文字。谜面用于展现,谜底用于校验。

  • 对于字符型验证码。比如:谜面是显示字符串"ABGH"的图片,谜底是字符串"ABGH"
  • 对于计算类验证码。比如:谜面是“1+1=”的图片,谜底是“2”
  • 对于拖拽类的验证码。比如:谜面是一个拖拽式的拼图,谜底是拼图位置的坐标

总之,不管什么形式的谜面,最后用户的输入内容要和谜底进行验证。


session存储验证码

图中蓝色为服务端、澄粉色为客户端。

这是一种最典型的验证码实现方式,实现方式也比较简单。

  • 应用服务端随机的生成验证码文字
  • 将验证码文字存到session里面
  • 根据验证码文字生成验证码图片,响应给客户端
  • 检查用户输入的内容与验证码谜底是否一致

这种实现方式的优点就是比较简单,缺点就是:因为一套应用部署一个session,当我们把应用部署多套如:A、B、C,他们各自有一个session并且不共享。导致的结果就是验证码和图片由A生成,但是验证请求发送到了B,这样就不可能验证通过。


共享session存储验证码

分布式应用验证码的实现,实际上不是验证码的问题,而是如何保证session唯一性或共享性的问题。主要的解决方案有两种:

  • 通常我们实现负载均衡应用的前端都是使用nginx或者haproxy,二者都可以配置负载均衡策略。其中一种策略就是:你的客户端ip上一次请求的是A应用,你的下一次请求还转发给A应用。这样就保证了session的唯一性。但是这种方式有可能会导致A、B、C应用其中一个或两个分配了大量的请求,而另外一个处理很少的请求,导致负载并不均衡。
  • 另外一种非常通用的方式就是将分布式应用的session统一管理,也就是说原来A、B、C各自的session都存在自己的内存中,现在更改为统一存储到一个地方,大家一起用。这样就实现了session的唯一和共享,是实现分布式应用session管理的有效途径。在Spring框架内,最成熟的解决方案就是spring session + redis 。可自行参考实现。

基于对称算法的验证码

可能出于主机资源的考虑,可能出于系统架构的考量,有些应用是无状态的

  • 什么是无状态应用:就是不保存用户状态的应用。
  • 什么是用户状态:比如当你登陆之后,在session中保存的用户的名称、组织等等信息。
  • 所以可以简单的理解,无状态应用就是无session应用。当然这并不完全准确。

那么对于这些无状态的应用,我们就无法使用session,或者换个说法从团队开发规范上就不让使用session。那么我们的验证码该怎么做?

  • 同样,首先要生成随机的验证码(谜底),但是不做任何存储操作
  • 将谜底(验证码文字)加上时间串、应用信息等组成一个字符串进行加密。必须是对称加密,也就是说可以解密的加密算法。
  • 生成验证码图片,并与加密后的密文,通过cookies一并返回给客户端。
  • 当用户输入验证码提交登录之后,服务端解密cookies中的密文(主要是验证码文字),与用户的输入进行验证比对。

这种做法的缺陷是显而易见的:实际上就是将验证码文字在客户端服务端之间走了一遍。虽然是加密后的验证码文字,但是有加密就必须有解密,否则无法验证。所以更为稳妥的做法是为每一个用户生成密钥,并将密钥保存到数据库里面,在对应的阶段内调用密钥进行加密或者解密。

从密码学的角度讲,没有一种对称的加密算法是绝对安全的。所以更重要的是保护好你的密钥。正如没有一把锁头是绝对安全的,更重要的是保护好你的钥匙。


基于session的图片验证码实现

本节基于google开源的验证码实现类库kaptcha,作为验证码工具实现验证码功能开发。验证码工具类通常要具有以下三种功能方法:

  • 生成验证码文字或其他用于校验的数据形式(即谜底)
  • 生成验证码前端显示图片或拼图等(即谜面)
  • 用于校验用户输入与谜底的校验方法(如果是纯文字,就自己比对以下就可以。如果是基于物理图形拖拽、旋转等方式,需要专用的校验方法)

基于session的图片验证码实现

本节基于google开源的验证码实现类库kaptcha,作为验证码工具实现验证码功能开发。验证码工具类通常要具有以下三种功能方法:

  • 生成验证码文字或其他用于校验的数据形式(即谜底)
  • 生成验证码前端显示图片或拼图等(即谜面)
  • 用于校验用户输入与谜底的校验方法(如果是纯文字,就自己比对以下就可以。如果是基于物理图形拖拽、旋转等方式,需要专用的校验方法)

这种验证码类库有很多,但是都是基于以上逻辑。我们本节使用kaptcha。


验证码生成之配置使用kaptcha

<dependency>
   <groupId>com.github.penggle</groupId>
   <artifactId>kaptcha</artifactId>
   <version>2.3.2</version>
   <exclusions>
      <exclusion>
         <artifactId>javax.servlet-api</artifactId>
         <groupId>javax.servlet</groupId>
      </exclusion>
   </exclusions>
</dependency>
  • 假设我们的配置文件是application.yml,新建一个单独的文件叫做kaptcha.properties。因为kaptcha的配置不符合yaml的规范格式,所以只能采用properties。需配合注解PropertySourc使用。
  • 假设我们的配置文件是application.properties,将下面这段代码加入进去即可,不用单独建立文件。
  • 下面的验证码配置,从英文单词的角度很容易理解,当我们需要调整验证码的边框、颜色、大小、字体等属性的时候,可以修改这些配置。
kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100
kaptcha.image.height=45
kaptcha.session.key=code
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=35
kaptcha.textproducer.char.length=4
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑

下面的代码加载了配置文件中的kaptcha配置(参考Spring Boot的配置加载),如果是独立的properties文件,需加上PropertySource注解说明。

另外,我们通过加载完成的配置,初始化captchaProducer的Spring Bean,用于生成验证码。

@Component
@PropertySource(value = "classpath:kaptcha.properties")
public class CaptchaConfig 

    @Value("$kaptcha.border")
    private String border;
    @Value("$kaptcha.border.color")
    private String borderColor;
    @Value("$kaptcha.textproducer.font.color")
    private String fontColor;
    @Value("$kaptcha.image.width")
    private String imageWidth;
    @Value("$kaptcha.image.height")
    private String imageHeight;
    @Value("$kaptcha.textproducer.char.length")
    private String charLength;
    @Value("$kaptcha.textproducer.font.names")
    private String fontNames;
    @Value("$kaptcha.textproducer.font.size")
    private String fontSize;
    @Value("$kaptcha.session.key")
    private String sessionKey;

    @Bean(name = "captchaProducer")
    public DefaultKaptcha getKaptchaBean() 
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border", border);
        properties.setProperty("kaptcha.border.color", borderColor);
        properties.setProperty("kaptcha.textproducer.font.color", fontColor);
        properties.setProperty("kaptcha.image.width", imageWidth);
        properties.setProperty("kaptcha.image.height", imageHeight);
        properties.setProperty("kaptcha.session.key", sessionKey);
        properties.setProperty("kaptcha.textproducer.char.length", charLength);
        properties.setProperty("kaptcha.textproducer.font.names", fontNames);
        properties.setProperty("kaptcha.textproducer.font.size",fontSize);
        //kapcha的配置类
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    


至此,Kaptcha开源验证码软件的配置我们就完成了,如果发现IDEA环境下配置文件读取中文乱码,修改如下配置。


验证码生成之session保存

生成验证码的Controller。同时需要开放路径"/kaptcha"的访问权限,配置成不需登录也无需任何权限即可访问的路径。

  • 通过captchaProducer.createText()生成验证码文字,并和失效时间一起保存到CaptchaImageVO中。
  • 将CaptchaImageVO验证码信息类对象,保存到session中。(这个类的代码后文有介绍)
  • 通过captchaProducer.createImage(capText)生成验证码图片,并通过ServletOutputStream返回给前端
@Controller
public class CodeController 

    //kapcha验证码生成
    @Resource
    DefaultKaptcha captchaProducer;
 
    @RequestMapping("/kaptcha")
    public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws IOException 
        HttpSession session = request.getSession();
        // 禁止服务器缓存
        response.setDateHeader("Expires",0);
        // 设置标准的 HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        // 设置IE扩展 HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        // 设置标准 HTTP/1.0 不缓存图片
        response.setHeader("Pragma", "no-cache");
        // 返回一个 jpeg图片, 默认是text/html
        response.setContentType("image/jpeg");
 
        // 生成验证码
        String capText = captchaProducer.createText(); // 为图片创建文本
        //创建验证码对象----验证码,过期时间
        CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
        //将验证码存到session
        session.setAttribute(Constants.KAPTCHA_SESSION_KEY, captchaImageVO);
        //将图片返回给前端
        try(ServletOutputStream out = response.getOutputStream();) 
            BufferedImage bi = captchaProducer.createImage(capText);
            ImageIO.write(bi, "jpg", out);
            out.flush();
        //使用try-with-resources不用手动关闭流

    

我们要把CaptchaImageVO保存到session里面。所以该类中不要加图片,只保存验证码文字和失效时间,用于后续验证即可。把验证码图片保存起来既没有用处,又浪费内存。

@Data
public class CaptchaImageVO 

    //验证码文字
    private String code;
    //验证码失效时间
    private LocalDateTime expireTime;

    public CaptchaImageVO(String code, int expireAfterSeconds)
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
    

    //验证码是否失效
    public boolean isExpried() 
        return LocalDateTime.now().isAfter(expireTime);
    

    public String getCode() 
        return code;
    


验证码用户访问

把如下代码加入到登录页面合适的位置,注意图片img标签放到登录表单中。

<img src="/kaptcha" id="kaptcha" width="110px" height="40px"/>

<script>
    window.onload=function()
        var kaptchaImg = document.getElementById("kaptcha");
        kaptchaImg.onclick = function()
            kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
        
    
</script>
  • 实现的效果是,页面初始化即加载验证码。以后每一次点击,都会更新验证码。
  • 注意:一定设置width和height,否则图片无法显示。
  • 需要为“/kaptcha”配置permitAll公开访问权限,否则无法访问到
  http.authorizeRequests()
              .antMatchers("/admin/**").hasRole("admin")
              .antMatchers("/user/**").hasRole("user")
              .antMatchers("/kaptcha").permitAll()//放行验证码的显示请求,不需要认证
              .anyRequest().authenticated()
              ....


验证码之安全校验

  • 编写我们的自定义图片验证码过滤器CaptchaCodeFilter,过滤器中拦截登录请求
  • CaptchaCodeFilter过滤器中从seesion获取验证码文字与用户输入比对,比对通过执行其他过滤器链
  • 比对不通过,抛出SessionAuthenticationException异常,交给AuthenticationFailureHandler处理
  • 最后将CaptchaCodeFilter放在UsernamePasswordAuthenticationFilter表单过滤器之前执行。
import com.google.code.kaptcha.Constants;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;

@Component
public class CaptchaCodeFilter extends OncePerRequestFilter 

    @Resource
    MyFailHandler myAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException 
         // 必须是登录的post请求才能进行验证,其他的直接放行
        if("/login".equals(request.getRequestURI())
                &&request.getMethod().equalsIgnoreCase("post"))
            try
                //1.验证谜底与用户输入是否匹配
                validate(new ServletWebRequest(request));
            catch(AuthenticationException e)
                 //2.捕获步骤1中校验出现异常,交给失败处理类进行进行处理
                myAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
                return;
            

        
        //通过校验,就放行
        filterChain.doFilter(request,response);
    

    private void validate(ServletWebRequest request) throws ServletRequestBindingException 

        HttpSession session = request.getRequest().getSession();
        //获取用户登录界面输入的code
        String codeInRequest = ServletRequestUtils.getStringParameter(
                request.getRequest(),"code");
        if(codeInRequest.isEmpty())
            throw new SessionAuthenticationException("验证码不能为空");
        

        // 获取session池中的验证码谜底
        CaptchaImageVO codeInSession = (CaptchaImageVO)
                session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        if(Objects.isNull(codeInSession)) 
            throw new SessionAuthenticationException("您输入的验证码不存在");
        

        // 校验服务器session池中的验证码是否过期
        if(codeInSession.isExpried()) 
            session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
            throw new SessionAuthenticationException("验证码已经过期");
        

        // 请求验证码校验
        if(!codeInSession.getCode().equals(codeInRequest)) 
            throw new SessionAuthenticationException("验证码不匹配");
        

    

  • 上面代码中之所以抛出SessionAuthenticationException异常,因为该异常是AuthenticationException的子类,同时也是针对Session数据校验的异常。可以在doFilterInternal中被捕获,交给MyAuthenticationFailureHandler处理。MyAuthenticationFailureHandler 只认识AuthenticationException及其子类
  • codeInRequest是用户请求输入的验证码
  • codeInSession是用户请求验证码图片时,保存在session中的验证码谜底。
@Component
//继承该类,是因为其默认的实现,可以简化我们的代码
public class MyFailHandler extends SimpleUrlAuthenticationFailureHandler 
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException 
        String errorMsg="用户名或密码错误";
        //验证码错误
        //如果是验证码错误,响应JSON数据给前端
        if(e instanceof SessionAuthenticationException)
        
            errorMsg=e.getMessage();
            httpServletResponse.setContentType("text/plain;charset=UTF-8");
            httpServletResponse.getWriter().write(errorMsg);
            return;
        
        //如果是用户名密码错误,调用父类的方法,默认跳转到登录页面
        super.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
    

最后将CaptchaCodeFilter过滤器放到用户名密码登录过滤器之前执行。login.html登录请求中要传递参数:code

在这里插<p>以上是关于Spring Security---验证码详解的主要内容,如果未能解决你的问题,请参考以下文章</p> 
<p > <a style=spring security相关认证类详解

将验证码与 Spring Security 集成

Spring Security---Oauth2详解

重构Spring Security实现图形验证码的功能

Spring Security-- 验证码功能的实现

Spring Security实现图形验证码的功能