java使用hutool工具生成二维码
工具类代码较长,放在文章末尾提供
一、 先引入需要使用到的依赖
maven引入
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- ZXing 核心 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.3</version>
</dependency>
<!-- ZXing Java SE 扩展(用于 BufferedImage 转换) -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.3</version>
</dependency>
gradle引入
implementation 'cn.hutool:hutool-all:5.8.25'
implementation 'com.google.zxing:core:3.4.1'
implementation 'com.google.zxing:javase:3.4.1'
二、代码使用示例
工具类中提供直接生成图像、或者在指定路径生成对应的二维码,考虑到对于二维码的安全性,提供安全生成和没有限制的生成方式,根据自身业务选择即可
此处只做安全生成二维码的示例,直接生成二维码传入需要参数即可
生成单个二维码
使用CustomQrCodeGenerator.generateSecureQrImage方法生成,传入给定的类型、id,这两个参数会进行加密存储,传入-1则代表当前二维码永不过期,输入正整数作为秒数计算二维过期时间。
/**
* 生成二维码 不限制过期时间
* @param type 设备类型
* @param id 设备id
* @param response 响应
* @throws Exception 异常
*/
@GetMapping("/generateQrSecure")
public void generateQr(
@RequestParam String type,
@RequestParam Long id,
HttpServletResponse response) throws Exception {
// 安全生成二维码图像
BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1);
response.setContentType("image/png");
ImageIO.write(bufferedImage, "png", response.getOutputStream());
}
生成指定样式二维码
BufferedImage传入二维码logo格式
当前方法通过BufferedImage传入二维码logo格式,CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoImage);在设置null处是指定二维码的颜色,传null会默认处理样式,传入指定的颜色,会定制生成
InputStream logoInputStream = getClass().getClassLoader()
.getResourceAsStream(“static/logo/apple-touch-icon.png”);是需要在你的项目的resources下有对应的logo图片,否则无法获取生成对应的二维码含logo样式
/**
* 生成指定logo的二维码 BufferedImage指定logo类型
* @param type 设备类型
* @param id 设备id
* @param response 响应
* @throws Exception 异常
*/
@GetMapping("/generateQrSecureWithLogo")
public void generateQrSecureWithLogo(
@RequestParam String type,
@RequestParam Long id,
HttpServletResponse response) throws Exception {
// 从 classpath 加载 logo
InputStream logoInputStream = getClass().getClassLoader()
.getResourceAsStream("static/logo/apple-touch-icon.png");
if (logoInputStream == null) {
throw new FileNotFoundException("Logo 文件未找到: logo/apple-touch-icon.png");
}
BufferedImage logoImage = ImageIO.read(logoInputStream);
// 安全生成二维码图像
BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoImage);
response.setContentType("image/png");
ImageIO.write(bufferedImage, "png", response.getOutputStream());
}
File传入二维码logo格式
当前使用File格式进行二维码的生成,要确保指定路径的图片存在,在生产环境可配置在配置文件中获取,但建议可以放在resorces下获取
/**
* 生成指定logo的二维码 File指定logo类型
* @param type 设备类型
* @param id 设备id
* @param response 响应
* @throws Exception 异常
*/
@GetMapping("/generateQrSecureFileWithLogo")
public void generateQrSecureFileWithLogo(
@RequestParam String type,
@RequestParam Long id,
HttpServletResponse response) throws Exception {
// 指定 Logo 文件路径(必须是真实存在的文件)
File logoFile = new File("D:/qrCode/apple-touch-icon.png");
// 验证 Logo 文件是否存在
if (!logoFile.exists()) {
throw new RuntimeException("Logo 文件不存在: " + logoFile.getAbsolutePath());
}
// 安全生成二维码图像
BufferedImage bufferedImage = CustomQrCodeGenerator.generateSecureQrImage(type, id, -1, null, null, logoFile);
response.setContentType("image/png");
ImageIO.write(bufferedImage, "png", response.getOutputStream());
}
批量生成二维码
idList.parallelStream()使用并行流处理数据,保证速度,如果数据过多建议分批处理,路径可以在配置文件配置再进行获取,此处方便展示,可根据实际需求更改
CustomQrCodeGenerator.saveSecureQrToFile当前方法,会将二维码生成为文件的形式,保存在指定的路径,如果有定制二维码的需求,可以调用对应的方法,原理同上述一致
/**
* 批量生成二维码
* @throws Exception 异常
*/
@GetMapping("/generateBatchQr")
public void generateBatchQr() throws Exception {
String basePath = "D:/qrCode/";
File baseDir = new File(basePath);
if (!baseDir.exists()) {
boolean created = baseDir.mkdirs();
if (!created) {
throw new RuntimeException("创建目录失败: " + basePath);
}
}
List<Device> list = deviceService.list();
List<Long> idList = list.stream().map(Device::getId).toList();
AtomicInteger count = new AtomicInteger(0);
idList.parallelStream().forEach(id -> {
try {
CustomQrCodeGenerator.saveSecureQrToFile("device", id, -1, basePath);
count.incrementAndGet();
}catch (Exception e){
e.printStackTrace();
}
});
System.out.println("共计设备:" + idList.size() + "生成二维码数量:" + count.get());
}
加密二维码解析
CustomQrCodeGenerator.verifyAndDecrypt方法是用来解析你在扫描二维码之后,获取到的加密数据,将加密的数据传入进来,方法会解析为type=xxx&id=123这样的数据,就是你生成二维码时候指定的数据,parseQueryString是方便将数据解析,以便于拿到对应的类型、id,可以在后续你需要获取某个id数据的详细信息使用,同时可通过使用规定的type类型调用不同接口获取不同数据,如果还需要别的参数可以自行在工具类添加。
/**
* 测试扫描二维码获得加密后的数据 进行解析
* @param payload 加密后的数
*/
@GetMapping("/scan")
public CommonResult<Object> scan(@RequestParam String payload) {
String decrypt = CustomQrCodeGenerator.verifyAndDecrypt(payload);
Map<String, String> queryMap = parseQueryString(decrypt);
String s = queryMap.get("type");
System.out.println("type:" + s);
String s1 = queryMap.get("id");
System.out.println("id:" + s1);
return CommonResult.ok(s1);
}
// 辅助方法:将 "type=xxx&id=123" 转为 Map
private static Map<String, String> parseQueryString(String query) {
Map<String, String> map = new HashMap<>();
if (query == null || query.isEmpty()) {
return map;
}
for (String pair : query.split("&")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
map.put(kv[0], kv[1]);
} else if (kv.length == 1) {
map.put(kv[0], "");
}
}
return map;
}
三、工具类代码
当前工具类在测试使用hutool的5.4.0、5.8.25均可使用
package com.basic.localcode.common.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
/**
* 自定义二维码生成器
* 功能:
* - 普通二维码:支持 Logo、前景/背景色(可选)
* - 安全二维码:加密 + 签名 + 过期时间(支持 -1 表示永不过期)
*
* 使用说明:
* - 调用简洁方法(如 saveSecureQrToFile(type, id, expire, path))将使用默认样式
* - 如需自定义颜色或 Logo,请使用完整参数版本
* - Logo 支持 File 或 BufferedImage(推荐从 classpath 加载为 BufferedImage)
*/
public class CustomQrCodeGenerator {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 300;
private static final int DEFAULT_MARGIN = 1;
// 安全密钥(生产环境请外部化到配置文件或环境变量)
// 必须 16 字节(AES-128)
private static final String SECRET_KEY = "MySecureKey12345";
private static final String HMAC_SECRET = "HmacSecretForQR!@#";
// ==================== 【1】基础普通二维码(默认样式)====================
public static BufferedImage generateQrImage(String type, Long id) {
return generateQrImage(type, id, null, null, (File) null);
}
public static void saveQrToFile(String type, Long id, String basePath) {
saveQrToFile(type, id, basePath, null, null, (File) null);
}
// ==================== 【2】普通二维码(带样式,File logo)====================
public static BufferedImage generateQrImage(
String type,
Long id,
Color foreColor,
Color backColor,
File logoFile) {
String content = "type=" + type + "&id=" + id;
return createQrCode(content, foreColor, backColor, logoFile);
}
public static void saveQrToFile(
String type,
Long id,
String basePath,
Color foreColor,
Color backColor,
File logoFile) {
BufferedImage image = generateQrImage(type, id, foreColor, backColor, logoFile);
String fileName = String.format("%s_%s.png", type, id);
File file = new File(basePath, fileName);
file.getParentFile().mkdirs();
try {
ImageIO.write(image, "png", file);
} catch (IOException e) {
throw new RuntimeException("生成二维码失败", e);
}
}
// ==================== 【2b】普通二维码(带样式,BufferedImage logo)====================
public static BufferedImage generateQrImage(
String type,
Long id,
Color foreColor,
Color backColor,
BufferedImage logoImage) {
String content = "type=" + type + "&id=" + id;
return createQrCode(content, foreColor, backColor, logoImage);
}
public static void saveQrToFile(
String type,
Long id,
String basePath,
Color foreColor,
Color backColor,
BufferedImage logoImage) {
BufferedImage image = generateQrImage(type, id, foreColor, backColor, logoImage);
String fileName = String.format("%s_%s.png", type, id);
File file = new File(basePath, fileName);
file.getParentFile().mkdirs();
try {
ImageIO.write(image, "png", file);
} catch (IOException e) {
throw new RuntimeException("生成二维码失败", e);
}
}
// ==================== 【3】安全二维码(简洁重载,默认样式)====================
public static BufferedImage generateSecureQrImage(String type, Long id, long expireInSeconds) {
return generateSecureQrImage(type, id, expireInSeconds, null, null, (File) null);
}
public static void saveSecureQrToFile(String type, Long id, long expireInSeconds, String basePath) {
saveSecureQrToFile(type, id, expireInSeconds, basePath, null, null, (File) null);
}
// ==================== 【3a】安全二维码(完整参数,File logo)====================
public static BufferedImage generateSecureQrImage(
String type,
Long id,
long expireInSeconds,
Color foreColor,
Color backColor,
File logoFile) {
String raw = buildRawContent(type, id, expireInSeconds);
String encrypted = encryptContent(raw);
String signed = signContent(encrypted);
return createQrCode(signed, foreColor, backColor, logoFile);
}
public static void saveSecureQrToFile(
String type,
Long id,
long expireInSeconds,
String basePath,
Color foreColor,
Color backColor,
File logoFile) {
BufferedImage image = generateSecureQrImage(type, id, expireInSeconds, foreColor, backColor, logoFile);
String fileName = String.format("%s_%s_secure.png", type, id);
File file = new File(basePath, fileName);
file.getParentFile().mkdirs();
try {
ImageIO.write(image, "png", file);
} catch (IOException e) {
throw new RuntimeException("生成安全二维码失败", e);
}
}
// ==================== 【3b】安全二维码(完整参数,BufferedImage logo)✅ 新增 ====================
public static BufferedImage generateSecureQrImage(
String type,
Long id,
long expireInSeconds,
Color foreColor,
Color backColor,
BufferedImage logoImage) {
String raw = buildRawContent(type, id, expireInSeconds);
String encrypted = encryptContent(raw);
String signed = signContent(encrypted);
return createQrCode(signed, foreColor, backColor, logoImage);
}
public static void saveSecureQrToFile(
String type,
Long id,
long expireInSeconds,
String basePath,
Color foreColor,
Color backColor,
BufferedImage logoImage) {
BufferedImage image = generateSecureQrImage(type, id, expireInSeconds, foreColor, backColor, logoImage);
String fileName = String.format("%s_%s_secure.png", type, id);
File file = new File(basePath, fileName);
file.getParentFile().mkdirs();
try {
ImageIO.write(image, "png", file);
} catch (IOException e) {
throw new RuntimeException("生成安全二维码失败", e);
}
}
// ==================== 【4】核心:使用 ZXing 生成二维码(File logo)====================
private static BufferedImage createQrCode(
String content,
Color foreColor,
Color backColor,
File logoFile) {
try {
java.util.Map<EncodeHintType, Object> hints = new java.util.HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, DEFAULT_MARGIN);
BitMatrix bitMatrix = new MultiFormatWriter().encode(
content,
BarcodeFormat.QR_CODE,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
hints
);
int fore = foreColor != null ? foreColor.getRGB() : 0xFF000000;
int back = backColor != null ? backColor.getRGB() : 0xFFFFFFFF;
MatrixToImageConfig config = new MatrixToImageConfig(fore, back);
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config);
if (logoFile != null && logoFile.exists()) {
BufferedImage logo = ImageIO.read(logoFile);
qrImage = addLogo(qrImage, logo);
}
return qrImage;
} catch (Exception e) {
throw new RuntimeException("生成二维码失败", e);
}
}
// ==================== 【4b】核心:使用 ZXing 生成二维码(BufferedImage logo)✅ 新增 ====================
private static BufferedImage createQrCode(
String content,
Color foreColor,
Color backColor,
BufferedImage logoImage) {
try {
java.util.Map<EncodeHintType, Object> hints = new java.util.HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, DEFAULT_MARGIN);
BitMatrix bitMatrix = new MultiFormatWriter().encode(
content,
BarcodeFormat.QR_CODE,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
hints
);
int fore = foreColor != null ? foreColor.getRGB() : 0xFF000000;
int back = backColor != null ? backColor.getRGB() : 0xFFFFFFFF;
MatrixToImageConfig config = new MatrixToImageConfig(fore, back);
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix, config);
if (logoImage != null) {
qrImage = addLogo(qrImage, logoImage);
}
return qrImage;
} catch (Exception e) {
throw new RuntimeException("生成二维码失败", e);
}
}
// ==================== 【5】Logo 处理逻辑 ====================
/**
* 将 BufferedImage 类型的 logo 添加到二维码中心
*/
private static BufferedImage addLogo(BufferedImage qrImage, BufferedImage logoImage) {
int qrWidth = qrImage.getWidth();
int qrHeight = qrImage.getHeight();
int logoWidth = logoImage.getWidth();
int logoHeight = logoImage.getHeight();
int maxSize = Math.min(qrWidth / 5, qrHeight / 5);
if (logoWidth > maxSize || logoHeight > maxSize) {
double scale = (double) maxSize / Math.max(logoWidth, logoHeight);
logoWidth = (int) (logoWidth * scale);
logoHeight = (int) (logoHeight * scale);
Image scaledLogo = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);
BufferedImage scaledBufferedLogo = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scaledBufferedLogo.createGraphics();
g.drawImage(scaledLogo, 0, 0, null);
g.dispose();
logoImage = scaledBufferedLogo;
}
int x = (qrWidth - logoWidth) / 2;
int y = (qrHeight - logoHeight) / 2;
Graphics2D g = qrImage.createGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
g.drawImage(logoImage, x, y, null);
g.dispose();
return qrImage;
}
// ==================== 【6】安全逻辑 ====================
private static String buildRawContent(String type, Long id, long expireInSeconds) {
StringBuilder sb = new StringBuilder();
sb.append("type=").append(type)
.append("&id=").append(id);
if (expireInSeconds != -1) {
long expireAt = Instant.now().plus(Duration.ofSeconds(expireInSeconds)).toEpochMilli();
sb.append("&expire=").append(expireAt);
} else {
sb.append("&expire=-1");
}
return sb.toString();
}
private static String encryptContent(String content) {
AES aes = SecureUtil.aes(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
return Base64.encode(aes.encrypt(content));
}
private static String signContent(String encryptedContent) {
byte[] hmac = SecureUtil.hmacSha256(HMAC_SECRET).digest(encryptedContent);
return encryptedContent + "." + HexUtil.encodeHexStr(hmac);
}
// ==================== 【7】验证与解密 ====================
public static String verifyAndDecrypt(String qrPayload) {
String[] parts = qrPayload.split("\\.", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("二维码格式无效");
}
String encrypted = parts[0];
String sig = parts[1];
String expectedSig = HexUtil.encodeHexStr(SecureUtil.hmacSha256(HMAC_SECRET).digest(encrypted));
if (!expectedSig.equals(sig)) {
throw new SecurityException("签名不匹配!");
}
AES aes = SecureUtil.aes(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
String raw = new String(aes.decrypt(Base64.decode(encrypted)), StandardCharsets.UTF_8);
for (String param : raw.split("&")) {
if (param.startsWith("expire=")) {
String expireStr = param.substring(7);
if ("-1".equals(expireStr)) {
return raw;
}
try {
long expireTime = Long.parseLong(expireStr);
if (System.currentTimeMillis() > expireTime) {
throw new IllegalStateException("二维码失效!");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("无效的过期时间戳");
}
break;
}
}
return raw;
}
}
1万+

被折叠的 条评论
为什么被折叠?



