簡單的Java二維碼應用

渔樵江渚發表於2024-10-28

閒來無事做一個java的小應用玩玩,可以模擬微信小程式刷二維碼。
需要解決的主要問題:

  • 二維碼獲取
  • 二維碼展示

1. 抓包微信小程式,獲取所需資訊

微信電腦版可以登入小程式,透過Charles可以抓包。
配置Charles,可以參考文章:charles使用教程, 只要配置好證書就行
執行Charles,開啟微信小程式,找到要抓的介面

1.1 獲取token

獲取token比較敏感,涉及到一些安全問題。略過。

1.2 獲取二維碼資訊

多重新整理幾次二維碼,輕鬆找到重新整理介面

可以發現,想要獲取二維碼需要兩個關鍵的資訊:token,卡號
而透過介面可以獲取二維碼字串,頭像地址
能夠成功抓到介面資訊,再使用程式碼實現就簡單多了

整體的應用使用springboot來實現

1.3 模擬請求重新整理二維碼

這裡使用了webClient傳送請求
上程式碼:

public HashMap<String, String> reqQR(int cardId) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader("token.txt"));
        String token = reader.readLine();
        String cardNo = "";
        if (cardId == 0){
            cardNo = cardNo0;
        }else
            if (cardId == 1){
                cardNo = cardNo1;
        }
            log.info("card: {}",cardNo);
        String url = "https://app.com/appGzh/getQRcode";
        String requestBody = "{\"token\":\"" + token + "\",\"isDefault\":true,\"CardNo\":\""+ cardNo +"\",\"isTrue\":\"1\"}";
        log.info(requestBody);
        byte[] response = webClient.post()
                .uri(url)
                .header(HttpHeaders.HOST, "app.com")
                .header(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11275")
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
                .header(HttpHeaders.ACCEPT, "*/*")
                .header(HttpHeaders.REFERER, "")
                .header(HttpHeaders.ACCEPT_LANGUAGE, "zh-CN,zh")
                .header("xweb_xhr", "1")
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(byte[].class)
                .block();
        assert response != null;
        String responseStr = new String(response, StandardCharsets.UTF_8);
        log.info(responseStr);
        log.info(cardNo);
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonNode = mapper.readTree(responseStr);

        HashMap<String, String> map = new HashMap<>();
        map.put("picPath",jsonNode.get("pic_Path").asText());
        map.put("qrStr",jsonNode.get("qr_Str").asText());
        return map;
    }

透過map封裝返回了二維碼字串,頭像地址資訊

2. 生成二維碼

二維碼本質上就是字串,獲取到字串後再轉換為二維碼形式
二維碼的解析與生成選擇了google的zxing
pom導包如下:

        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.5.1</version>
        </dependency>

程式碼:

public File conductQR(int cardId) throws IOException {
        String QrString = reqQR(cardId).get("qrStr");
        int width = 300;
        int height = 300;
        String filePath = "qr_code_"+cardId+".png";

        try {
            BitMatrix bitMatrix = new QRCodeWriter().encode(QrString, BarcodeFormat.QR_CODE, width, height);
            Path path = FileSystems.getDefault().getPath(filePath);
            MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
            log.info(" create QR code");
        } catch (WriterException | IOException e) {
            e.printStackTrace();
        }
        return new File(filePath);
    }

可以成功生成二維碼:

3. 生成展示圖片

能夠成功生成二維碼,是不是可以更進一步做的更像些。
可以在模擬的背景圖上畫出二維碼

3.1 畫二維碼

 int qrWidth = 380;
        int qrHeight = 380;
        String charset = "UTF-8";
        Map<EncodeHintType, ErrorCorrectionLevel> hintMap = new HashMap<>();
        hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        //獲取二維碼字串
        HashMap<String, String> infoMap = reqQR(cardId);
        String QrString =infoMap.get("qrStr");
        // 生成二維碼
        BitMatrix bitMatrix = new MultiFormatWriter().encode(
                QrString,
                BarcodeFormat.QR_CODE,
                qrWidth,
                qrHeight,
                hintMap);
        BufferedImage qrImage = toBufferedImage(bitMatrix);

在二維碼中心繪製logo

BufferedImage logoImage = ImageIO.read(new File("logo.jpg"));
        // 在二維碼中間繪製 logo
        log.info("draw logo");
        int qrImageWidth = qrImage.getWidth();
        int qrImageHeight = qrImage.getHeight();
        int logoWidth = logoImage.getWidth()/3;
        int logoHeight = logoImage.getHeight()/3;
        int x_qr = (qrImageWidth - logoWidth) / 2;
        int y_qr = (qrImageHeight - logoHeight) / 2;
        Graphics2D g2d_qr = qrImage.createGraphics();
        g2d_qr.drawImage(logoImage, x_qr, y_qr, logoWidth, logoHeight, null);
        g2d_qr.dispose();

3.3 動態頭像

