selenium+java破解極驗滑動驗證碼的示例代碼

轉自: https://www.jianshu.com/p/1466f1ba3275css

selenium+java破解極驗滑動驗證碼

96 臥顏沉默 關注java

2017.08.15 20:07* 字數 3085 閱讀 2548評論 2喜歡 10node

摘要

分析驗證碼素材圖片混淆原理,並採用selenium模擬人拖動滑塊過程,進而破解驗證碼。git

人工驗證的過程

  1. 打開威鋒網註冊頁面(https://passport.feng.com/?r=user/register
  2. 移動鼠標至小滑塊,一張完整的圖片會出現(以下圖1)
     

     

  3. 點擊鼠標左鍵,圖片中間會出現一個缺塊(以下圖2)
     

     

  4. 移動小滑塊正上方圖案至缺塊處
  5. 驗證經過

selenium模擬驗證的過程

  1. 加載威鋒網註冊頁面(https://passport.feng.com/?r=user/register
  2. 下載圖片1和缺塊圖片2
  3. 根據兩張圖片的差別計算平移的距離x
  4. 模擬鼠標點擊事件,點擊小滑塊向右移動x
  5. 驗證經過

詳細分析

  1. 打開chrome瀏覽器控制檯,會發現圖1所示的驗證碼圖片並非極驗後臺返回的原圖。而是由多個div拼接而成(以下圖3)
     

     


    經過圖片顯示div的style屬性可知,極驗後臺把圖片進行切割加錯位處理。把素材圖片切割成10 * 58大小的52張小圖,再進行錯位處理。在網頁上顯示的時候,再經過css的background-position屬性對圖片進行還原。以上的圖1和圖2都是通過了這種處理。在這種狀況下,使用selenium模擬驗證是須要對下載的驗證碼圖片進行還原。如上圖3的第一個div.gt_cut_fullbg_slice標籤,它的大小爲10px * 58px,其中style屬性爲background-image: url("http://static.geetest.com/pictures/gt/969ffa43c/969ffa43c.webp"); background-position: -157px -58px;會把該屬性對應url的圖片進行一個平移操做,以左上角爲參考,向左平移157px,向上平移58px,圖片超出部分不會顯示。因此上圖1所示圖片是由26 * 2個10px * 58px大小的div組成(以下圖4)。每個小方塊的大小58 * 10
     

     

  2. 下載圖片並還原,上一步驟分析了圖片具體的混淆邏輯,具體還原圖片的代碼實現以下,主要邏輯是把原圖裁剪爲52張小圖,而後拼接成一張完整的圖。

    複製代碼

    1 /**
     2  *還原圖片
     3  * @param type
     4  */
     5 private static void restoreImage(String type) throws IOException {
     6     //把圖片裁剪爲2 * 26份
     7     for(int i = 0; i < 52; i++){
     8         cutPic(basePath + type +".jpg"
     9                 ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58);
    10     }
    11     //拼接圖片
    12     String[] b = new String[26];
    13     for(int i = 0; i < 26; i++){
    14         b[i] = String.format(basePath + "result/" + type + "%d.jpg", i);
    15     }
    16     mergeImage(b, 1, basePath + "result/" + type + "result1.jpg");
    17     //拼接圖片
    18     String[] c = new String[26];
    19     for(int i = 0; i < 26; i++){
    20         c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26);
    21     }
    22     mergeImage(c, 1, basePath + "result/" + type + "result2.jpg");
    23     mergeImage(new String[]{basePath + "result/" + type + "result1.jpg",
    24             basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg");
    25     //刪除產生的中間圖片
    26     for(int i = 0; i < 52; i++){
    27         new File(basePath + "result/" + type + i + ".jpg").deleteOnExit();
    28     }
    29     new File(basePath + "result/" + type + "result1.jpg").deleteOnExit();
    30     new File(basePath + "result/" + type + "result2.jpg").deleteOnExit();
    31 }
    32 
    33 做者:臥顏沉默
    34 連接:https://www.jianshu.com/p/1466f1ba3275
    35 來源:簡書
    36 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

    複製代碼

     

    還原過程須要注意的是,後臺返回錯位的圖片是312 * 116大小的。而網頁上圖片div的大小是260 * 116。
  3. 計算平移距離,遍歷圖片的每個像素點,當兩張圖的R、G、B之差的和大於255,說明該點的差別過大,頗有可能就是須要平移到該位置的那個點,代碼以下。

    複製代碼

    1 BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg"));
     2     BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg"));
     3     for (int i = 0; i < bgBI.getWidth(); i++){
     4         for (int j = 0; j < bgBI.getHeight(); j++) {
     5             int[] fullRgb = new int[3];
     6             fullRgb[0] = (fullBI.getRGB(i, j)  & 0xff0000) >> 16;
     7             fullRgb[1] = (fullBI.getRGB(i, j)  & 0xff00) >> 8;
     8             fullRgb[2] = (fullBI.getRGB(i, j)  & 0xff);
     9 
    10             int[] bgRgb = new int[3];
    11             bgRgb[0] = (bgBI.getRGB(i, j)  & 0xff0000) >> 16;
    12             bgRgb[1] = (bgBI.getRGB(i, j)  & 0xff00) >> 8;
    13             bgRgb[2] = (bgBI.getRGB(i, j)  & 0xff);
    14             if(difference(fullRgb, bgRgb) > 255){
    15                 return i;
    16             }
    17         }
    18     }
    19 
    20 做者:臥顏沉默
    21 連接:https://www.jianshu.com/p/1466f1ba3275
    22 來源:簡書
    23 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

    複製代碼

     

  4. 模擬鼠標移動事件,這一步驟是最關鍵的步驟,極驗驗證碼後臺正是經過移動滑塊的軌跡來判斷是否爲機器所爲。整個移動軌跡的過程越隨機越好,我這裏提供一種成功率較高的移動算法,代碼以下。

    複製代碼

    1 public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
     2         int xDis = distance + 11;
     3         System.out.println("應平移距離:" + xDis);
     4         int moveX = new Random().nextInt(8) - 5;
     5         int moveY = 1;
     6         Actions actions = new Actions(driver);
     7         new Actions(driver).clickAndHold(element).perform();
     8         Thread.sleep(200);
     9         printLocation(element);
    10         actions.moveToElement(element, moveX, moveY).perform();
    11         System.out.println(moveX + "--" + moveY);
    12         printLocation(element);
    13         for (int i = 0; i < 22; i++){
    14             int s = 10;
    15             if (i % 2 == 0){
    16                 s = -10;
    17             }
    18             actions.moveToElement(element, s, 1).perform();
    19             printLocation(element);
    20             Thread.sleep(new Random().nextInt(100) + 150);
    21         }
    22 
    23         System.out.println(xDis + "--" + 1);
    24         actions.moveByOffset(xDis, 1).perform();
    25         printLocation(element);
    26         Thread.sleep(200);
    27         actions.release(element).perform();
    28     }
    29 
    30 做者:臥顏沉默
    31 連接:https://www.jianshu.com/p/1466f1ba3275
    32 來源:簡書
    33 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

    複製代碼

     

  5. 完整代碼以下

    複製代碼

    1 package com.github.wycm;
      2 
      3 import org.apache.commons.io.FileUtils;
      4 import org.jsoup.Jsoup;
      5 import org.jsoup.nodes.Document;
      6 import org.jsoup.nodes.Element;
      7 import org.jsoup.select.Elements;
      8 import org.openqa.selenium.By;
      9 import org.openqa.selenium.Point;
     10 import org.openqa.selenium.WebDriver;
     11 import org.openqa.selenium.WebElement;
     12 import org.openqa.selenium.chrome.ChromeDriver;
     13 import org.openqa.selenium.interactions.Actions;
     14 import org.openqa.selenium.support.ui.ExpectedCondition;
     15 import org.openqa.selenium.support.ui.WebDriverWait;
     16 
     17 import javax.imageio.ImageIO;
     18 import javax.imageio.ImageReadParam;
     19 import javax.imageio.ImageReader;
     20 import javax.imageio.stream.ImageInputStream;
     21 import java.awt.*;
     22 import java.awt.image.BufferedImage;
     23 import java.io.File;
     24 import java.io.FileInputStream;
     25 import java.io.IOException;
     26 import java.net.URL;
     27 import java.util.Iterator;
     28 import java.util.Random;
     29 import java.util.regex.Matcher;
     30 import java.util.regex.Pattern;
     31 
     32 public class GeettestCrawler {
     33     private static String basePath = "src/main/resources/";
     34     private static String FULL_IMAGE_NAME = "full-image";
     35     private static String BG_IMAGE_NAME = "bg-image";
     36     private static int[][] moveArray = new int[52][2];
     37     private static boolean moveArrayInit = false;
     38     private static String INDEX_URL = "https://passport.feng.com/?r=user/register";
     39     private static WebDriver driver;
     40 
     41     static {
     42         System.setProperty("webdriver.chrome.driver", "D:/dev/selenium/chromedriver_V2.30/chromedriver_win32/chromedriver.exe");
     43         if (!System.getProperty("os.name").toLowerCase().contains("windows")){
     44             System.setProperty("webdriver.chrome.driver", "/Users/wangyang/workspace/selenium/chromedriver_V2.30/chromedriver");
     45         }
     46         driver = new ChromeDriver();
     47     }
     48 
     49     public static void main(String[] args) throws InterruptedException {
     50         for (int i = 0; i < 10; i++){
     51             try {
     52                 invoke();
     53             } catch (IOException e) {
     54                 e.printStackTrace();
     55             } catch (InterruptedException e) {
     56                 e.printStackTrace();
     57             }
     58         }
     59         driver.quit();
     60     }
     61     private static void invoke() throws IOException, InterruptedException {
     62         //設置input參數
     63         driver.get(INDEX_URL);
     64 
     65         //經過[class=gt_slider_knob gt_show]
     66         By moveBtn = By.cssSelector(".gt_slider_knob.gt_show");
     67         waitForLoad(driver, moveBtn);
     68         WebElement moveElemet = driver.findElement(moveBtn);
     69         int i = 0;
     70         while (i++ < 15){
     71             int distance = getMoveDistance(driver);
     72             move(driver, moveElemet, distance - 6);
     73             By gtTypeBy = By.cssSelector(".gt_info_type");
     74             By gtInfoBy = By.cssSelector(".gt_info_content");
     75             waitForLoad(driver, gtTypeBy);
     76             waitForLoad(driver, gtInfoBy);
     77             String gtType = driver.findElement(gtTypeBy).getText();
     78             String gtInfo = driver.findElement(gtInfoBy).getText();
     79             System.out.println(gtType + "---" + gtInfo);
     80             /**
     81              * 再來一次:
     82              * 驗證失敗:
     83              */
     84             if(!gtType.equals("再來一次:") && !gtType.equals("驗證失敗:")){
     85                 Thread.sleep(4000);
     86                 System.out.println(driver);
     87                 break;
     88             }
     89             Thread.sleep(4000);
     90         }
     91     }
     92 
     93     /**
     94      * 移動
     95      * @param driver
     96      * @param element
     97      * @param distance
     98      * @throws InterruptedException
     99      */
    100     public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
    101         int xDis = distance + 11;
    102         System.out.println("應平移距離:" + xDis);
    103         int moveX = new Random().nextInt(8) - 5;
    104         int moveY = 1;
    105         Actions actions = new Actions(driver);
    106         new Actions(driver).clickAndHold(element).perform();
    107         Thread.sleep(200);
    108         printLocation(element);
    109         actions.moveToElement(element, moveX, moveY).perform();
    110         System.out.println(moveX + "--" + moveY);
    111         printLocation(element);
    112         for (int i = 0; i < 22; i++){
    113             int s = 10;
    114             if (i % 2 == 0){
    115                 s = -10;
    116             }
    117             actions.moveToElement(element, s, 1).perform();
    118 //            printLocation(element);
    119             Thread.sleep(new Random().nextInt(100) + 150);
    120         }
    121 
    122         System.out.println(xDis + "--" + 1);
    123         actions.moveByOffset(xDis, 1).perform();
    124         printLocation(element);
    125         Thread.sleep(200);
    126         actions.release(element).perform();
    127     }
    128     private static void printLocation(WebElement element){
    129         Point point  = element.getLocation();
    130         System.out.println(point.toString());
    131     }
    132     /**
    133      * 等待元素加載,10s超時
    134      * @param driver
    135      * @param by
    136      */
    137     public static void waitForLoad(final WebDriver driver, final By by){
    138         new WebDriverWait(driver, 10).until(new ExpectedCondition<Boolean>() {
    139             public Boolean apply(WebDriver d) {
    140                 WebElement element = driver.findElement(by);
    141                 if (element != null){
    142                     return true;
    143                 }
    144                 return false;
    145             }
    146         });
    147     }
    148 
    149     /**
    150      * 計算須要平移的距離
    151      * @param driver
    152      * @return
    153      * @throws IOException
    154      */
    155     public static int getMoveDistance(WebDriver driver) throws IOException {
    156         String pageSource = driver.getPageSource();
    157         String fullImageUrl = getFullImageUrl(pageSource);
    158         FileUtils.copyURLToFile(new URL(fullImageUrl), new File(basePath + FULL_IMAGE_NAME + ".jpg"));
    159         String getBgImageUrl = getBgImageUrl(pageSource);
    160         FileUtils.copyURLToFile(new URL(getBgImageUrl), new File(basePath + BG_IMAGE_NAME + ".jpg"));
    161         initMoveArray(driver);
    162         restoreImage(FULL_IMAGE_NAME);
    163         restoreImage(BG_IMAGE_NAME);
    164         BufferedImage fullBI = ImageIO.read(new File(basePath + "result/" + FULL_IMAGE_NAME + "result3.jpg"));
    165         BufferedImage bgBI = ImageIO.read(new File(basePath + "result/" + BG_IMAGE_NAME + "result3.jpg"));
    166         for (int i = 0; i < bgBI.getWidth(); i++){
    167             for (int j = 0; j < bgBI.getHeight(); j++) {
    168                 int[] fullRgb = new int[3];
    169                 fullRgb[0] = (fullBI.getRGB(i, j)  & 0xff0000) >> 16;
    170                 fullRgb[1] = (fullBI.getRGB(i, j)  & 0xff00) >> 8;
    171                 fullRgb[2] = (fullBI.getRGB(i, j)  & 0xff);
    172 
    173                 int[] bgRgb = new int[3];
    174                 bgRgb[0] = (bgBI.getRGB(i, j)  & 0xff0000) >> 16;
    175                 bgRgb[1] = (bgBI.getRGB(i, j)  & 0xff00) >> 8;
    176                 bgRgb[2] = (bgBI.getRGB(i, j)  & 0xff);
    177                 if(difference(fullRgb, bgRgb) > 255){
    178                     return i;
    179                 }
    180             }
    181         }
    182         throw new RuntimeException("未找到須要平移的位置");
    183     }
    184     private static int difference(int[] a, int[] b){
    185         return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
    186     }
    187     /**
    188      * 獲取move數組
    189      * @param driver
    190      */
    191     private static void initMoveArray(WebDriver driver){
    192         if (moveArrayInit){
    193             return;
    194         }
    195         Document document = Jsoup.parse(driver.getPageSource());
    196         Elements elements = document.select("[class=gt_cut_bg gt_show]").first().children();
    197         int i = 0;
    198         for(Element element : elements){
    199             Pattern pattern = Pattern.compile(".*background-position: (.*?)px (.*?)px.*");
    200             Matcher matcher = pattern.matcher(element.toString());
    201             if (matcher.find()){
    202                 String width = matcher.group(1);
    203                 String height = matcher.group(2);
    204                 moveArray[i][0] = Integer.parseInt(width);
    205                 moveArray[i++][1] = Integer.parseInt(height);
    206             } else {
    207                 throw new RuntimeException("解析異常");
    208             }
    209         }
    210         moveArrayInit = true;
    211     }
    212     /**
    213      *還原圖片
    214      * @param type
    215      */
    216     private static void restoreImage(String type) throws IOException {
    217         //把圖片裁剪爲2 * 26份
    218         for(int i = 0; i < 52; i++){
    219             cutPic(basePath + type +".jpg"
    220                     ,basePath + "result/" + type + i + ".jpg", -moveArray[i][0], -moveArray[i][1], 10, 58);
    221         }
    222         //拼接圖片
    223         String[] b = new String[26];
    224         for(int i = 0; i < 26; i++){
    225             b[i] = String.format(basePath + "result/" + type + "%d.jpg", i);
    226         }
    227         mergeImage(b, 1, basePath + "result/" + type + "result1.jpg");
    228         //拼接圖片
    229         String[] c = new String[26];
    230         for(int i = 0; i < 26; i++){
    231             c[i] = String.format(basePath + "result/" + type + "%d.jpg", i + 26);
    232         }
    233         mergeImage(c, 1, basePath + "result/" + type + "result2.jpg");
    234         mergeImage(new String[]{basePath + "result/" + type + "result1.jpg",
    235                 basePath + "result/" + type + "result2.jpg"}, 2, basePath + "result/" + type + "result3.jpg");
    236         //刪除產生的中間圖片
    237         for(int i = 0; i < 52; i++){
    238             new File(basePath + "result/" + type + i + ".jpg").deleteOnExit();
    239         }
    240         new File(basePath + "result/" + type + "result1.jpg").deleteOnExit();
    241         new File(basePath + "result/" + type + "result2.jpg").deleteOnExit();
    242     }
    243     /**
    244      * 獲取原始圖url
    245      * @param pageSource
    246      * @return
    247      */
    248     private static String getFullImageUrl(String pageSource){
    249         String url = null;
    250         Document document = Jsoup.parse(pageSource);
    251         String style = document.select("[class=gt_cut_fullbg_slice]").first().attr("style");
    252         Pattern pattern = Pattern.compile("url\\(\"(.*)\"\\)");
    253         Matcher matcher = pattern.matcher(style);
    254         if (matcher.find()){
    255             url = matcher.group(1);
    256         }
    257         url = url.replace(".webp", ".jpg");
    258         System.out.println(url);
    259         return url;
    260     }
    261     /**
    262      * 獲取帶背景的url
    263      * @param pageSource
    264      * @return
    265      */
    266     private static String getBgImageUrl(String pageSource){
    267         String url = null;
    268         Document document = Jsoup.parse(pageSource);
    269         String style = document.select(".gt_cut_bg_slice").first().attr("style");
    270         Pattern pattern = Pattern.compile("url\\(\"(.*)\"\\)");
    271         Matcher matcher = pattern.matcher(style);
    272         if (matcher.find()){
    273             url = matcher.group(1);
    274         }
    275         url = url.replace(".webp", ".jpg");
    276         System.out.println(url);
    277         return url;
    278     }
    279     public static boolean cutPic(String srcFile, String outFile, int x, int y,
    280                                  int width, int height) {
    281         FileInputStream is = null;
    282         ImageInputStream iis = null;
    283         try {
    284             if (!new File(srcFile).exists()) {
    285                 return false;
    286             }
    287             is = new FileInputStream(srcFile);
    288             String ext = srcFile.substring(srcFile.lastIndexOf(".") + 1);
    289             Iterator<ImageReader> it = ImageIO.getImageReadersByFormatName(ext);
    290             ImageReader reader = it.next();
    291             iis = ImageIO.createImageInputStream(is);
    292             reader.setInput(iis, true);
    293             ImageReadParam param = reader.getDefaultReadParam();
    294             Rectangle rect = new Rectangle(x, y, width, height);
    295             param.setSourceRegion(rect);
    296             BufferedImage bi = reader.read(0, param);
    297             File tempOutFile = new File(outFile);
    298             if (!tempOutFile.exists()) {
    299                 tempOutFile.mkdirs();
    300             }
    301             ImageIO.write(bi, ext, new File(outFile));
    302             return true;
    303         } catch (Exception e) {
    304             e.printStackTrace();
    305             return false;
    306         } finally {
    307             try {
    308                 if (is != null) {
    309                     is.close();
    310                 }
    311                 if (iis != null) {
    312                     iis.close();
    313                 }
    314             } catch (IOException e) {
    315                 e.printStackTrace();
    316                 return false;
    317             }
    318         }
    319     }
    320     /**
    321      * 圖片拼接 (注意:必須兩張圖片長寬一致哦)
    322      * @param files 要拼接的文件列表
    323      * @param type  1橫向拼接,2 縱向拼接
    324      * @param targetFile 輸出文件
    325      */
    326     private static void mergeImage(String[] files, int type, String targetFile) {
    327         int length = files.length;
    328         File[] src = new File[length];
    329         BufferedImage[] images = new BufferedImage[length];
    330         int[][] ImageArrays = new int[length][];
    331         for (int i = 0; i < length; i++) {
    332             try {
    333                 src[i] = new File(files[i]);
    334                 images[i] = ImageIO.read(src[i]);
    335             } catch (Exception e) {
    336                 throw new RuntimeException(e);
    337             }
    338             int width = images[i].getWidth();
    339             int height = images[i].getHeight();
    340             ImageArrays[i] = new int[width * height];
    341             ImageArrays[i] = images[i].getRGB(0, 0, width, height, ImageArrays[i], 0, width);
    342         }
    343         int newHeight = 0;
    344         int newWidth = 0;
    345         for (int i = 0; i < images.length; i++) {
    346             // 橫向
    347             if (type == 1) {
    348                 newHeight = newHeight > images[i].getHeight() ? newHeight : images[i].getHeight();
    349                 newWidth += images[i].getWidth();
    350             } else if (type == 2) {// 縱向
    351                 newWidth = newWidth > images[i].getWidth() ? newWidth : images[i].getWidth();
    352                 newHeight += images[i].getHeight();
    353             }
    354         }
    355         if (type == 1 && newWidth < 1) {
    356             return;
    357         }
    358         if (type == 2 && newHeight < 1) {
    359             return;
    360         }
    361         // 生成新圖片
    362         try {
    363             BufferedImage ImageNew = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
    364             int height_i = 0;
    365             int width_i = 0;
    366             for (int i = 0; i < images.length; i++) {
    367                 if (type == 1) {
    368                     ImageNew.setRGB(width_i, 0, images[i].getWidth(), newHeight, ImageArrays[i], 0,
    369                             images[i].getWidth());
    370                     width_i += images[i].getWidth();
    371                 } else if (type == 2) {
    372                     ImageNew.setRGB(0, height_i, newWidth, images[i].getHeight(), ImageArrays[i], 0, newWidth);
    373                     height_i += images[i].getHeight();
    374                 }
    375             }
    376             //輸出想要的圖片
    377             ImageIO.write(ImageNew, targetFile.split("\\.")[1], new File(targetFile));
    378 
    379         } catch (Exception e) {
    380             throw new RuntimeException(e);
    381         }
    382     }
    383 }
    384 
    385 做者:臥顏沉默
    386 連接:https://www.jianshu.com/p/1466f1ba3275
    387 來源:簡書
    388 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

    複製代碼

     

  6. pom文件依賴以下

    複製代碼

    1     <dependency>
     2       <groupId>org.seleniumhq.selenium</groupId>
     3       <artifactId>selenium-server</artifactId>
     4       <version>3.0.1</version>
     5     </dependency>
     6     <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
     7     <dependency>
     8       <groupId>org.jsoup</groupId>
     9       <artifactId>jsoup</artifactId>
    10       <version>1.7.2</version>
    11     </dependency>
    12 
    13 做者:臥顏沉默
    14 連接:https://www.jianshu.com/p/1466f1ba3275
    15 來源:簡書
    16 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

    複製代碼

     

最後

  1. 完整代碼已上傳至github,地址:https://github.com/wycm/selenium-geetest-crack
  2. 附上一張滑動效果圖
相關文章
相關標籤/搜索