侧边栏壁纸
博主头像
小小酥心

旧书不厌百回读,熟读精思子自知💪

  • 累计撰写 22 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Java验证码图片生成

小小酥心
2022-09-22 / 0 评论 / 0 点赞 / 1,250 阅读 / 3,807 字
温馨提示:
本文最后更新于 2022-09-22,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Java生成验证码图片

Java原生的awt包生成

public static void createImage() {
    // 1 创建一张空图片,并且指定宽高。 理解为:创建一张画纸
    BufferedImage image = new BufferedImage(70, 30, BufferedImage.TYPE_INT_RGB);

    // 2 根据图片获取一个画笔,通过该画笔画的内容都会画到该图片上
    Graphics g = image.getGraphics();
    // 用于生成随机数(随机数位line的字符下标)
    Random random = new Random();
    // 为图片背景填充一个随机颜色
    // 创建Color时,需要指定三个参数,分别是,红,绿,蓝。数字范围都是(0-255)之间
    Color bgcolor = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
    // 将画笔设置为该颜色
    g.setColor(bgcolor);
    // 填充整张图片为画笔当前颜色
    g.fillRect(0, 0, 70, 30);

    // 3 确定验证码内容(字母与数字的组合)
    String line = "abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    // 向图片上画4个字符
    for (int i = 0; i < 4; i++) {
        //随机生成一个字符
        String str = line.charAt(random.nextInt(line.length())) + "";
        //生成随机颜色
        Color color = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
        //设置画笔颜色
        g.setColor(color);
        //设置字体
        g.setFont(new Font(null, Font.BOLD, 20));
        //将字符串画到图片指定的位置上
        g.drawString(str, i * 15 + 5, 18 + random.nextInt(11) - 5);
    }

    //随机生成4条干扰线
    for (int i = 0; i < 4; i++) {
        Color color = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
        g.setColor(color);
        g.drawLine(random.nextInt(71), random.nextInt(31),
            random.nextInt(71), random.nextInt(31));
    }

    //将图片写入文件来生成该图片文件
    try {
        ImageIO.write(image, "jpg", new FileOutputStream("./random.jpg"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

利用Google的kaptcha包生成

引入依赖包
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
配置样式

com.google.code.kaptcha包下Constants类中有以下常量:

常量名 描述 默认
KAPTCHA_SESSION_KEY The value for the kaptcha is generated and is put into the HttpSession. This is the key value for that item in the session. KAPTCHA_SESSION_KEY
KAPTCHA_SESSION_DATE The date the kaptcha is generated is put into the HttpSession. This is the key value for that item in the session. KAPTCHA_SESSION_DATE
KAPTCHA_SESSION_CONFIG_KEY
KAPTCHA_SESSION_CONFIG_DATE
KAPTCHA_BORDER Border around kaptcha. Legal values are yes or no. yes
KAPTCHA_BORDER_COLOR Color of the border. Legal values are r,g,b (and optional alpha) or white,black,blue. black
KAPTCHA_BORDER_THICKNESS Thickness of the border around kaptcha. Legal values are > 0. 1
KAPTCHA_NOISE_COLOR The noise color. Legal values are r,g,b. black
KAPTCHA_NOISE_IMPL The noise producer. com.google.code.kaptcha.impl.DefaultNoise
KAPTCHA_OBSCURIFICATOR_IMPL The obscurificator implementation. com.google.code.kaptcha.impl.WaterRipple
KAPTCHA_PRODUCER_IMPL The image producer. com.google.code.kaptcha.impl.DefaultKaptcha
KAPTCHA_TEXTPRODUCER_IMPL The text producer. com.google.code.kaptcha.text.impl.DefaultTextCreator
KAPTCHA_TEXTPRODUCER_CHAR_STRING The characters that will create the kaptcha. abcde2345678gfynmnpwx
KAPTCHA_TEXTPRODUCER_CHAR_LENGTH The number of characters to display. 5
KAPTCHA_TEXTPRODUCER_FONT_NAMES A list of comma separated font names. Arial, Courier
KAPTCHA_TEXTPRODUCER_FONT_COLOR The color to use for the font. Legal values are r,g,b. black
KAPTCHA_TEXTPRODUCER_FONT_SIZE The size of the font to use. 40px.
KAPTCHA_TEXTPRODUCER_CHAR_SPACE The space between the characters 2
KAPTCHA_WORDRENDERER_IMPL
KAPTCHA_BACKGROUND_IMPL The background implementation. com.google.code.kaptcha.impl.DefaultBackground
KAPTCHA_BACKGROUND_CLR_FROM Starting background color. Legal values are r,g,b. light grey
KAPTCHA_BACKGROUND_CLR_TO Ending background color. Legal values are r,g,b. white
KAPTCHA_IMAGE_WIDTH Width in pixels of the kaptcha image. 200
KAPTCHA_IMAGE_HEIGHT Height in pixels of the kaptcha image. 50

示例:

字符串验证码
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
数学运算验证码
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
自定义文本生成器
public class KaptchaTextCreator extends DefaultTextCreator {
    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");

    @Override
    public String getText() {
        int result = 0;
        // 强随机数生成器
        Random random = new SecureRandom();
        int x = random.nextInt(10);
        int y = random.nextInt(10);
        StringBuilder suChinese = new StringBuilder();
        int randomoperands = (int) Math.round(Math.random() * 2);
        if(randomoperands == 0) {
            result = x * y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("*");
            suChinese.append(CNUMBERS[y]);
        } else if(randomoperands == 1) {
            if(!(x == 0) && y % x == 0) {
                result = y / x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("/");
                suChinese.append(CNUMBERS[x]);
            } else {
                result = x + y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("+");
                suChinese.append(CNUMBERS[y]);
            }
        } else if(randomoperands == 2) {
            if(x >= y) {
                result = x - y;
                suChinese.append(CNUMBERS[x]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[y]);
            } else {
                result = y - x;
                suChinese.append(CNUMBERS[y]);
                suChinese.append("-");
                suChinese.append(CNUMBERS[x]);
            }
        } else {
            result = x + y;
            suChinese.append(CNUMBERS[x]);
            suChinese.append("+");
            suChinese.append(CNUMBERS[y]);
        }
        suChinese.append("=?@" + result);
        return suChinese.toString();
    }
}
生成文字验证码
String code = defaultKaptcha.createText();
BufferedImage image = defaultKaptcha.createImage(code)
ImageIO.write(image, "jpg", out)

// ============================================
    
// 运算验证码需在获取text后处理
String capText = defaultKaptcha.createText();
String capStr = capText.substring(0, capText.lastIndexOf("@"));
String code = capText.substring(capText.lastIndexOf("@") + 1);
BufferedImage image = producer.createImage(capStr);
ImageIO.write(image, "png", out);

滑块拼图验证码

实现原理

1、后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标
2、前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值,比如以下示例

3、前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。

这里单纯校验用户滑动距离是最基本的校验,出于更高的安全考虑,可能还会考虑用户滑动的整个轨迹,用户在当前页面的访问行为等。这些可以很复杂,甚至借助到用户行为数据分析模型,最终的目标都是增加非法的模拟和绕过的难度。这些有机会可以再归纳总结常用到的方法,本文重点集中在如何基于Java来一步步实现滑动验证码的生成。

可以看到,滑动图形验证码,重要有两个图片组成,抠块和带有抠块阴影的原图,这里面有两个重要特性保证被暴力破解的难度:抠块的形状随机和抠块所在原图的位置随机。这样就可以在有限的图集中制造出随机的、无规律可寻的抠图和原图的配对。

用代码如何从一张大图中抠出一个有特定随机形状的小图呢?

步骤与代码

第一步,先确定一个抠出图的轮廓,方便后续真正开始执行图片处理操作

图片是有像素组成,每个像素点对应一种颜色,颜色可以用RGB形式表示,外加一个透明度,把一张图理解成一个平面图形,左上角为原点,向右x轴,向下y轴,一个坐标值对应该位置像素点的颜色,这样就可以把一张图转换成一个二维数组。基于这个考虑,轮廓也用二维数组来表示,轮廓内元素值为1,轮廓外元素值对应0。

这时候就要想这个轮廓形状怎么生成了。有坐标系、有矩形、有圆形,没错,用到数学的图形函数。典型用到一个圆的函数方程和矩形的边线的函数,类似:

(x-a)²+(y-b)²=r²中,有三个参数a、b、r,即圆心坐标为(a,b),半径r。这些将抠图放在上文描述的坐标系上很容易就图算出来具体的值。

static int targetWidth = 55;//小图长
static int targetHeight = 45;//小图宽
static int circleR = 8;//半径
static int r1 = 4;//距离点
 
/**
 * 生成小图轮廓
 */
private static int[][] getBlockData() {
    int[][] data = new int[targetWidth][targetHeight];
    double x2 = targetWidth -circleR; //47

    //随机生成圆的位置
    double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);
    double po = Math.pow(circleR,2); //64

    double xbegin = targetWidth - circleR - r1;
    double ybegin = targetHeight- circleR - r1;

    //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
    //计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
    for (int i = 0; i < targetWidth; i++) {
        for (int j = 0; j < targetHeight; j++) {
            double d2 = Math.pow(j - 2,2) + Math.pow(i - h1,2);
            double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);
            if ((j <= ybegin && d2 < po)||(i >= xbegin && d3 > po)) {
                data[i][j] = 0;
            }  else {
                data[i][j] = 1;
            }
        }
    }
    return data;
}

第二步,有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影。

/**
 * 有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影
 * @param oriImage  原图
 * @param targetImage  抠图拼图
 * @param templateImage 颜色
 * @param x
 * @param y void
 */
private static void cutByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] templateImage, int x, int y){
    int[][] martrix = new int[3][3];
    int[] values = new int[9];
    //创建shape区域
    for (int i = 0; i < targetWidth; i++) {
        for (int j = 0; j < targetHeight; j++) {
            int rgb = templateImage[i][j];
            // 原图中对应位置变色处理
            int rgb_ori = oriImage.getRGB(x + i, y + j);

            if (rgb == 1) {
                targetImage.setRGB(i, j, rgb_ori);

                //抠图区域高斯模糊
                readPixel(oriImage, x + i, y + j, values);
                fillMatrix(martrix, values);
                oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
            }else{
                //这里把背景设为透明
                targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
            }
        }
    }
}


