閒來無事做一個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);
3.2 二維碼中心logo
在二維碼中心繪製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;
}