旋转图片验证码,一个为防止爬虫攻击的行为验证产品。它是由最初的字符验证码演变而来,与其相似的产品还有滑动拼图,文字点选,以及刮涂层等产品。
这些产品有很多共同点,它们的目标都是为了防止爬虫攻击,防止一些恶意的程序来爬取网站数据,或者恶意注册等等,它们都统称为人机校验程序。
当然这些所谓的人机校验程序也就是反爬攻城狮们为了对抗爬虫攻城狮迫于无奈而想出的这些招数。对于爬虫攻城狮来讲也破解就是时间问题,而对于用户来将,这些产品反而是降低了用户体验。
那么问题来了,旋转图片验证码到底如何破解呢?
结果:
实现原理分析:
首先说下图片库,经过不断抓取图片发现,这里的图片库基本上由几十张样本图,各自旋转360°的部分结果。那我们就
按照他们生成图片库的方法来生成样本数据,首先我们要通过某种方式来得到正确的样本数据,就是的到一张图和它对
应滑动的距离。这样就可以将这张图的360°样本图全部计算得出,从而得到模型匹配库。当样本库与他们的图片库数量
趋于一致时,基本上就可以达到95%以上的识别率了。
那么就有了以下几个问题:
1.第一个问题呢,目前还没有找到能够直接识别图片旋转角度的方法。所以我们就先通过计较笨的办法来得到样本数据。当然可以直接手动去滑动来得到样本数据和他的结果。这里比较懒,就写了一段程序去抓,毕竟手动去找样本数据还是很累的。既然来破解页面滑动那大家对selenium应该都很熟悉了,大多数人都用python,但是我这里用的是Java,其实大同小异了。这里就不放代码的,说下具体思路吧:
先给一个初始的距离,然后每次都从那个位置开始尝试,成功则记录结果,失败则先记下失败,下一次给一个偏动距离
再次尝试,直达所有距离都尝试过基本就有答案了,再没有就是这张图的问题了。本来想从左到右依次往上加的,但是
发现答案最多的地方基本都在正中间左右一点的位置,所以就改为从中间开始,向两边依次加减,左右轮回,这样更容
易得到正确结果。
这里补充一段计算图片唯一标识(Checksum)的Demo:
/**
* 计算Checksum
*
* @param imgUrl
* @return imgChecksum
* @throws IOException
*/
public static byte[] getPic(String bgUrl) throws IOException {
URL url = new URL(bgUrl);
DataInputStream dataInputStream = null;
try {
dataInputStream = new DataInputStream(url.openStream());
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = dataInputStream.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
return (output != null && output.size() > 0) ? output.toByteArray() : null;
} catch (SocketException | SocketTimeoutException | ConnectTimeoutException | NoHttpResponseException | UnknownHostException e) {
System.out.println("getPic() Network exception ! ");
return null;
} catch (MalformedURLException e) {
System.out.println("getPic() MalformedURLException bgUrl= " + bgUrl);
return null;
} catch (IOException e) {
System.out.println("getPic() IOException ! ");
return null;
} catch (Exception e) {
System.out.println("getPic() Exception ! " + e.toString());
return null;
} finally {
dataInputStream.close();
}
}
public static String genChecksum(byte[] input) throws NoSuchAlgorithmException, IOException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(input);
byte[] digestBytes = messageDigest.digest();
String ret = String.format(DatatypeConverter.printHexBinary(digestBytes).toLowerCase());
return ret;
}
2.第二个问题是对样本图片进行处理,得到旋转360°的结果。
样本图:
生成模型图:
实现代码:
public static Color bgColor = new Color(255, 255, 255);
/**
* 创建任意角度的旋转图像
*
* @param image
* @param theta
* @param backgroundColor
* @return
*/
public BufferedImage rotateImage(BufferedImage image, double theta, Color backgroundColor) {
int width = image.getWidth();
int height = image.getHeight();
double angle = theta * Math.PI / 180; // 度转弧度
double[] xCoords = getX(width / 2, height / 2, angle);
double[] yCoords = getY(width / 2, height / 2, angle);
int WIDTH = (int) (xCoords[3] - xCoords[0]);
int HEIGHT = (int) (yCoords[3] - yCoords[0]);
BufferedImage resultImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < HEIGHT; j++) {
int x = i - WIDTH / 2;
int y = HEIGHT / 2 - j;
double radius = Math.sqrt(x * x + y * y);
double angle1;
if (y > 0) {
angle1 = Math.acos(x / radius);
} else {
angle1 = 2 * Math.PI - Math.acos(x / radius);
}
x = (int) Math.round(radius * Math.cos(angle1 - angle));
y = (int) Math.round(radius * Math.sin(angle1 - angle));
if (x < (width / 2) & x > -(width / 2) & y < (height / 2) & y > -(height / 2)) {
int rgb = image.getRGB((int) Math.round(x + width / 2), (int) Math.round(height / 2 - y));
resultImage.setRGB(i, j, rgb);
} else {
resultImage.setRGB(i, j, -1);
}
}
}
return resultImage;
}
// 获取四个角点旋转后Y方向坐标
private double[] getY(int i, int j, double angle) {
double results[] = new double[4];
double radius = Math.sqrt(i * i + j * j);
double angle1 = Math.asin(j / radius);
results[0] = radius * Math.sin(angle1 + angle);
results[1] = radius * Math.sin(Math.PI - angle1 + angle);
results[2] = -results[0];
results[3] = -results[1];
Arrays.sort(results);
return results;
}
// 获取四个角点旋转后X方向坐标
private double[] getX(int i, int j, double angle) {
double results[] = new double[4];
double radius = Math.sqrt(i * i + j * j);
double angle1 = Math.acos(i / radius);
results[0] = radius * Math.cos(angle1 + angle);
results[1] = radius * Math.cos(Math.PI - angle1 + angle);
results[2] = -results[0];
results[3] = -results[1];
Arrays.sort(results);
return results;
}
public BufferedImage writeCyclePic(BufferedImage image) {
BufferedImage newImage = new BufferedImage(350, 350, BufferedImage.TYPE_INT_BGR);
try {
int width = image.getWidth();
int heigth = image.getHeight();
double x0 = width / 2;
double y0 = heigth / 2;
int woffset = (width - 350) / 2;
int hoffset = (heigth - 350) / 2;
for (int i = woffset; i < 350 + woffset; i++) {
for (int j = hoffset; j < 350 + hoffset; j++) {
double r = Math.sqrt(Math.pow(Math.abs(i - x0), 2.0) + Math.pow(Math.abs(j - y0), 2.0));
if (r > (x0 - woffset)) {
newImage.setRGB(i - woffset, j - hoffset, -1);
} else {
newImage.setRGB(i - woffset, j - hoffset, image.getRGB(i, j));
}
}
}
return newImage;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
// 旋转生成图片
public Map<String, Object[]> rotate360(File input, String outPath) {
Map<String, Object[]> binMap = new LinkedHashMap<String, Object[]>();
System.out.println("rotate360() start");
System.out.print("distance list:");
try {
BufferedImage image = ImageIO.read(input);
BufferedImage mid, result;
File output = null;
for (int i = 0; i < 360; i++) {
mid = rotateImage(image, i, bgColor);
result = writeCyclePic(mid);
output = new File(outPath + i + ".jpg");
if (!output.exists()) {
output.mkdirs();
}
ImageIO.write(result, "jpg", output);
System.out.print(output.getName() + "\n");
}
System.out.println("\n------> rotate() finish ");
return binMap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3.这里用到了相似度匹配算法:(这里参考了别人的博文,具体不太记得了,找到后再@原博主吧)
public final class PicFinger {
/**
* 图像指纹的尺寸,将图像resize到指定的尺寸,来计算哈希数组
*/
private static final int HASH_SIZE = 16;
/**
* 保存图像指纹的二值化矩阵
*/
private final byte[] binaryzationMatrix;
public PicFinger(byte[] hashValue) {
if (hashValue.length != HASH_SIZE * HASH_SIZE) {
throw new IllegalArgumentException(String.format("length of hashValue must be %d", HASH_SIZE * HASH_SIZE));
}
this.binaryzationMatrix = hashValue;
}
public PicFinger(String hashValue) {
this(toBytes(hashValue));
}
public PicFinger(BufferedImage src) {
this(hashValue(src));
}
public byte[] getBinaryzationMatrix() {
return binaryzationMatrix;
}
private static byte[] hashValue(BufferedImage src) {
BufferedImage hashImage = resize(src, HASH_SIZE, HASH_SIZE);
byte[] matrixGray = (byte[]) toGray(hashImage).getData().getDataElements(0, 0, HASH_SIZE, HASH_SIZE, null);
return binaryzation(matrixGray);
}
/**
* 从压缩格式指纹创建{@link PicFinger}对象
*
* @param compactValue
* @return
*/
public static PicFinger createFromCompact(byte[] compactValue) {
return new PicFinger(uncompact(compactValue));
}
public static boolean validHashValue(byte[] hashValue) {
if (hashValue.length != HASH_SIZE) {
return false;
}
for (byte b : hashValue) {
{
if (0 != b && 1 != b) {
return false;
}
}
}
return true;
}
public static boolean validHashValue(String hashValue) {
if (hashValue.length() != HASH_SIZE) {
return false;
}
for (int i = 0; i < hashValue.length(); ++i) {
if ('0' != hashValue.charAt(i) && '1' != hashValue.charAt(i)) {
return false;
}
}
return true;
}
public byte[] compact() {
return compact(binaryzationMatrix);
}
/**
* 指纹数据按位压缩
*
* @param hashValue
* @return
*/
private static byte[] compact(byte[] hashValue) {
byte[] result = new byte[(hashValue.length + 7) >> 3];
byte b = 0;
for (int i = 0; i < hashValue.length; ++i) {
if (0 == (i & 7)) {
b = 0;
}
if (1 == hashValue[i]) {
b |= 1 << (i & 7);
} else if (hashValue[i] != 0) {
throw new IllegalArgumentException("invalid hashValue,every element must be 0 or 1");
}
if (7 == (i & 7) || i == hashValue.length - 1) {
result[i >> 3] = b;
}
}
return result;
}
/**
* 压缩格式的指纹解压缩
*
* @param compactValue
* @return
*/
private static byte[] uncompact(byte[] compactValue) {
byte[] result = new byte[compactValue.length << 3];
for (int i = 0; i < result.length; ++i) {
if ((compactValue[i >> 3] & (1 << (i & 7))) == 0) {
result[i] = 0;
} else {
result[i] = 1;
}
}
return result;
}
/**
* 字符串类型的指纹数据转为字节数组
*
* @param hashValue
* @return
*/
private static byte[] toBytes(String hashValue) {
hashValue = hashValue.replaceAll("\\s", "");
byte[] result = new byte[hashValue.length()];
for (int i = 0; i < result.length; ++i) {
char c = hashValue.charAt(i);
if ('0' == c) {
result[i] = 0;
} else if ('1' == c) {
result[i] = 1;
} else {
throw new IllegalArgumentException("invalid hashValue String");
}
}
return result;
}
/**
* 缩放图像到指定尺寸
*
* @param src
* @param width
* @param height
* @return
*/
private static BufferedImage resize(Image src, int width, int height) {
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
Graphics g = result.getGraphics();
try {
g.drawImage(src.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
} finally {
g.dispose();
}
return result;
}
/**
* 计算均值
*
* @param src
* @return
*/
private static int mean(byte[] src) {
long sum = 0;
// 将数组元素转为无符号整数
for (byte b : src) {
sum += (long) b & 0xff;
}
return (int) (Math.round((float) sum / src.length));
}
/**
* 二值化处理
*
* @param src
* @return
*/
private static byte[] binaryzation(byte[] src) {
byte[] dst = src.clone();
int mean = mean(src);
for (int i = 0; i < dst.length; ++i) {
// 将数组元素转为无符号整数再比较
dst[i] = (byte) (((int) dst[i] & 0xff) >= mean ? 1 : 0);
}
return dst;
}
/**
* 转灰度图像
*
* @param src
* @return
*/
private static BufferedImage toGray(BufferedImage src) {
if (src.getType() == BufferedImage.TYPE_BYTE_GRAY) {
return src;
} else {
// 图像转灰
BufferedImage grayImage = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null).filter(src, grayImage);
return grayImage;
}
}
@Override
public String toString() {
return toString(true);
}
/**
* @param multiLine
* 是否分行
* @return
*/
public String toString(boolean multiLine) {
StringBuffer buffer = new StringBuffer();
int count = 0;
for (byte b : this.binaryzationMatrix) {
buffer.append(0 == b ? '0' : '1');
if (multiLine && ++count % HASH_SIZE == 0) {
buffer.append('\n');
}
}
return buffer.toString();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PicFinger) {
return Arrays.equals(this.binaryzationMatrix, ((PicFinger) obj).binaryzationMatrix);
} else {
return super.equals(obj);
}
}
/**
* 与指定的压缩格式指纹比较相似度
*
* @param compactValue
* @return
* @see #compare(PicFinger)
*/
public float compareCompact(byte[] compactValue) {
return compare(createFromCompact(compactValue));
}
/**
* @param hashValue
* @return
* @see #compare(PicFinger)
*/
public float compare(String hashValue) {
return compare(new PicFinger(hashValue));
}
/**
* 与指定的指纹比较相似度
*
* @param hashValue
* @return
* @see #compare(PicFinger)
*/
public float compare(byte[] hashValue) {
return compare(new PicFinger(hashValue));
}
/**
* 与指定图像比较相似度
*
* @param image2
* @return
* @see #compare(PicFinger)
*/
public float compare(BufferedImage image2) {
return compare(new PicFinger(image2));
}
/**
* 比较指纹相似度
*
* @param src
* @return
* @see #compare(byte[], byte[])
*/
public float compare(PicFinger src) {
if (src.binaryzationMatrix.length != this.binaryzationMatrix.length) {
throw new IllegalArgumentException("length of hashValue is mismatch");
}
return compare(binaryzationMatrix, src.binaryzationMatrix);
}
/**
* 判断两个数组相似度,数组长度必须一致否则抛出异常
*
* @param f1
* @param f2
* @return 返回相似度(0.0 ~ 1.0)
*/
private static float compare(byte[] f1, byte[] f2) {
if (f1.length != f2.length) {
throw new IllegalArgumentException("mismatch FingerPrint length");
}
int sameCount = 0;
for (int i = 0; i < f1.length; ++i) {
{
if (f1[i] == f2[i]) {
++sameCount;
}
}
}
return (float) sameCount / f1.length;
}
public static float compareCompact(byte[] f1, byte[] f2) {
return compare(uncompact(f1), uncompact(f2));
}
public static float compare(BufferedImage image1, BufferedImage image2) {
return new PicFinger(image1).compare(new PicFinger(image2));
}
public static Map<String, byte[]> getLibMatrix(List<File> imgListLib) {
Map<String, byte[]> binMap = new ConcurrentHashMap<String, byte[]>();
BufferedImage libBuf = null;
PicFinger fpLib = null;
// 初始化Lib库
String fileName = null;
System.out.print("getLibMatrix() imgListLib size=" + imgListLib.size());
int c = 0;
for (File imgfileLib : imgListLib) {
if (imgfileLib.exists()) {
fileName = imgfileLib.getName();
fileName = fileName.substring(0, fileName.indexOf("."));
try {
libBuf = ImageIO.read(imgfileLib);
} catch (IOException e) {
e.printStackTrace();
}
if (libBuf != null) {
fpLib = new PicFinger(libBuf);
binMap.put(fileName, fpLib.getBinaryzationMatrix());
if (c % 100 == 0) {
System.out.print("\ngetLib() list c=" + c + " ");
}
System.out.print(fileName + ",");
c++;
} else {
System.out.println("libBuf=" + libBuf + "|imgfileLib=" + imgfileLib.getName());
}
} else {
continue;
}
}
System.out.println("\ngetLibMatrix() size=" + binMap.size());
return binMap;
}
}
作者:香芋味的猫丶
博主V:XY-Cat3230
博主Q:1956507329
猜你喜欢:
《腾讯防水墙滑动拼图验证码》
《百度旋转图片验证码》
《网易易盾滑动拼图验证码》
《顶象区域面积点选验证码》
《顶象滑动拼图验证码》
《极验滑动拼图验证码》
《使用深度学习来破解 captcha 验证码》