Spring Security---验证码详解
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security---验证码详解相关的知识,希望对你有一定的参考价值。
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
spring security相关认证类详解