import com.zx.silverfox.common.exception.GlobalException; import com.zx.silverfox.common.properties.ValidateCodeProperties; import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor; import com.zx.silverfox.common.validate.code.ValidateCodeRepository; import com.zx.silverfox.common.validate.code.ValidateCodeType; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.request.ServletWebRequest; import javax.imageio.ImageIO; import java.io.IOException; /** @author zouwei */ public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor<ImageValidateCode> { /** 生成的图片的格式 */ private static final String JPEG_IMAGE_TYPE = "JPEG"; public ImageValidateCodeProcessor( @Autowired ValidateCodeProperties validateCodeProperties, @Autowired ValidateCodeRepository repository) { super( new ImageValidateCodeGenerator(validateCodeProperties.getImage()), repository, validateCodeProperties.getImage()); } @Override protected ValidateCodeType getValidateCodeType() { return ValidateCodeType.IMAGE; } @Override protected void send(ServletWebRequest request, ImageValidateCode validateCode) throws GlobalException { try { ImageIO.write( validateCode.getImage(), JPEG_IMAGE_TYPE, request.getResponse().getOutputStream()); } catch (IOException e) { throw GlobalException.newInstance("IMAGE_CODE_CREATE_FAIL", "图片验证码生成失败"); } } @Override protected boolean validate(String code, ImageValidateCode validateCode) { return StringUtils.equalsIgnoreCase(code, validateCode.getCode()); } } 复制代码
滑块验证码:
import com.zx.silverfox.common.validate.code.ValidateCode; import lombok.Data; import lombok.EqualsAndHashCode; /** @author zouwei */ @Data @EqualsAndHashCode(callSuper = true) public class SlideImageCode extends ValidateCode { private double heightYPercentage; private transient String srcImg; private transient String markImg; public SlideImageCode( double heightYPercentage, String srcImg, String markImg, String code, long expireInSeconds) { super(code, expireInSeconds); this.heightYPercentage = heightYPercentage; this.srcImg = srcImg; this.markImg = markImg; } } 复制代码
import com.zx.silverfox.common.properties.ValidateCodeProperties; import com.zx.silverfox.common.validate.code.ValidateCode; import com.zx.silverfox.common.validate.code.ValidateCodeGenerator; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; import org.springframework.web.context.request.ServletWebRequest; import javax.imageio.ImageIO; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Random; @Slf4j public class SlideImageCodeGenerator implements ValidateCodeGenerator { private ValidateCodeProperties.SlideImageProperties slideImageProperties; public SlideImageCodeGenerator( ValidateCodeProperties.SlideImageProperties slideImageProperties) { this.slideImageProperties = slideImageProperties; } @Override public ValidateCode createValidateCode(ServletWebRequest request) { try (InputStream in = getOriginImage()) { SlideImageUtil.SlideImage slideImage = SlideImageUtil.getVerifyImage(ImageIO.read(in)); int width = slideImage.getWidth(); int x = slideImage.getX(); int height = slideImage.getHeight(); int y = slideImage.getY(); double widthXPercentage = width / (x * 1.0); double heightYPercentage = height / (y * 1.0); String code = widthXPercentage + ":" + heightYPercentage; return new SlideImageCode( heightYPercentage, slideImage.getSrcImg(), slideImage.getMarkImg(), code, slideImageProperties.getExpiredInSecond()); } catch (IOException e) { e.printStackTrace(); } return null; } private InputStream getOriginImage() throws IOException { // 从resources下的slideimg文件夹中随机获取一张图片进行处理 ClassPathResource classPathResource = new ClassPathResource("slideimg"); File dirFile = classPathResource.getFile(); File[] listFiles = dirFile.listFiles(); int index = new Random().nextInt(listFiles.length); return new FileInputStream(listFiles[index]); } } 复制代码
import com.zx.silverfox.common.exception.GlobalException; import com.zx.silverfox.common.properties.ValidateCodeProperties; import com.zx.silverfox.common.util.CastUtil; import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor; import com.zx.silverfox.common.validate.code.ValidateCodeRepository; import com.zx.silverfox.common.validate.code.ValidateCodeType; import com.zx.silverfox.common.vo.CommonResponse; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.context.request.ServletWebRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 滑动验证码 * * @author zouwei */ public class SlideImageCodeProcessor extends AbstractValidateCodeProcessor<SlideImageCode> { public SlideImageCodeProcessor( @Autowired ValidateCodeProperties validateCodeProperties, @Autowired ValidateCodeRepository validateCodeRepository) { super( new SlideImageCodeGenerator(validateCodeProperties.getSlide()), validateCodeRepository, validateCodeProperties.getSlide()); } @Override protected ValidateCodeType getValidateCodeType() { return ValidateCodeType.SLIDE; } @Override protected void send(ServletWebRequest request, SlideImageCode validateCode) throws GlobalException { double heightY = validateCode.getHeightYPercentage(); try { HttpServletResponse response = request.getResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getOutputStream() .write( CommonResponse.successInstance( new SlideValidateCodeImage( heightY, validateCode.getSrcImg(), validateCode.getMarkImg())) .toJson() .getBytes()); } catch (IOException e) { throw GlobalException.newInstance("", "图片验证码生成失败"); } } /** * 滑动验证码验证 * * @param code * @param validateCode * @return */ @Override protected boolean validate(String code, SlideImageCode validateCode) { try { String[] location = StringUtils.splitByWholeSeparatorPreserveAllTokens(code, ":"); double x1 = CastUtil.castDouble(location[0]); double y1 = CastUtil.castDouble(location[1]); String sessionCode = validateCode.getCode(); String[] sessionLocation = StringUtils.splitByWholeSeparatorPreserveAllTokens(sessionCode, ":"); double x2 = CastUtil.castDouble(sessionLocation[0]); double y2 = CastUtil.castDouble(sessionLocation[1]); double distance = Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2)); return distance < 0.06; } catch (Exception e) { return false; } } @Data @NoArgsConstructor @AllArgsConstructor private static class SlideValidateCodeImage { private double heightY; private String srcImg; private String markImg; } } 复制代码
滑块处理工具类:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.util.Base64Utils; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * @author zouwei */ public final class SlideImageUtil { private static final String IMAGE_TYPE = "png"; /** 源文件宽度 */ private static int ORI_WIDTH = 300; /** 源文件高度 */ private static int ORI_HEIGHT = 150; /** 模板图宽度 */ private static int CUT_WIDTH = 50; /** 模板图高度 */ private static int CUT_HEIGHT = 50; /** 抠图凸起圆心 */ private static int circleR = 5; /** 抠图内部矩形填充大小 */ private static int RECTANGLE_PADDING = 8; /** 抠图的边框宽度 */ private static int SLIDER_IMG_OUT_PADDING = 1; @Data @AllArgsConstructor @NoArgsConstructor public static class SlideImage { /** 底图 */ private String srcImg; /** 标记图片 */ private String markImg; /** x轴 */ private int x; /** y轴 */ private int y; /** 原图的宽度 */ private int width; /** 原图的高度 */ private int height; } /** * 根据传入的路径生成指定验证码图片 * * @param originImage * @return * @throws IOException */ public static SlideImage getVerifyImage(BufferedImage originImage) throws IOException { int width = originImage.getWidth(); int height = originImage.getHeight(); int locationX = CUT_WIDTH + new Random().nextInt(width - CUT_WIDTH * 3); int locationY = CUT_HEIGHT + new Random().nextInt(height - CUT_HEIGHT) / 2; BufferedImage markImage = new BufferedImage(CUT_WIDTH, CUT_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR); int[][] data = getBlockData(); cutImgByTemplate(originImage, markImage, data, locationX, locationY); return new SlideImage( getImageBASE64(originImage), getImageBASE64(markImage), locationX, locationY, width, height); } /** * 生成随机滑块形状 * * <p>0 透明像素 1 滑块像素 2 阴影像素 * * @return int[][] */ private static int[][] getBlockData() { int[][] data = new int[CUT_WIDTH][CUT_HEIGHT]; Random random = new Random(); // (x-a)²+(y-b)²=r² // x中心位置左右5像素随机 double x1 = RECTANGLE_PADDING + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0 - 5 + random.nextInt(10); // y 矩形上边界半径-1像素移动 double y1_top = RECTANGLE_PADDING - random.nextInt(3); double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3); double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom; double x2_right = CUT_WIDTH - RECTANGLE_PADDING - circleR + random.nextInt(2 * circleR - 4); double x2_left = RECTANGLE_PADDING + circleR - 2 - random.nextInt(2 * circleR - 4); double x2 = random.nextInt(2) == 1 ? x2_right : x2_left; double y2 = RECTANGLE_PADDING + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0 - 4 + random.nextInt(10); double po = Math.pow(circleR, 2); for (int i = 0; i < CUT_WIDTH; i++) { for (int j = 0; j < CUT_HEIGHT; j++) { // 矩形区域 boolean fill; if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING) && (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) { data[i][j] = 1; fill = true; } else { data[i][j] = 0; fill = false; } // 凸出区域 double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2); if (d3 < po) { data[i][j] = 1; } else { if (!fill) { data[i][j] = 0; } } // 凹进区域 double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2); if (d4 < po) { data[i][j] = 0; } } } // 边界阴影 for (int i = 0; i < CUT_WIDTH; i++) { for (int j = 0; j < CUT_HEIGHT; j++) { // 四个正方形边角处理 for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) { // 左上、右上 if (i >= RECTANGLE_PADDING - k && i < RECTANGLE_PADDING && ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING) || (j >= CUT_HEIGHT - RECTANGLE_PADDING - k && j < CUT_HEIGHT - RECTANGLE_PADDING + 1))) { data[i][j] = 2; } // 左下、右下 if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1 && i < CUT_WIDTH - RECTANGLE_PADDING + 1) { for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) { if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING) || (j >= CUT_HEIGHT - RECTANGLE_PADDING - n && j <= CUT_HEIGHT - RECTANGLE_PADDING))) { data[i][j] = 2; } } } } if (data[i][j] == 1 && j - SLIDER_IMG_OUT_PADDING > 0 && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) { data[i][j - SLIDER_IMG_OUT_PADDING] = 2; } if (data[i][j] == 1 && j + SLIDER_IMG_OUT_PADDING > 0 && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) { data[i][j + SLIDER_IMG_OUT_PADDING] = 2; } if (data[i][j] == 1 && i - SLIDER_IMG_OUT_PADDING > 0 && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) { data[i - SLIDER_IMG_OUT_PADDING][j] = 2; } if (data[i][j] == 1 && i + SLIDER_IMG_OUT_PADDING > 0 && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) { data[i + SLIDER_IMG_OUT_PADDING][j] = 2; } } } return data; } /** * 裁剪区块 根据生成的滑块形状,对原图和裁剪块进行变色处理 * * @param oriImage 原图 * @param targetImage 裁剪图 * @param blockImage 滑块 * @param x 裁剪点x * @param y 裁剪点y */ private static void cutImgByTemplate( BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) { for (int i = 0; i < CUT_WIDTH; i++) { for (int j = 0; j < CUT_HEIGHT; j++) { int _x = x + i; int _y = y + j; int rgbFlg = blockImage[i][j]; int rgb_ori = oriImage.getRGB(_x, _y); // 原图中对应位置变色处理 if (rgbFlg == 1) { // 抠图上复制对应颜色值 targetImage.setRGB(i, j, rgb_ori); // 原图对应位置颜色变化 oriImage.setRGB(_x, _y, Color.LIGHT_GRAY.getRGB()); } else if (rgbFlg == 2) { targetImage.setRGB(i, j, Color.WHITE.getRGB()); oriImage.setRGB(_x, _y, Color.GRAY.getRGB()); } else if (rgbFlg == 0) { // int alpha = 0; targetImage.setRGB(i, j, rgb_ori & 0x00ffffff); } } } } /** * 随机获取一张图片对象 * * @param path * @return * @throws IOException */ public static BufferedImage getRandomImage(String path) throws IOException { File files = new File(path); File[] fileList = files.listFiles(); List<String> fileNameList = new ArrayList<>(); if (fileList != null && fileList.length != 0) { for (File tempFile : fileList) { if (tempFile.isFile() && tempFile.getName().endsWith(".jpg")) { fileNameList.add(tempFile.getAbsolutePath().trim()); } } } Random random = new Random(); File imageFile = new File(fileNameList.get(random.nextInt(fileNameList.size()))); return ImageIO.read(imageFile); } /** * 将IMG输出为文件 * * @param image * @param file * @throws Exception */ public static void writeImg(BufferedImage image, String file) throws Exception { try (ByteArrayOutputStream bao = new ByteArrayOutputStream()) { ImageIO.write(image, IMAGE_TYPE, bao); FileOutputStream out = new FileOutputStream(new File(file)); out.write(bao.toByteArray()); } } /** * 将图片转换为BASE64 * * @param image * @return * @throws IOException */ public static String getImageBASE64(BufferedImage image) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { ImageIO.write(image, IMAGE_TYPE, out); // 生成BASE64编码 return Base64Utils.encodeToString(out.toByteArray()); } } /** * 将BASE64字符串转换为图片 * * @param base64String * @return */ public static BufferedImage base64StringToImage(String base64String) throws IOException { try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64Utils.decodeFromString(base64String))) { return ImageIO.read(bais); } } } 复制代码
短信验证码:
import com.zx.silverfox.common.validate.code.ValidateCode; public class SmsValidateCode extends ValidateCode { public SmsValidateCode(String code, long expireInSeconds) { super(code, expireInSeconds); } } 复制代码
import com.zx.silverfox.common.properties.ValidateCodeProperties; import com.zx.silverfox.common.validate.code.ValidateCodeGenerator; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.web.context.request.ServletWebRequest; /** @author zouwei */ public class SmsValidateCodeGenerator implements ValidateCodeGenerator { private ValidateCodeProperties.SmsProperties smsProperties; public SmsValidateCodeGenerator(ValidateCodeProperties.SmsProperties smsProperties) { this.smsProperties = smsProperties; } @Override public SmsValidateCode createValidateCode(ServletWebRequest request) { String code = RandomStringUtils.randomNumeric(smsProperties.getLength()); return new SmsValidateCode(code, smsProperties.getExpiredInSecond()); } } 复制代码
import com.zx.silverfox.common.exception.GlobalException; import com.zx.silverfox.common.properties.ValidateCodeProperties; import com.zx.silverfox.common.util.CastUtil; import com.zx.silverfox.common.util.SmsUtil; import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor; import com.zx.silverfox.common.validate.code.ValidateCodeRepository; import com.zx.silverfox.common.validate.code.ValidateCodeType; import com.zx.silverfox.common.vo.CommonResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.context.request.ServletWebRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** @author zouwei */ @Slf4j public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor<SmsValidateCode> { public SmsValidateCodeProcessor( @Autowired ValidateCodeProperties validateCodeProperties, @Autowired ValidateCodeRepository validateCodeRepository) { super( new SmsValidateCodeGenerator(validateCodeProperties.getSms()), validateCodeRepository, validateCodeProperties.getSms()); } @Override protected ValidateCodeType getValidateCodeType() { return ValidateCodeType.SMS; } @Override protected void send(ServletWebRequest request, SmsValidateCode validateCode) throws GlobalException { // 手机号码 String mobile = request.getParameter("mobile"); String type = request.getParameter("type"); if (StringUtils.isBlank(mobile) || StringUtils.isBlank(type)) { // 获取验证码参数没提供 throw GlobalException.newInstance( "SMS_VALIDATE_CODE_PARAM_ERROR", "没有给电话号码或者指明短信类型,无法发送短信"); } long minute = validateCode.minute(); SmsUtil.send( SmsUtil.SmsType.format(type), mobile, validateCode.getCode(), CastUtil.castString(minute)); HttpServletResponse response = request.getResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); try { response.getOutputStream().write(CommonResponse.successInstance().toJson().getBytes()); } catch (IOException e) { log.error("response.getOutputStream()出异常", e); } } @Override protected boolean validate(String code, SmsValidateCode validateCode) { return StringUtils.equalsIgnoreCase(code, validateCode.getCode()); } } 复制代码
注意事项:想要使用滑块验证码,需要在resources文件夹里面创建一个slideimg文件夹,并且把需要的图片放进去:
GlobalException是我自己设计的异常类,建议需要的小伙伴换成自己应用的异常类。
ok,整个验证码组件设计加上具体实现都已经完毕,下面就是如何使用:
首先,把自定义注解放在springboot项目启动类上:
建议按需配置,如果不需要图片验证码或者滑块验证码,可以不加载进来
然后就是配置文件:
比如你需要在发生短信验证码之前先触发滑块验证码,那么可以把"/code/sms"这个url放进validate.slide.filter-urls配置中。
好吧,怎么使用已经讲解完毕,配置文件中的其他配置参数包括图片的大小和验证码的位数等等,小伙伴可以根据自身需要去配置。