[
  {
    "path": ".gitignore",
    "content": ".idea\n__pycache__\n"
  },
  {
    "path": "README.md",
    "content": "# slide_captcha_cracker\r\n\r\n![](https://github.com/chxj1992/slide_captcha_cracker/raw/master/screenshot.png)\r\n\r\n[在线Demo](http://slide-captcha.chxj.name/)\r\n\r\n\r\n### 简介 \r\n\r\n本项目是一个通过简单图片边缘检测算法来定位滑动验证码拼图在背景图中位置的一个例子。\r\n\r\n代码主要采用`opencv`提供的一些函数对图片进行处理并实现定位，在这里只是提供一个思路，抛砖引玉。\r\n\r\n\r\n### 关于滑动验证码\r\n\r\n演示项目所用到的滑动验证码的实现相对简单，整个交互过程主要包含以下步骤：\r\n\r\n* 服务端将背景图片和拼图图片合并为一张图片，并记录下拼图在背景图中的`x坐标`，然后将拼好图片和单独的拼图图片返回给客户端\r\n* 客户端实现单片拼图在背景图上拖动的动画效果，并在用户完成拖动动作后，将当前拼图所处位置的坐标数据加密后返回给服务端\r\n* 服务端解密数据并比较客户端返回的`x坐标`数据并与之前保存的`x坐标`数据进行比较，允许小范围内的误差\r\n\r\n\r\n### 实现原理\r\n\r\n基于以上的验证码实现，本例子通过以下方式实现对验证码拼图在背景图中的定位（其他步骤较为简单，不做考虑）：\r\n\r\n* 利用`opencv`库中提供的边界查找函数(cv2.findContours)提取单片拼图边缘轨迹并构造成一个二维矩阵（算子）\r\n* 利用 `高斯模糊算子`（cv2.GaussianBlur）和 `Canny边缘检测算子`（cv2.Canny）对背景图进行处理，凸显出拼图在图片中的边缘\r\n* 用拼图轨迹算子在处理后的背景图上进行 `互相关操作`，所得最大（小）值的位置就是拼图在背景图中的坐标\r\n\r\n\r\n### 其他\r\n\r\n拼图的定位只是破解滑动验证码的一个中间环节，想要破解一个好的验证码产品除了定位拼图在整个图片中的位置外，可能还有以下几个问题需要考虑：\r\n\r\n* 从服务端获取的背景图可能是经过加密的（需要阅读js源码获得恢复图片的算法）\r\n* 返回给服务端的参数一般是经过加密处理的（需要阅读js源码理解算法）\r\n* 服务端可能会对用户滑动行为的移动轨迹对用户进行校验（可以尝试通过selenium等工具模拟拖动行为，或者积累真实的拖动数据后学习规律并通过js源码获得构造数据的算法）\r\n"
  },
  {
    "path": "app.py",
    "content": "from flask import Flask\nfrom flask import jsonify, render_template\nfrom matplotlib.pyplot import imsave\n\napp = Flask(__name__)\nimport base64\nfrom io import BytesIO\n\nimport cv2\nimport numpy as np\nimport requests\nfrom PIL import Image\n\n# 偏移量\nbias = -1\n\nslide_captcha_url = 'http://api.shuibei.chxj.name/slide-captcha'\n\n\ndef predict():\n    response = requests.get(slide_captcha_url)\n    base64_image = response.json()['data']['dataUrl']\n    base64_image_without_head = base64_image.replace('data:image/png;base64,', '')\n\n    bytes_io = BytesIO(base64.b64decode(base64_image_without_head))\n    img = np.array(Image.open(bytes_io).convert('RGB'))\n\n    img_blur = cv2.GaussianBlur(img, (3, 3), 0)\n    img_gray = cv2.cvtColor(img_blur, cv2.COLOR_BGR2GRAY)\n    img_canny = cv2.Canny(img_gray, 100, 200)\n\n    operator = get_operator('shape.png')\n\n    (x, y), _ = best_match(img_canny, operator)\n    x = x + bias\n    print('the position of x is', x)\n\n    buffer = mark(img, x, y)\n\n    return {'value': x, 'image': base64.b64encode(buffer.getbuffer()).decode()}\n\n\ndef get_operator(path, url=False, expand=False):\n    if url:\n        req = requests.get(path)\n        arr = np.asarray(bytearray(req.content), dtype=np.uint8)\n        shape = cv2.resize(cv2.imdecode(arr, -1), (69, 69))\n    else:\n        shape = cv2.resize(cv2.imread('shape.png'), (69, 69))\n\n    shape_gray = cv2.cvtColor(shape, cv2.COLOR_BGR2GRAY)\n\n    _, shape_binary = cv2.threshold(shape_gray, 127, 255, cv2.THRESH_BINARY)\n\n    _, contours, hierarchy = cv2.findContours(shape_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)\n    contour = contours[0]\n\n    operator = np.zeros((69, 69))\n\n    for point in contour:\n        operator[point[0][0]][point[0][1]] = 1\n        if expand:\n            if point[0][0] > 0:\n                operator[point[0][0] - 1][point[0][1]] = 1\n            if point[0][0] < 68:\n                operator[point[0][0] + 1][point[0][1]] = 1\n            if point[0][1] > 0:\n                operator[point[0][0]][point[0][1] - 1] = 1\n            if point[0][1] < 68:\n                operator[point[0][0]][point[0][1] + 1] = 1\n\n    return operator\n\n\ndef best_match(image, operator):\n    y_range, x_range = image.shape\n    max_value, position = 0, (1, 1)\n\n    for y in range(1, y_range - 1):\n        for x in range(1, x_range - 1):\n            if y + 69 > 185 or x + 69 > 315:\n                continue\n            block = image[(y - 1):(y + 68), (x - 1):(x + 68)]\n            value = (block * operator).sum()\n            if value > max_value:\n                max_value = value\n                position = (x, y)\n\n    return position, max_value\n\n\ndef mark(img, x, y):\n    cv2.putText(img, 'O', (x - 15, y + 70), cv2.FONT_HERSHEY_SIMPLEX, 4, (255, 0, 0), 2, cv2.LINE_AA)\n\n    buffer = BytesIO()\n    imsave(buffer, img)\n    return buffer\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\n@app.route('/fetch')\ndef fetch():\n    return jsonify(predict())\n"
  },
  {
    "path": "run.sh",
    "content": "export FLASK_APP=app.py\nflask run --host=0.0.0.0\n"
  },
  {
    "path": "templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <title>Slide Captcha</title>\n    <link href=\"https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <script src=\"https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js\"></script>\n</head>\n<body style=\"padding-top: 130px;\">\n\n<a href=\"https://github.com/chxj1992/slide_captcha_cracker\">\n    <img style=\"position: absolute; top: 0; right: 0; border: 0; z-index: 9999;\"\n         src=\"https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67\"\n         alt=\"Fork me on GitHub\"\n         data-canonical-src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png\">\n</a>\n\n<nav class=\"navbar navbar-default navbar-fixed-top\">\n    <div class=\"container-fluid\">\n        <div class=\"navbar-header\">\n            <a class=\"navbar-brand\">\n                <span class=\"glyphicon glyphicon-th\"> </span> 滑动验证码识别 (Canny算子边缘检测)\n            </a>\n        </div>\n    </div>\n</nav>\n\n<div class=\"container\">\n    <br>\n    <div class=\"row\">\n        <div class=\"text-center\">\n            <img id=\"img\" src=\"\" alt=\"\">\n        </div>\n    </div>\n    <br>\n    <div class=\"row\">\n        <div class=\"text-center\">\n            滑动点X坐标 ： <strong id=\"value\"></strong>\n        </div>\n    </div>\n    <br>\n    <div class=\"row\">\n        <div class=\"text-center\">\n            <button id=\"btn\" class=\"btn btn-success\"> 刷新</button>\n        </div>\n    </div>\n</div>\n\n<script>\n    $(document).ready(function () {\n\n        var fetch = function () {\n\n            $.getJSON('/fetch', function (res) {\n                $('#img').attr('src', 'data:image/png;base64,' + res['image']);\n                $('#value').text(res['value']);\n                $('#btn').attr('disabled', false);\n                $('body').css('cursor', 'auto');\n            });\n        };\n\n        $('#btn').click(function () {\n            $('#btn').attr('disabled', true);\n            $('body').css('cursor', 'wait');\n            fetch();\n        });\n\n        fetch();\n    });\n\n</script>\n\n</body>\n</html>"
  }
]