(附代码)手写滑动验证码,完整代码开放

目录

一、概述

二、原理分析    

三、代码实现

四、测试验证

五、总结


一、概述

滑动验证码在很多网站流行,一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。当然到目前为止,没有绝对的安全验证,只是不断增加攻击者的绕过成本。

二、原理分析    

接下来分析下滑动验证码的核心流程:

  1. 后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标
  2. 前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值,比如上述示例
  3. 前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。

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

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

三、代码实现

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

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

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

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

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

示例代码如下:

	private int[][] getBlockData() {
		int[][] data = new int[targetLength][targetWidth];
		double x2 = targetLength-circleR-2;
		//随机生成圆的位置
		double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);
		double po = circleR*circleR;
		
		double xbegin = targetLength-circleR-r1;
		double ybegin = targetWidth-circleR-r1;
		
		for (int i = 0; i < targetLength; i++) {
			for (int j = 0; j < targetWidth; j++) {
				//右边○
				double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);
				
				if (d1 <= po
						|| (j >= ybegin && d2 >= po)
						|| (i >= xbegin && d3 >= po)
						) {
					data[i][j] = 0;
					
				}  else {
					data[i][j] = 1;
				}
			}
		}
		return data;
	}

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

    操作如下:

private void cutByTemplate(BufferedImage oriImage,BufferedImage targetImage, int[][] templateImage, int x,
			int y){
		for (int i = 0; i < targetLength; i++) {
			for (int j = 0; j < targetWidth; j++) {
				int rgb = templateImage[i][j];
				// 原图中对应位置变色处理
				
				int rgb_ori = oriImage.getRGB(x + i, y + j);
				
				if (rgb == 1) {
                    //抠图上复制对应颜色值
					targetImage.setRGB(i, y + j, rgb_ori);
					int r = (0xff & rgb_ori);
					int g = (0xff & (rgb_ori >> 8));
					int b = (0xff & (rgb_ori >> 16)));
					rgb_ori = r + (g << 8) + (b << 16) + (200 << 24);
                    //原图对应位置颜色变化
					oriImage.setRGB(x + i, y + j, rgb_ori);
				} 
			}
		}
	}

    经过前面两步后,就得到了抠图和带抠图阴影的原图。为增加混淆和提高网络加载效果,还需要对图片做进一步处理。一般有两件事需要做,一对图片做模糊处理增加机器识别难度,二做适当同质量压缩。模糊处理很容易想到高斯模糊,原理很好理解,大家可以去 google 了解下。具体到 Java 里面的实现,有很多版本,现在不借助任何第三方 jar,提供一个示例:

public static ConvolveOp getGaussianBlurFilter(int radius,
            boolean horizontal) {
        if (radius < 1) {
            throw new IllegalArgumentException("Radius must be >= 1");
        }
        
        int size = radius * 2 + 1;
        float[] data = new float[size];
        
        float sigma = radius / 3.0f;
        float twoSigmaSquare = 2.0f * sigma * sigma;
        float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
        float total = 0.0f;
        
        for (int i = -radius; i <= radius; i++) {
            float distance = i * i;
            int index = i + radius;
            data[index] = (float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
            total += data[index];
        }
        
        for (int i = 0; i < data.length; i++) {
            data[i] /= total;
        }        
        
        Kernel kernel = null;
        if (horizontal) {
            kernel = new Kernel(size, 1, data);
        } else {
            kernel = new Kernel(1, size, data);
        }
        return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
    }

public static void simpleBlur(BufferedImage src,BufferedImage dest) {
		    BufferedImageOp op = getGaussianBlurFilter(2,false);
		    op.filter(src, dest);
	}

    经测试模糊效果很不错。另外是图片压缩,也不借助任何第三方工具,做同质压缩。

public static byte[] fromBufferedImage2(BufferedImage img,String imagType) throws IOException {
		bos.reset();
		// 得到指定Format图片的writer
		Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(imagType);
		ImageWriter writer = (ImageWriter) iter.next(); 

		// 得到指定writer的输出参数设置(ImageWriteParam )
		ImageWriteParam iwp = writer.getDefaultWriteParam();
		iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // 设置可否压缩
		iwp.setCompressionQuality(1f); // 设置压缩质量参数

		iwp.setProgressiveMode(ImageWriteParam.MODE_DISABLED);

		ColorModel colorModel = ColorModel.getRGBdefault();
		// 指定压缩时使用的色彩模式
		iwp.setDestinationType(new javax.imageio.ImageTypeSpecifier(colorModel,
		colorModel.createCompatibleSampleModel(16, 16)));
		
		writer.setOutput(ImageIO
		.createImageOutputStream(bos));
		IIOImage iIamge = new IIOImage(img, null, null);
		writer.write(null, iIamge, iwp);
		
		byte[] d = bos.toByteArray();
		return d;
	}

    至此,滑动验证码核心的代码处理流程已全部结束,这里面有很多细节可以不断打磨优化,让滑动体验可以变得更好。希望可以帮助到某些准备自己构建滑动验证码的同学。

四、测试验证

简单测试代码如下:固定输入距离值200,检验结果。

    public static void main(String[] args) throws InterruptedException {
        log.info("Go");
        SwipeCaptchaService scs = new SwipeCaptchaServiceImpl();

        String channel = "user-login";
        String uid = "u123456";
        UserCaptcha uc = scs.getCaptcha(channel, uid, System.currentTimeMillis());

        Thread.sleep(1000);
//        Thread.sleep(5000);
        CaptchaResult cr = scs.verifyCaptcha(channel, uid, uc.getUuid(), "200");

        log.info("验证结果,code:{},msg:{}",cr.getCode(),cr.getMsg());

        cr = scs.verifyCaptcha(channel, uid, uc.getUuid(), "200");

        log.info("重复验证结果,code:{},msg:{}",cr.getCode(),cr.getMsg());
    }

为方便本地测试,代码中将误差值放大到100px,验证有效期为5s,一方面增大成功率,一方面模拟有效期。

1. 验证不通过场景,模拟滑动位置不匹配:输入200距离,误差是113,超过误差阈值100

23:37:24  [ com.echx.captcha.service.SwipeCaptchaServiceImpl ] - [ INFO ]  Generated captcha distance:313, uid:u123456, uuid:714867ee-2b91-484e-aaa4-2b0f6e4a11d1
23:37:25  [ com.echx.captcha.bean.CachedCaptcha ] - [ INFO ]  uuid:714867ee-2b91-484e-aaa4-2b0f6e4a11d1,滑动误差值:113
23:37:25  [ com.echx.captcha.main.AppMain ] - [ INFO ]  验证结果,code:-1,msg:验证失败
23:37:25  [ com.echx.captcha.main.AppMain ] - [ INFO ]  重复验证结果,code:-1,msg:验证失败

2. 验证通过场景,滑动距离准确,不能二次验证:

23:41:06  [ com.echx.captcha.service.SwipeCaptchaServiceImpl ] - [ INFO ]  Generated captcha distance:202, uid:u123456, uuid:75baf690-8e66-4115-843c-dc29ca208a4b
23:41:07  [ com.echx.captcha.bean.CachedCaptcha ] - [ INFO ]  uuid:75baf690-8e66-4115-843c-dc29ca208a4b,滑动误差值:2
23:41:07  [ com.echx.captcha.main.AppMain ] - [ INFO ]  验证结果,code:0,msg:验证通过
23:41:07  [ com.echx.captcha.main.AppMain ] - [ INFO ]  重复验证结果,code:-1,msg:验证失败

3. 验证不通过场景,Thread.sleep(5000),  过期:

23:49:44  [ com.echx.captcha.service.SwipeCaptchaServiceImpl ] - [ INFO ]  Generated captcha distance:156, uid:u123456, uuid:ace36c66-5592-4669-8544-b8230551fb68
23:49:50  [ com.echx.captcha.bean.CachedCaptcha ] - [ INFO ]  uuid:ace36c66-5592-4669-8544-b8230551fb68,滑动误差值:44
23:49:50  [ com.echx.captcha.service.SwipeCaptchaServiceImpl ] - [ WARN ]  验证过期:user-login,u123456,ace36c66-5592-4669-8544-b8230551fb68,200,1652197790957
23:49:50  [ com.echx.captcha.main.AppMain ] - [ INFO ]  验证结果,code:-2,msg:验证过期
23:49:50  [ com.echx.captcha.main.AppMain ] - [ INFO ]  重复验证结果,code:-1,msg:验证失败

五、总结

以上代码实现都非常的精炼,一方面为了保证性能,另一方面好理解。完整代码在GitHub-techingon/swipe-captcha中开放,如遇疑问,可留言交流。经测试,该生成滑动图形的流程响应时间可以控制在 20ms 左右,如果原图分辨率在 300px*150px 以下,可以到 10ms 左右,在可接受范围内。

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,以下是手写数字识别完整代码带注释,使用的是Python和Keras框架: ```python # 导入所需的库和模块 import keras from keras.datasets import mnist from keras.models import Sequential from keras.layers import Dense, Dropout from keras.optimizers import RMSprop # 加载MNIST数据集并将其分为训练集和测试集 (x_train, y_train), (x_test, y_test) = mnist.load_data() # 将训练集和测试集的像素值从0到255的范围缩放到0到1的范围内 x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 # 将标签转换为独热编码形式 y_train = keras.utils.to_categorical(y_train, 10) y_test = keras.utils.to_categorical(y_test, 10) # 创建模型,包含两个隐藏层和一个输出层 model = Sequential() model.add(Dense(512, activation='relu', input_shape=(784,))) model.add(Dropout(0.2)) model.add(Dense(512, activation='relu')) model.add(Dropout(0.2)) model.add(Dense(10, activation='softmax')) # 输出模型的结构信息 model.summary() # 编译模型,使用交叉熵损失函数和RMSprop优化器 model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy']) # 训练模型并在测试集上进行评估 history = model.fit(x_train, y_train, batch_size=128, epochs=20, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1]) ``` 注释中已经解释了每个步骤的作用,这个代码可以训练一个具有两个隐藏层和一个输出层的神经网络模型,用于识别手写数字。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechingOn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值