一: 生成验证码
添加验证码大致可以分为三个步骤:根据随机数生成验证码图片;将验证码图片显示到登录页面;认证流程中加入验证码校验。Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。
导入依赖:
<dependency><groupId>org.springframework.social</groupId><artifactId>spring-social-config</artifactId><version>1.1.4.RELEASE</version></dependency>
创建图片验证码对象
package com.springboot.securityone.utils;import java.awt.image.BufferedImage;import java.time.LocalDateTime;/*** <p>* Description:[]* </p>** @author shf* @version 1.0* @date Created on 2020/4/30 16:52*/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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {this.image = image;this.code = code;this.expireTime = expireTime;}boolean isExpire() {return LocalDateTime.now().isAfter(expireTime);}public BufferedImage getImage() {return image;}public void setImage(BufferedImage image) {this.image = image;}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;}}
新增获取验证码的 controller
package com.springboot.securityone.controller;import com.springboot.securityone.utils.ImageCode;import org.springframework.social.connect.web.HttpSessionSessionStrategy;import org.springframework.social.connect.web.SessionStrategy;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.context.request.ServletWebRequest;import javax.imageio.ImageIO;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.awt.*;import java.awt.image.BufferedImage;import java.io.IOException;import java.util.Random;/*** <p>* Description:[]* </p>** @author shf* @version 1.0* @date Created on 2020/4/30 16:53*/@RestControllerpublic class ValidateController {public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@GetMapping("/code/image")public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {ImageCode imageCode = createImageCode();sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());}/*** 功能描述: <br>* 〈生成验证码〉* @Param:* @Return:* @Author: shihengfei* @Date: 2020/4/30 16:59*/private ImageCode createImageCode() {int width = 100; // 验证码图片宽度int height = 36; // 验证码图片长度int length = 4; // 验证码位数int expireIn = 60; // 验证码有效时间 60sBufferedImage 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 < length; 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(), expireIn);}private Color getRandColor(int fc, int bc) {Random random = new Random();if (fc > 255) {fc = 255;}if (bc > 255) {bc = 255;}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);}}
二:登录页面新增验证码:
<input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/><img src="/code/image"/>
因为 code/image 接口需要无权限访问,配置放开:
.antMatchers("/login.html","/code/image").permitAll()
三:登录流程添加验证码校验
1. 定义验证码异常处理类
在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等,所以我们定义一个验证码类型的异常类:
import org.springframework.security.core.AuthenticationException;/*** <p>* Description:[验证码校验异常]* </p>** @author shf* @version 1.0* @date Created on 2020/4/30 17:11*/public class ValidateCodeException extends AuthenticationException {private static final long serialVersionUID = 5022575393500654458L;ValidateCodeException(String message) {super(message);}}
2. 定义验证码验证过滤器
Spring Security实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后采去校验用户名和密码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter:
package com.springboot.securityone.filter;import com.springboot.securityone.exception.ValidateCodeException;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import org.springframework.social.connect.web.HttpSessionSessionStrategy;import org.springframework.social.connect.web.SessionStrategy;import org.springframework.stereotype.Component;import org.springframework.web.bind.ServletRequestBindingException;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 java.io.IOException;/*** <p>* Description:[验证码过滤器]* </p>** @author shf* @version 1.0* @date Created on 2020/4/30 17:14*/@Componentpublic class ValidateCodeFilter extends OncePerRequestFilter {@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,FilterChain filterChain) throws ServletException, IOException {if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI())&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {try {validateCode(new ServletWebRequest(httpServletRequest));} catch (ValidateCodeException e) {authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}filterChain.doFilter(httpServletRequest, httpServletResponse);}/*** 功能描述: <br>* 〈校验验证码〉* @Param:* @Return:* @Author: shihengfei* @Date: 2020/4/30 17:16*/private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {// 校验}}
ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会执行一次。
在doFilterInternal方法中我们判断了请求URL是否为/login,该路径对应登录form表单的action路径,请求的方法是否为POST,是的话进行验证码校验逻辑,否则直接执行filterChain.doFilter让代码往下走。当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器AuthenticationFailureHandler进行处理。
3. validateCode的校验逻辑
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {// 校验ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");if (StringUtils.isBlank(codeInRequest)) {throw new ValidateCodeException("验证码不能为空!");}if (codeInSession == null) {throw new ValidateCodeException("验证码不存在!");}if (codeInSession.isExpire()) {sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);throw new ValidateCodeException("验证码已过期!");}if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {throw new ValidateCodeException("验证码不正确!");}sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);}
从 SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); 中获取验证码信息。校验。
4.添加过滤器到 UsernamePasswordAuthenticationFilter 过滤器之前。
配置文件新增:
@Autowiredprivate ValidateCodeFilter validateCodeFilter;......http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器.formLogin()
注入ValidateCodeFilter,然后通过addFilterBefore方法将ValidateCodeFilter验证码校验过滤器添加到了UsernamePasswordAuthenticationFilter前面。