private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
    int xStart = x - 1;
    int yStart = y - 1;
    int current = 0;
    for (int i = xStart; i < 3 + xStart; i++)
        for (int j = yStart; j < 3 + yStart; j++) {
            int tx = i;
            if (tx < 0) {
                tx = -tx;

            } else if (tx >= img.getWidth()) {
                tx = x;
            }
            int ty = j;
            if (ty < 0) {
                ty = -ty;
            } else if (ty >= img.getHeight()) {
                ty = y;
            }
            pixels[current++] = img.getRGB(tx, ty);

        }
}

private static void fillMatrix(int[][] matrix, int[] values) {
    int filled = 0;
    for (int i = 0; i < matrix.length; i++) {
        int[] x = matrix[i];
        for (int j = 0; j < x.length; j++) {
            x[j] = values[filled++];
        }
    }
}

private static int avgMatrix(int[][] matrix) {
    int r = 0;
    int g = 0;
    int b = 0;
    for (int i = 0; i < matrix.length; i++) {
        int[] x = matrix[i];
        for (int j = 0; j < x.length; j++) {
            if (j == 1) {
                continue;
            }
            Color c = new Color(x[j]);
            r += c.getRed();
            g += c.getGreen();
            b += c.getBlue();
        }
    }
    return new Color(r / 8, g / 8, b / 8).getRGB();
}

