图片验证码是我们在做登录的时候需求很多的一个功能,其可以帮我们防止恶意登录,保护账户安全。
com.github.penggle
kaptcha
2.3.2
javax.servlet-api
javax.servlet
第二步:## 在properties配置文件中做相关的配置
kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100 #图片宽度
kaptcha.image.height=45 #图片高度
kaptcha.session.key=code #sessio密钥
kaptcha.textproducer.font.color=blue #验证码颜色
kaptcha.textproducer.font.size=35 #验证码字体大小
kaptcha.textproducer.char.length=4 #验证码位数
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑 #验证码字体
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.util.Properties;
/*
验证码配置
*/
//加载自定义的配置文件
@Component
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.session.key}")
private String sessionKey;
@Value("${kaptcha.textproducer.char.length}")
private String charLength;
@Value("${kaptcha.textproducer.font.names}")
private String fontNames;
@Value("${kaptcha.textproducer.font.size}")
private String fontSize;
@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.image.width",imageWidth );
properties.setProperty("kaptcha.textproducer.font.color",fontColor );
properties.setProperty("kaptcha.image.height", imageHeight );
properties.setProperty("kaptcha.session.key",sessionKey );
properties.setProperty("kaptcha.textproducer.font.size", fontSize );
properties.setProperty("kaptcha.textproducer.char.length", charLength );
properties.setProperty("kaptcha.textproducer.font.names", fontNames );
defaultKaptcha.setConfig(new Config(properties));
return defaultKaptcha;
}
}
该类的成员变量用@Value注解从properties配置文件取值注入
再向容器中注入一个Bean(DefaultKaptcha 类的实例),该实例需要一个Properties集合参数,而Properties集合的键值对信息就是生成验证码的相关配置。
/*
验证码过期的相关配置
*/
import java.time.LocalDateTime;
public class CaptchaImageVO {
private String code;
private LocalDateTime expireTime; //过期时间
public CaptchaImageVO(){}
public CaptchaImageVO(String code,int expireAfterSeconds){
this.code = code;
//当前时间加上指定时间后过期
this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
}
public boolean isExpired(){
//判断当前时间时候超过设置的时间
boolean isExpired = LocalDateTime.now().isAfter(expireTime);
return isExpired;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
}
提供两个成员方法:
public CaptchaImageVO(String code,int expireAfterSeconds)
该方法参数一是生成的验证码的答案,比如4h16,t8p3,XUOp,这种四位验证码,参数二是设置验证码的失效时间,比如60秒之后过期。
public boolean isExpired()
该方法判断验证码是否过期,如果设定验证码的过期时间超过了LocalDateTime ,now()则说明没有失效,例如:设定的过期时间是1月30号10点,而现在的时间是1.28号10点,这就是没有失效,反之就是失效了。
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.wk.springcloud.config.auth.imagecode.CaptchaImageVO;
import com.wk.springcloud.config.auth.imagecode.CaptchaUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
@RestController
public class CaptchaController {
@Resource(name = "captchaProducer")
private DefaultKaptcha captchaProducer;
//todo 切记这里的映射要通知springsecurity允许访问(permitAll())
@RequestMapping(value = "/kaptcha",method = RequestMethod.GET)
public void kaptcha(HttpSession session, HttpServletResponse response){
response.setDateHeader("Expires",0);
response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");
response.setHeader("Cache-Control","post-check=0, pre-check=0");
response.setHeader("Pragma","no-cache");
response.setContentType("image/jpeg");
//生成谜底
String text = captchaProducer.createText();
//将谜底保存到session中 并设置120秒之后过期
session.setAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY,new CaptchaImageVO(text,120));
try {
ServletOutputStream outputStream = response.getOutputStream();
//根据谜底生成谜面
BufferedImage image = captchaProducer.createImage(text);
//用输出流写入到页面
ImageIO.write(image,"jpg",outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个请求路径要和前端的img的src属性保持一致
这个类的话就是设置响应对象的一些配置,响应为图片,禁用缓存等等
并创建一个验证码,设置好过期时间后将其放入session中,然后再用输出流返回到前端页面。
mport com.wk.springcloud.config.auth.MyAuthenticationFailureHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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.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 {
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,IOException {
if (Objects.equals("/login",request.getRequestURI()) &&
"post".equalsIgnoreCase(request.getMethod())
){
try {
//验证谜底与用户输入是否匹配
validate(new ServletWebRequest(request));
} catch (AuthenticationException e) {
//如果验证有异常 调用登录失败的方法
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
filterChain.doFilter(request,response);
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
HttpSession session = request.getRequest().getSession();
String captchaCode = ServletRequestUtils.getStringParameter(request.getRequest(),"captchaCode");
if (StringUtils.isEmpty(captchaCode)){
throw new SessionAuthenticationException("验证码不能为空!");
}
//获取session中的验证码
CaptchaImageVO codeInSession = (CaptchaImageVO)session.getAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY);
if (Objects.isNull(codeInSession)){
throw new SessionAuthenticationException("验证码不存在!");
}
//校验是否过期
if (codeInSession.isExpired()){
//从session中移除
session.removeAttribute(CaptchaUtils.CAPTCHA_SESSION_KEY);
throw new SessionAuthenticationException("验证码已经过期!");
}
//校验是否匹配
if (!Objects.equals(codeInSession.getCode(),captchaCode)){
throw new SessionAuthenticationException("验证码不匹配!");
}
}
}
该类继承自OncePerRequestFilter 并重写doFilterInternal方法。
首先我们来看第一个方法 在该过滤器执行时就会调用这个方法
首先请求URI必须是/login,而且是post方式,因为验证码是在登录的请求时才进行校验的,所以这里的URI写的就是系统登录的URI,如果URI不是/login的话,我们就filterChain.doFilter(request,response);放行
接着我们分析一下validate(new ServletWebRequest(request));这个方法
:首先这个方法会拿到session的请求参数captchaCode的值,这个captchaCode是什么?其实就是验证码的input的name属性
这是我们前端的那个验证码输入框。拿到这个请求参数之后,我们先判断一下是否为空,如果为空直接抛出异常,为什么会抛出SessionAuthenticationException这个异常?这个待会说一下。
然后再接着判断session中的验证码是否空,为空还是继续抛出异常,
再继续判断时候过期,如果过期继续抛出异常,再继续判断是否匹配,如果不匹配继续抛出异常。
接着我们看这里,挺关键的
抛出异常后我们调用了登录失败的处理器,并将异常对象传入,了解security的同学应该知道登录失败可以设置默认的URL或者处理器,这里我们就使用处理器的方式,且抛出的异常刚好是AuthenticationException的子类,这样我们就能顺利捕捉。 接下来我们来看一下这个失败处理器的逻辑。这里记得catch后要return;
package com.wk.springcloud.config.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Value("${spring.security.loginType}")
private String loginType;
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//todo 判断是否是验证码抛出的异常
String message = "用户名或密码错误!";
if (exception instanceof SessionAuthenticationException){
message = exception.getMessage();
}
if ("JSON".equalsIgnoreCase(loginType)){
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(ResponseA.failed().addMsg(message)));
}else {
super.onAuthenticationFailure(request, response, exception);
}
}
}
首先我们定义了一个message 并判断发生的异常是不是SessionAuthenticationException的实例,如果是的话,说明抛出的是验证码相关异常。否则就是正常登录失败的异常。然后将响应的提示信息相应给前端。
点击按钮刷新验证码