透過之前的reqQR(int cardId) 可以拿到頭像的地址
將圖片放到本地:
在這裡使用了一個靜態方法,可以將網路地址上的檔案儲存到本地。

    /**
     * 將圖片轉為file
     *
     * @param urlPath 圖片url
     * @return File
     */
    private static void saveFile(String urlPath,String filePath) throws Exception {
        // 構造URL
        URL url = new URL(urlPath);
        // 開啟連線
        URLConnection con = url.openConnection();
        // 輸入流
        InputStream is = con.getInputStream();
        // 1K的資料緩衝
        byte[] bs = new byte[1024];
        // 讀取到的資料長度
        int len;
        // 輸出的檔案流
        String filename = filePath;  //下載路徑及下載圖片名稱
        File file = new File(filename);
        FileOutputStream os = new FileOutputStream(file, true);
        // 開始讀取
        while ((len = is.read(bs)) != -1) {
            os.write(bs, 0, len);
        }
        // 完畢,關閉所有連結
        os.close();
        is.close();
    }

在背景圖片上繪製頭像

        String urlPath = infoMap.get("picPath");
        log.info("avatarPath:{}", urlPath);
        //獲取頭像到本地
        saveFile(urlPath,"avatarPic_"+cardId+".jpg");

        BufferedImage avatarImage = ImageIO.read(new File("avatarPic_"+cardId+".jpg"));
        int avatarWidth = avatarImage.getWidth()/2;
        int avatarHeight = avatarImage.getHeight()/2;
        // 建立一個與頭像相同大小的圓形遮罩
        log.info("draw avatar......");
        BufferedImage mask = new BufferedImage(avatarWidth, avatarHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D maskGraphics = mask.createGraphics();
        maskGraphics.setComposite(AlphaComposite.Clear);
        maskGraphics.fillRect(0, 0, avatarWidth, avatarHeight);
        maskGraphics.setComposite(AlphaComposite.Src);
        maskGraphics.setColor(Color.WHITE);
        maskGraphics.fill(new Ellipse2D.Float(0, 0, avatarWidth, avatarHeight));
        // 在頭像上應用圓形遮罩
        BufferedImage roundedAvatar = new BufferedImage(avatarWidth, avatarHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D avatarGraphics = roundedAvatar.createGraphics();
        avatarGraphics.drawImage(avatarImage, 0, 0, null);
        avatarGraphics.setComposite(AlphaComposite.DstIn);
        avatarGraphics.drawImage(mask, 0, 0, null);
        avatarGraphics.dispose();
        // 在背景圖片上方繪製圓形頭像
        Graphics2D g2d_avator = backgroundImage.createGraphics();
        g2d_avator.drawImage(roundedAvatar, (bgWidth - avatarWidth) / 2, 20, avatarWidth, avatarHeight, null);
        g2d_avator.dispose();

3.4 二維碼繪製到背景圖上

 // 讀取背景圖片
        //String backgroundPath = "background_"+cardId+".jpg";
        String backgroundPath = "background.jpg";
        BufferedImage backgroundImage = ImageIO.read(new File(backgroundPath));
        int bgWidth = backgroundImage.getWidth();
        int bgHeight = backgroundImage.getHeight();
 log.info("draw QR code......");
        Graphics2D g2d = backgroundImage.createGraphics();
        int x = (bgWidth - qrWidth) / 2;
        int y = (bgHeight - qrHeight) / 2-60;
        g2d.drawImage(qrImage, x, y, qrWidth, qrHeight, null);
        g2d.dispose();

        // 儲存合成後的圖片
        ImageIO.write(backgroundImage, "png", new File("imageCode_"+cardId+".png"));

3.5 最終效果:
訪問介面: http://localhost:8080/getImage/1/615Q66
成功在瀏覽器上獲取二維碼圖片
本地對比下,右邊為小程式原始截圖,左邊為生成圖片

4. 其他

為了確保二維碼使用的時效性,新增了一個重置介面。

4.1 重置

生成一個隨機的字串,拼接到請求路徑。
當需要過期操作時,可以重置字串進而重置訪問路徑,以此確保時效性。

 public String buildRandomID() throws IOException {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            // 生成大寫字母或數字
            int choice = random.nextInt(2);
            if (choice == 0) {
                // 生成大寫字母(A-Z 的 ASCII 碼範圍是 65-90)
                sb.append((char) (random.nextInt(26) + 65));
            } else {
                // 生成數字(0-9 的 ASCII 碼範圍是 48-57)
                sb.append((char) (random.nextInt(10) + 48));
            }
        }
            FileWriter fw = new FileWriter("RandomID.txt",false);
            fw.write(sb.toString());
        log.info("reset ID:{}", sb.toString());
            fw.close();
        return sb.toString();
    }

4.2 校驗

 public boolean verifyID(String id) throws IOException {
        String filePath = "RandomID.txt";
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line = reader.readLine();
            log.info("verify ID:{}", line);
                return line.equals(id);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;

    }

處理請求之前先進行校驗:

if(qrService.verifyID(randomID))

4.3 多張卡操作

可以將卡號入參,但卡號不能直接暴露在外,目前的解決方案是用 switch case ,將卡號儲存到yml裡
目前只有兩張卡,先簡單用else if

 String cardNo = "";
        if (cardId == 0){
            cardNo = cardNo0;
        }else
            if (cardId == 1){
                cardNo = cardNo1;
        }

相關文章