经过前面两步后,就得到了抠图和带高斯模糊抠图阴影的原图。返回生成的抠图和带阴影的大图base64码及抠图坐标。

/**
 * 读取本地图片,生成拼图验证码
 * @return Map<String,Object>  返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
 */
public static Map<String,Object> createImage(File file, Map<String,Object> resultMap){
    try {
        BufferedImage oriImage = ImageIO.read(file);
        Random random = new Random();
        //X轴距离右端targetWidth  Y轴距离底部targetHeight以上
        int widthRandom = random.nextInt(oriImage.getWidth()-  2*targetWidth) + targetWidth;
        int heightRandom = random.nextInt(oriImage.getHeight()- targetHeight);
        logger.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",oriImage.getWidth(),oriImage.getHeight(),widthRandom,heightRandom);

        BufferedImage targetImage= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
        cutByTemplate(oriImage,targetImage,getBlockData(),widthRandom,heightRandom);

        resultMap.put("bigImage", getImageBASE64(oriImage));//大图
        resultMap.put("smallImage", getImageBASE64(targetImage));//小图
        resultMap.put("xWidth",widthRandom);
        resultMap.put("yHeight",heightRandom);
    } catch (Exception e) {
        logger.info("创建图形验证码异常",e);
    } finally{
        return resultMap;
    }
}

