SpringSceurity(3)---图形验证码功能实现
有关springSceurity以前有写过两篇文章:html
一、SpringSecurity(1)---认证+受权代码实现前端
二、SpringSecurity(2)---记住我功能实现java
这篇咱们来说图形验证码功能实现。git
1、思路
我整理下springSceurity整合图形验证码的大体思路:github
一、首先对于验证码自己而言,应该有三部分组成 一、存放验证码的背景图片 二、验证码 三、验证码的有效时间。 二、对于springSceurity而言,验证码的执行校验顺序确定是在UsernamePasswordAuthenticationFilter以前的,由于若是验证码都不对,那么 根本都不须要验证帐号密码。因此咱们须要自定义一个验证码过滤器,而且配置在UsernamePasswordAuthenticationFilter以前执行。 三、对于获取验证码的接口,确定是不须要进行认证限制的。 四、对于获取验证码的接口的时候,须要把该验证码信息+当前浏览器的SessonId绑定在一块儿存在Seesion中,为了后面校验的时候经过SessonId 去取这个验证码信息。 五、登录请求接口,除了带上用户名和密码以外,还须要带上验证码信息。在进入验证码过滤器的时候,首先经过SessonId获取存在Sesson中的 验证码信息,拿到验证码信息以后首先还要校验该验证码是否在有效期内。以后再和当前登录接口带来的验证码进行对比,若是一致,那么当前 验证码这一关就过了,就开始验证下一步帐号和密码是否正确了。
整个流程大体就是这样。下面如今是具体代码,而后进行测试。spring
2、代码展现
这里只展现一些核心代码,具体完整项目会放到github上。浏览器
一、ImageCodeProperties
这个是一个bean实体,是一个图形验证码的默认配置。session
@Data public class ImageCodeProperties { /** * 验证码宽度 */ private int width = 67; /** * 验证码高度 */ private int height = 23; /** * 验证码长度 */ private int length = 4; /** * 验证码过时时间 */ private int expireIn = 60; /** * 须要验证码的请求url字符串,用英文逗号隔开 */ private String url = "/login"; }
二、ImageCode
这个是图片验证码的完整信息,也会将这个完整信息存放于Sesson中。app
图片验证码信息 由三部分组成 :dom
1.图片信息(长、宽、背景色等等)。2.code就是真正的验证码,用来验证用。3.该验证码的有效时间。
@Data @AllArgsConstructor @NoArgsConstructor public class ImageCode { private BufferedImage image; private String code; private LocalDateTime expireTime; public ImageCode(BufferedImage image, String code, int expireIn) { this.image = image; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } /** * 校验是否过时 */ public boolean isExpired() { return LocalDateTime.now().isAfter(expireTime); } }
三、ValidateCodeGeneratorService
获取验证码的接口
public interface ValidateCodeGeneratorService { /** * 生成图片验证码 * * @param request 请求 * @return ImageCode实例对象 */ ImageCode generate(ServletWebRequest request); }
四、ImageCodeGeneratorServiceImpl
获取图片验证码的接口的实现类
@Data public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService { private static final String IMAGE_WIDTH_NAME = "width"; private static final String IMAGE_HEIGHT_NAME = "height"; private static final Integer MAX_COLOR_VALUE = 255; private ImageCodeProperties imageCodeProperties; @Override public ImageCode generate(ServletWebRequest request) { //设置图片的宽度和高度 int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, imageCodeProperties.getWidth()); int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME, imageCodeProperties.getHeight()); 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, 20)); 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(); for (int i = 0; i < imageCodeProperties.getLength(); i++) { String rand = String.valueOf(random.nextInt(10)); sRand.append(rand); g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110))); g.drawString(rand, 13 * i + 6, 16); } g.dispose(); //这样验证码的图片 、数字、有效期都有组装好了 return new ImageCode(image, sRand.toString(), imageCodeProperties.getExpireIn()); } /** * 生成随机背景条纹 * * @param fc 前景色 * @param bc 背景色 * @return RGB颜色 */ private Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > MAX_COLOR_VALUE) { fc = MAX_COLOR_VALUE; } if (bc > MAX_COLOR_VALUE) { bc = MAX_COLOR_VALUE; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } }
五、ValidateCodeController
获取验证码的请求接口。
@RestController public class ValidateCodeController { /** * 前缀 */ public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; private static final String FORMAT_NAME = "JPEG"; @Autowired private ValidateCodeGeneratorService imageCodeGenerator; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { // 第一步:根据请求生成一个图形验证码对象 ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request)); // 第二步:将图形验证码对象存到session中,第一个参数能够从传入的请求中获取session sessionStrategy.setAttribute(new ServletRequestAttributes(request), SESSION_KEY, imageCode); // 第三步:将生成的图片写到接口的响应中 ImageIO.write(imageCode.getImage(), FORMAT_NAME, response.getOutputStream()); } }
到这里,咱们可用请求获取图片验证码的信息了。接下来咱们就要登录请求部分的代码。
六、ValidateCodeFilter
自定义过滤器,这里面才是核心的代码,首先继承OncePerRequestFilter(直接继承Filter也是可用的),实现InitializingBean是为了初始化一些初始数据。
这里走的逻辑就是把存在session中的图片验证码和当前请求的验证码进行比较,若是相同则放行,不然直接抛出异常。
@Data @Slf4j @Component public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { private static final String SUBMIT_FORM_DATA_PATH = "/login"; /** * 失败处理器 */ @Autowired private AuthenctiationFailHandler authenctiationFailHandler; /** * 验证码属性类 */ @Autowired private ImageCodeProperties imageCodeProperties; /** * 存放须要走验证码请求url */ private Set<String> urls = new HashSet<>(); /** * 处理session工具类 */ private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); /** * 正则配置工具 */ private final AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 在初始化bean的时候都会执行该方法 */ @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); String[] configUrls = StringUtils.split(imageCodeProperties.getUrl(), ","); // 登陆的连接是必需要进行验证码验证的 urls.addAll(Arrays.asList(configUrls)); } /** * 拦截请求进来的方法。 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { boolean action = false; for (String url : urls) { // 若是实际访问的URL能够与用户在imageCodeProperties中url配置的相同,那么就进行验证码校验 log.info("request.getRequestURI = {}",request.getRequestURI()); if (antPathMatcher.match(url, request.getRequestURI())) { action = true; } } //说明须要校验 if (action) { try { validate(new ServletWebRequest(request)); } catch (ValidateCodeException e) { authenctiationFailHandler.onAuthenticationFailure(request, response, e); return; } } //进入下一个过滤器 filterChain.doFilter(request, response); } /** * 验证码校验逻辑 * */ private void validate(ServletWebRequest request) throws ServletRequestBindingException { // 从session中获取图片验证码 ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY); // 从请求中获取用户填写的验证码 String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode"); if (StringUtils.isBlank(imageCodeInRequest)) { throw new ValidateCodeException("验证码不能为空"); } if (null == imageCodeInSession) { throw new ValidateCodeException("验证码不存在"); } if (imageCodeInSession.isExpired()) { sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY); throw new ValidateCodeException("验证码已过时"); } log.info("session中获取的验证码={},sessionId ={}",imageCodeInSession.getCode(),request.getSessionId()); log.info("登录操做传来的验证码={}",imageCodeInRequest); if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) { throw new ValidateCodeException("验证码不匹配"); } // 验证成功,删除session中的验证码 sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY); } }
七、WebSecurityConfig
SpringSecurity的Java 配置类也须要作一点点改动。那就是须要设置ValidateCodeFilter要在UsernamePasswordAuthenticationFilter以前进行拦截过滤。

到这里整个图形验证码的功能就开发完成了,具体代码放在github上下面进行测试。
3、测试
说明下我这里懒的写前端相关代码了,因此直接用posman用请求来获取验证码,获取验证码以后再进行登录操做。
一、获取验证码

这里验证码code为:1848
二、登录成功

输入的验证码也是1848,显示登录成功。
三、登录失败
由于配置的时候图片验证码有效期为60秒,因此在咱们获取验证码后,过60秒再去登录,就能发现,验证码已过时。

整个验证码功能大体就是这样。
Github地址
: spring-boot-security-study-03
别人骂我胖,我会生气,由于我内心认可了我胖。别人说我矮,我就会以为可笑,由于我内心知道我不可能矮。这就是咱们为何会对别人的攻击生气。 攻我盾者,乃我心里之矛(19)