/**
* 读取网络图片,生成拼图验证码
* @return Map<String,Object>  返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
*/
public static Map<String,Object> createImage(String imgUrl, Map<String,Object> resultMap){
	try {
		//通过URL 读取图片
		URL url = new URL(imgUrl);
		BufferedImage bufferedImage = ImageIO.read(url.openStream());
		Random rand = new Random();
		int widthRandom = rand.nextInt(bufferedImage.getWidth()-  targetWidth - 100 + 1 ) + 100;
		int heightRandom = rand.nextInt(bufferedImage.getHeight()- targetHeight + 1 );
		logger.info("原图大小{} x {},随机生成的坐标 X,Y 为({},		{})",bufferedImage.getWidth(),bufferedImage.getHeight(),widthRandom,heightRandom);

		BufferedImage target= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
		cutByTemplate(bufferedImage,target,getBlockData(),widthRandom,heightRandom);
		resultMap.put("bigImage", getImageBASE64(bufferedImage));//大图
		resultMap.put("smallImage", getImageBASE64(target));//小图
		resultMap.put("xWidth",widthRandom);
		resultMap.put("yHeight",heightRandom);
	} catch (Exception e) {
		logger.info("创建图形验证码异常",e);
	} finally{
		return resultMap;
	}
}
 
/**
 * 图片转BASE64
 * @param image
 * @return
 * @throws IOException String
 */
 public static String getImageBASE64(BufferedImage image) throws IOException {
 	byte[] imagedata = null;
 	ByteArrayOutputStream bao=new ByteArrayOutputStream();
 	ImageIO.write(image,"png",bao);
	imagedata=bao.toByteArray();
	BASE64Encoder encoder = new BASE64Encoder();
	String BASE64IMAGE=encoder.encodeBuffer(imagedata).trim();
	BASE64IMAGE = BASE64IMAGE.replaceAll("\r|\n", "");  //删除 \r\n
	return BASE64IMAGE;
 }

控制层代码实现及校验验证码:

/**
 * 生成滑块拼图验证码
 * @return BaseRestResult 返回类型
 */
@RequestMapping(value = "/getImageVerifyCode.do", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
public BaseRestResult getImageVerifyCode() {
    Map<String, Object> resultMap = new HashMap<>();
    //读取本地路径下的图片,随机选一条
    File file = new File(this.getClass().getResource("/image").getPath());
    File[] files = file.listFiles();
    int n = new Random().nextInt(files.length);
    File imageUrl = files[n];
    ImageUtil.createImage(imageUrl, resultMap);

    //读取网络图片
    //ImageUtil.createImage("/7986d66f29bfeb6015aaaec33d33fcd1d875ca16316f-2bMHNG_fw658",resultMap);
    session.setAttribute("xWidth", resultMap.get("xWidth"));
    resultMap.remove("xWidth");
    resultMap.put("errcode", 0);
    resultMap.put("errmsg", "success");
    return new BaseRestResult(resultMap);
}


/**
 * 校验滑块拼图验证码
 *
 * @param moveLength 移动距离
 * @return BaseRestResult 返回类型
 */
@RequestMapping(value = "/verifyImageCode.do", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
public BaseRestResult verifyImageCode(@RequestParam(value = "moveLength") String moveLength) {
    Double dMoveLength = Double.valueOf(moveLength);
    Map<String, Object> resultMap = new HashMap<>();
    try {
        Integer xWidth = (Integer) session.getAttribute("xWidth");
        if (xWidth == null) {
            resultMap.put("errcode", 1);
            resultMap.put("errmsg", "验证过期,请重试");
            return new BaseRestResult(resultMap);
        }
        if (Math.abs(xWidth - dMoveLength) > 10) {
            resultMap.put("errcode", 1);
            resultMap.put("errmsg", "验证不通过");
        } else {
            resultMap.put("errcode", 0);
            resultMap.put("errmsg", "验证通过");
        }
    } catch (Exception e) {
        throw new EsServiceException(e.getMessage());
    } finally {
        session.removeAttribute("xWidth");
    }
    return new BaseRestResult(resultMap);
}
0

评论区