关于 MySQL 的三个小问题

之前曾经写过MySQL的一个小问题,就是 MySQL 如果采用的字符集是 utf8 的话,emoji 的话,是存不进去的,因为历史原因 MySQL 的 utf8 不是真正的 utf8,utf8mb4 才是真正的 utf8,所以在创建数据库的时候编码集推荐选择 真正的 utf8mb4。今天就再写两个小问题: allowMultiQueries=true 前一段时间同事有个需求,使用 mybatis 的时候,在同一个 mapper 方法中需要同时执行两个 MySQL 语句,当时同事说他把 SQL 拷贝出来执行都没有问题,但是在代码中执行就是报错,让我帮忙看一下原因,当时听他描述,我大概猜到了,然后一看果然就是在一个 mapper 方法中同时执行两个 SQL 语句,在 MySQL 的连接中加上 allowMultiQueries=true 搞定。 MySQL8.0 中 group by 报错的问题 可能因为我一直在互联网公司的原因,所以在我的职业生涯中,一直都是 MySQL,前几年在 MySQL8.0 还没成为主流,主流还是使用 5.6 的时候,有个刚入职不久的同事问如下的语句: select a, b from t_test group by a 说不报错吗?当时我很懵,不报错啊,代码中很多地方我也一直这么写啊,然后同事说他之前用 oracle 是不行的,然后到 MySQL5.7 之后,发现 MySQL 默认也不能这么写了,其实解决起来也很简单。 第一种方案: 修改 MySQL 的语句: select a from t_test group by a 或者: ...

October 19, 2024 · 1 min · 125 words · Bridge Li

全国中小企业融资综合信用服务平台-省级节点数据接口规范-河南省营商环境和社会信用建设中心

开始之前先说一点题外话,几年前曾经看过一个视频,其中一个观点大概就是程序员是一个反传统的群体,其他群体掌握了某个技术,一般都是当做内部商业机密,而程序员则不一样,喜欢开源,尤其 GPL 协议的开源,不仅自己毫无保留的开源,还要求使用他的软件也得开源,也正是这种开源造就了互联网的蓬勃发展。我目前所在公司因为是做金融相关的公司,国家出于某些原因,要求要上报相关的数据到省平台,而省平台的技术采用的是 webservice,和我们目前习惯的 http 接口不太一样,所以前一段时间在写这个的时候走了不少弯路,而网上也没有参考资料,所以决定把相关的核心代码公布出来,供需要的同学参考。需要说明的是:这是我们河南省的系统相关接口,不知道外省是否一致,省平台给的接口文档名是:全国中小企业融资综合信用服务平台省级节点数据接口规范V5.3.pdf,首页写的是:全国中小企业融资综合信用服务平台省级节点数据接口规范,国家公共信用信息中心,河南省营商环境和社会信用建设中心,2024年7月 jar 包引入 <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.10</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>4.0.5</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>4.0.5</version> </dependency> 上报工具类,其中:queryData 方法是用来查询,sendData 方法用来上报数据 package cn.bridgeli.demo; import cn.hutool.core.util.RandomUtil; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.apache.cxf.endpoint.Client; import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import javax.xml.namespace.QName; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.PublicKey; @Slf4j public class XyhnUtil { private static String pubKey; private static String queryPubKey; private static PublicKey reportPublicKey; // private static XyhnConfig xyhnConfig = SpringUtils.getBean(XyhnConfig.class); private static XyhnConfig xyhnConfig = null; /** * 授权查询省平台接口 * * @param method 要调取的方法名 * @param object 查询方法的参数(不包含:publicKey、appKey) * @return 省平台解密后的数据 */ public static String queryData(String method, JSONObject object) { //1、组装报文 if (StringUtils.isEmpty(queryPubKey)) { queryPubKey = convertFileToBase64(xyhnConfig.getQueryPubKeyPath()); } object.put("publicKey", queryPubKey); object.put("appKey", xyhnConfig.getQueryAppKey()); //2、发送报文 String jsonRes = callInterface("query", method, object.toJSONString()); //3、解密返回数据 JSONObject json = JSONObject.parseObject(jsonRes); Boolean success = json.getBoolean("success"); if ("uploadLicense".equals(method)) { if (null == success || !success) { return jsonRes; } } else if ("cancelLicense".equals(method)) { if (null != success && success) { return jsonRes; } } BigInteger d = new BigInteger(xyhnConfig.getQueryPriKey(), 16); BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d); String key0 = json.getString("key"); String data = json.getString("data"); String signatureData = json.getString("signatureData"); byte[] decode = Hex.decode(key0); // sm2解密 byte[] bytes1 = GMUtil.sm2Decrypt(decode, bcecPrivateKey); // sm4解密 String content = GMUtil.sm4Decrypt(new String(bytes1), data); log.info("content: " + content); // 4、验签 File file = base64ToFileEx(json.getString("pubKey")); PublicKey publicKey = GMUtil.getPublickeyFromX509File(file); byte[] signatureData1 = Hex.decode(signatureData); boolean verifyRes = GMUtil.verifySm3WithSm2(content.getBytes(), xyhnConfig.getUserId().getBytes(), signatureData1, publicKey); log.info("method 验签结果:" + verifyRes); if (!verifyRes) { return null; } return content; } private static PublicKey getPublicKey() { //1、组装报文 if (StringUtils.isEmpty(pubKey)) { pubKey = convertFileToBase64(xyhnConfig.getPubKeyPath()); } JSONObject jsonObject = new JSONObject(); jsonObject.put("key", xyhnConfig.getAppKey()); JSONObject object = new JSONObject(); object.put("requestData", jsonObject); object.put("publicKey", pubKey); //2、发送报文 String jsonRes = callInterface("report", "getPublicKey", object.toJSONString()); //3、解密返回数据 JSONObject json = JSONObject.parseObject(jsonRes); BigInteger d = new BigInteger(xyhnConfig.getPriKey(), 16); BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d); String key0 = json.getString("key"); String data = json.getString("data"); String signatureData = json.getString("signatureData"); byte[] decode = Hex.decode(key0); // sm2解密 byte[] bytes1 = GMUtil.sm2Decrypt(decode, bcecPrivateKey); // sm4解密 String returnData = GMUtil.sm4Decrypt(new String(bytes1), data); log.info("returnData:" + returnData); // 4、验签 File file = base64ToFileEx(returnData); PublicKey publicKey = GMUtil.getPublickeyFromX509File(file); byte[] signatureData1 = Hex.decode(signatureData); boolean verifyRes = GMUtil.verifySm3WithSm2(returnData.getBytes(), xyhnConfig.getUserId().getBytes(), signatureData1, publicKey); log.info("getPublicKey 接口验签结果:" + verifyRes); if (!verifyRes) { return null; } return publicKey; } public static String sendData(String jsonStr, String method) { log.info("调用省平台方法名:" + method + ",参数:" + jsonStr); try { if (StringUtils.isEmpty(pubKey)) { // 解析公钥 pubKey = convertFileToBase64(xyhnConfig.getPubKeyPath()); } if (reportPublicKey == null) { //请求接口,获取公钥 reportPublicKey = getPublicKey(); } // 1、摘要签名 sm2withsm3 byte[] msg = jsonStr.getBytes(CharEncoding.ISO_8859_1); byte[] userIdBytes = xyhnConfig.getUserId().getBytes(); BigInteger d = new BigInteger(xyhnConfig.getPriKey(), 16); BCECPrivateKey bcecPrivateKey = GMUtil.getPrivatekeyFromD(d); byte[] sig = GMUtil.signSm3WithSm2(msg, userIdBytes, bcecPrivateKey); String signature = Hex.toHexString(sig); // 2、sm4加密数据报文 String key1 = RandomUtil.randomString(16); String jsonobj = GMUtil.sm4Encrypt(key1, jsonStr); //3、sm2加密16位随机码key byte[] datamsg = GMUtil.sm2Encrypt(key1.getBytes(), reportPublicKey); String s = Hex.toHexString(datamsg); //4、组装并发送报文 JSONObject jsonObject2 = new JSONObject(); jsonObject2.put("requestData", jsonobj); // sm4加密的数据集 jsonObject2.put("key", s); // sm2加密的16位随机码 jsonObject2.put("signatureData", signature); // 签名 jsonObject2.put("publicKey", pubKey); // 我的公钥 jsonObject2.put("appKey", xyhnConfig.getAppKey()); // 数据授权的key String setFPRes = callInterface("report", method, jsonObject2.toJSONString()); return setFPRes; } catch (Exception ex) { log.error("请求省平台接口异常", ex); return null; } } /** * 请求接口 * * @param type,report 回传数据,query 查询 * @param method * @param json * @return */ private static String callInterface(String type, String method, String json) { log.info("调用省平台类型:" + type + ",接口:" + method + ",参数:" + json); Client client = null; QName name = null; JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance(); String result = null; try { if ("report".equals(type)) { client = dcf.createClient(xyhnConfig.getWsAddr()); name = new QName(xyhnConfig.getNamespaceURI(), method); } else if ("query".equals(type)) { client = dcf.createClient(xyhnConfig.getQueryWsAddr()); name = new QName(xyhnConfig.getQueryNamespaceURI(), method); } Object[] objects = client.invoke(name, json); result = objects[0].toString(); } catch (Exception e) { log.error("调用省平台接口报错", e); } finally { if (client != null) { try { client.close(); } catch (Exception e) { log.error("关闭Client资源时发生异常", e); } } } log.info("调用省平台类型:" + type + ",接口:" + method + ",返回值:" + result); return result; } private static String convertFileToBase64(String imgPath) { byte[] data = null; // 读取图片字节数组 try (InputStream in = new FileInputStream(imgPath);) { data = new byte[in.available()]; in.read(data); } catch (IOException e) { log.error("IOException", e); } // 对字节数组进行Base64编码,得到Base64编码的字符串 String base64Str = java.util.Base64.getEncoder().encodeToString(data); return base64Str; } private static File base64ToFileEx(String base64) { if (StringUtils.isBlank(base64)) { return null; } byte[] buff = Base64.decode(base64); File file = null; FileOutputStream fout = null; try { file = File.createTempFile("tmp", null); fout = new FileOutputStream(file); fout.write(buff); file.deleteOnExit(); } catch (IOException e) { e.printStackTrace(); } finally { if (fout != null) { try { fout.close(); } catch (IOException e) { e.printStackTrace(); } } } return file; } } 用到的工具类 package cn.bridgeli.demo; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jcajce.spec.SM2ParameterSpec; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECPrivateKeySpec; import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.Signature; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Arrays; @Slf4j public class GMUtil { private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1"); private static ECDomainParameters ecDomainParameters; private static ECParameterSpec ecParameterSpec; public static byte[] signSm3WithSm2(byte[] msg, byte[] userId, PrivateKey privateKey) { return rsAsn1ToPlainByteArray(signSm3WithSm2Asn1Rs(msg, userId, privateKey)); } public static byte[] signSm3WithSm2Asn1Rs(byte[] msg, byte[] userId, PrivateKey privateKey) { try { SM2ParameterSpec parameterSpec = new SM2ParameterSpec(userId); Signature signer = Signature.getInstance("SM3withSM2", "BC"); signer.setParameter(parameterSpec); signer.initSign(privateKey, new SecureRandom()); signer.update(msg, 0, msg.length); byte[] sig = signer.sign(); return sig; } catch (Exception e) { throw new RuntimeException(e); } } public static boolean verifySm3WithSm2(byte[] msg, byte[] userId, byte[] rs, PublicKey publicKey) { return verifySm3WithSm2Asn1Rs(msg, userId, rsPlainByteArrayToAsn1(rs), publicKey); } public static boolean verifySm3WithSm2Asn1Rs(byte[] msg, byte[] userId, byte[] rs, PublicKey publicKey) { try { SM2ParameterSpec parameterSpec = new SM2ParameterSpec(userId); Signature verifier = Signature.getInstance("SM3withSM2", "BC"); verifier.setParameter(parameterSpec); verifier.initVerify(publicKey); verifier.update(msg, 0, msg.length); return verifier.verify(rs); } catch (Exception e) { throw new RuntimeException(e); } } public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3) { int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; byte[] result = new byte[c1c2c3.length]; System.arraycopy(c1c2c3, 0, result, 0, c1Len); System.arraycopy(c1c2c3, c1c2c3.length &#8211; 32, result, c1Len, 32); System.arraycopy(c1c2c3, c1Len, result, c1Len + 32, c1c2c3.length &#8211; c1Len &#8211; 32); return result; } public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2) { int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; byte[] result = new byte[c1c3c2.length]; System.arraycopy(c1c3c2, 0, result, 0, c1Len); System.arraycopy(c1c3c2, c1Len + 32, result, c1Len, c1c3c2.length &#8211; c1Len &#8211; 32); System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length &#8211; 32, 32); return result; } public static byte[] sm2Decrypt(byte[] data, PrivateKey key) { return sm2DecryptOld(changeC1C3C2ToC1C2C3(data), key); } public static byte[] sm2Encrypt(byte[] data, PublicKey key) { return changeC1C2C3ToC1C3C2(sm2EncryptOld(data, key)); } public static byte[] sm2EncryptOld(byte[] data, PublicKey key) { BCECPublicKey localECPublicKey = (BCECPublicKey) key; ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), ecDomainParameters); SM2Engine sm2Engine = new SM2Engine(); sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom())); try { return sm2Engine.processBlock(data, 0, data.length); } catch (InvalidCipherTextException e) { throw new RuntimeException(e); } } public static byte[] sm2DecryptOld(byte[] data, PrivateKey key) { BCECPrivateKey localECPrivateKey = (BCECPrivateKey) key; ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(localECPrivateKey.getD(), ecDomainParameters); SM2Engine sm2Engine = new SM2Engine(); sm2Engine.init(false, ecPrivateKeyParameters); try { return sm2Engine.processBlock(data, 0, data.length); } catch (InvalidCipherTextException e) { throw new RuntimeException(e); } } public static byte[] sm4Encrypt(byte[] keyBytes, byte[] plain) { byte[] keyBytes0; if (keyBytes.length != 16) { keyBytes0 = new byte[16]; for (int i = 0; i < keyBytes0.length; ++i) { if (keyBytes.length > i) { keyBytes0[i] = keyBytes[i]; } } keyBytes = keyBytes0; } if (plain.length % 16 != 0) { keyBytes0 = new byte[16 * (plain.length / 16 + 1)]; System.arraycopy(plain, 0, keyBytes0, 0, plain.length); plain = keyBytes0; } try { Key key = new SecretKeySpec(keyBytes, "SM4"); Cipher out = Cipher.getInstance("SM4/ECB/NoPadding", "BC"); out.init(1, key); return out.doFinal(plain); } catch (Exception e) { throw new RuntimeException(e); } } public static byte[] sm4Decrypt(byte[] keyBytes, byte[] cipher) { byte[] keyBytes0; if (keyBytes.length != 16) { keyBytes0 = new byte[16]; for (int i = 0; i < keyBytes0.length; ++i) { if (keyBytes.length > i) { keyBytes0[i] = keyBytes[i]; } } keyBytes = keyBytes0; } if (cipher.length % 16 != 0) { keyBytes0 = new byte[16 * (cipher.length / 16 + 1)]; System.arraycopy(cipher, 0, keyBytes0, 0, cipher.length); cipher = keyBytes0; } try { Key key = new SecretKeySpec(keyBytes, "SM4"); Cipher in = Cipher.getInstance("SM4/ECB/NoPadding", "BC"); in.init(2, key); byte[] bytes = in.doFinal(cipher); for (int i = bytes.length &#8211; 1; i >= 0; &#8211;i) { if (bytes[i] != 0) { byte[] bytes1 = new byte[i + 1]; System.arraycopy(bytes, 0, bytes1, 0, i + 1); bytes = bytes1; i = -1; } } return bytes; } catch (Exception e) { throw new RuntimeException(e); } } public static String sm4Encrypt(String key, String plan) { byte[] keyBytes = new byte[16]; byte[] keyBytes0 = key.getBytes(StandardCharsets.UTF_8); for (int i = 0; i < keyBytes.length; ++i) { if (keyBytes0.length > i) { keyBytes[i] = keyBytes0[i]; } } byte[] cipher = plan.getBytes(StandardCharsets.UTF_8); byte[] bytes = sm4Encrypt(keyBytes, cipher); return Hex.toHexString(bytes).toUpperCase(); } public static String sm4Decrypt(String key, String cipher) { byte[] keyBytes = new byte[16]; byte[] keyBytes0 = key.getBytes(StandardCharsets.UTF_8); for (int i = 0; i < keyBytes.length; ++i) { if (keyBytes0.length > i) { keyBytes[i] = keyBytes0[i]; } } byte[] cipherbytes = Hex.decode(cipher); byte[] bytes = sm4Decrypt(keyBytes, cipherbytes); return new String(bytes, StandardCharsets.UTF_8); } private static byte[] bigIntToFixexLengthBytes(BigInteger rOrS) { byte[] rs = rOrS.toByteArray(); if (rs.length == 32) { return rs; } else if (rs.length == 33 && rs[0] == 0) { return Arrays.copyOfRange(rs, 1, 33); } else if (rs.length < 32) { byte[] result = new byte[32]; Arrays.fill(result, (byte) 0); System.arraycopy(rs, 0, result, 32 &#8211; rs.length, rs.length); return result; } else { throw new RuntimeException("err rs: " + Hex.toHexString(rs)); } } private static byte[] rsAsn1ToPlainByteArray(byte[] rsDer) { ASN1Sequence seq = ASN1Sequence.getInstance(rsDer); byte[] r = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue()); byte[] s = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue()); byte[] result = new byte[64]; System.arraycopy(r, 0, result, 0, r.length); System.arraycopy(s, 0, result, 32, s.length); return result; } private static byte[] rsPlainByteArrayToAsn1(byte[] sign) { if (sign.length != 64) { throw new RuntimeException("err rs. "); } else { BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, 32)); BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, 32, 64)); ASN1EncodableVector v = new ASN1EncodableVector(); v.add(new ASN1Integer(r)); v.add(new ASN1Integer(s)); try { return (new DERSequence(v)).getEncoded("DER"); } catch (IOException var5) { throw new RuntimeException(var5); } } } public static BCECPrivateKey getPrivatekeyFromD(BigInteger d) { ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecParameterSpec); return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION); } public static PublicKey getPublickeyFromX509File(File file) { try { CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC"); FileInputStream in = new FileInputStream(file); X509Certificate x509 = (X509Certificate) cf.generateCertificate(in); return x509.getPublicKey(); } catch (Exception var4) { throw new RuntimeException(var4); } } static { ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN()); ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN()); if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } } } 相关配置 package cn.bridgeli.demo; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "xyhn") //@RefreshScope public class XyhnConfig { private String userId; private String platformId; private String appKey; private String priKey; private String wsAddr; private String namespaceURI; private String pubKeyPath; private String queryAppKey; private String queryPriKey; private String queryWsAddr; private String queryNamespaceURI; private String queryPubKeyPath; } 以上是上报和查询数据的核心方法,下面是查询具体数据的封装 ...

September 24, 2024 · 13 min · 2571 words · Bridge Li

使用 knife4j 实现 Swagger 文档增强

相信使用 Java 开发的人,对 Swagger 一定不会感到陌生,不过个人对 Swagger 一直没有太多好感,因为他的 UI 实在太难看了,用起来也颇为不顺手,所以国内有人开发了 knife4j 对 Swagger 进行增强,随着时间的推移,现在很多项目都在从 Java8 到 Java17,SpringBoot2 到 SpringBoot3 的迁移,发现 knife4j 现在也开始做了支持,而且用起来更方便。下面简单说一说如何使用。 引入依赖 <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>4.5.0</version> </dependency> 从中我们可以看到 artifactId 做了全新的修改,这个需要注意。另外 Spring Boot 3 只支持 OpenAPI3 规范。Knife4j提供的 starter 已经引用 springdoc-openapi 的 jar,大家需注意避免 jar 包冲突,引入之后,其余的配置,开发者即可完全参考 springdoc-openapi 的项目说明,Knife4j 只提供了增强部分,如果要启用 Knife4j 的增强功能,可以在配置文件中进行开启,其实个人测试就算完全不配置,此时也已经可以通过 http://ip:port/doc.html 查看文档: knife4j: enable: true basic: enable: true username: BridgeLi password: BridgeLi springdoc: default-flat-param-object: true 最后,使用 OpenAPI3 的规范注解,注释各个 Spring 的 Rest 接口。 ...

June 10, 2024 · 1 min · 205 words · Bridge Li

如何构建一个可重复读流 InputStream 的 HttpServletRequest?

之前在某公司工作的时候,领导要求所有前端向后端传递的参数都要经过前端加密,后端解密。说一句题外话:个人认为这种操作纯属脱裤子放屁,没啥用。因为前端代码都是公开的,无论你采用对称加密、非对称加密,或者摘要算法验签等等,对于稍懂技术的人来说,稍稍分析一下就能找到前端加密的方法,然后直接用相同的方式加密就行,所以这就是障眼法,只能骗骗不懂技术的人。不过领导的要求吗,既然定下来了,那么我们总要服从。因为每个方法都需要有这个解密或者验签的过程,我们自然而然想要到了通过 Filter、Interceptor 或者 AOP 等技术统一来做,不可能在各个方法中做这件事,在但是我们都知道,对于 post、put 等请求,参数都是放在请求体中的,需要通过流读出来,而流是不可以重复读的,所以我们应该怎么来解决这个问题,来构造一个可以重复读流 InputStream 的 HttpServletRequest。 解决方法:使用自定义类来缓存 stream 即可 RequestWrapper 类:缓存字节数据 package cn.bridgeli.filter; import cn.bridgeli.utils.http.HttpHelper; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * 构建可重复读取inputStream的request * * @author BridgeLi */ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RepeatedlyRequestWrapper(HttpServletRequest request) { super(request); body = HttpHelper.getBodyString(request).getBytes(StandardCharsets.UTF_8); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return bais.read(); } @Override public int available() throws IOException { return body.length; } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } } package cn.bridgeli.utils.http; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * 通用http工具封装 * * @author BridgeLi */ public class HttpHelper { private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); public static String getBodyString(ServletRequest request) { StringBuilder sb = new StringBuilder(); try (InputStream inputStream = request.getInputStream()) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { LOGGER.error("getBodyString出现问题!", e); } return sb.toString(); } } 然后,可以在 Servlet 或 Filter 中使用 RepeatableFilter 替换原始的 HttpServletRequest。 ...

May 3, 2024 · 2 min · 343 words · Bridge Li

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

自从 ChatGPT 横空出世之后,突然没了写博客的兴趣,因为无论有什么问题,随便往 ChatGPT 一帖,结果就哗哗的出来了,今天这个问题就是。前一段时间和某公司联调接口,对方的接口测试环境好好的,一上线调不通,报错信息就是标题,只见贴到 ChatGPT,ChatGPT 回答如下: 这个错误通常是由于缺少信任证书或证书链不完整导致的。解决方法通常有两种: **添加证书到信任库**:获取正确的证书,并将其添加到 Java 的信任库中。可以使用 `keytool` 工具来完成此操作。具体步骤如下: – 首先,使用以下命令导出证书: keytool -printcert -rfc -sslserver <hostname> -port <port> 其中 <hostname> 和 <port> 是你要访问的 HTTPS 接口的主机名和端口号。 – 然后将证书导出到一个文件中。 – 最后,使用以下命令将证书添加到 Java 的信任库中: keytool -importcert -file <certificate-file> -keystore <path-to-truststore> -alias <alias> 其中 <certificate-file> 是你导出的证书文件,<path-to-truststore> 是 Java 信任库的路径,<alias> 是证书的别名。 **忽略证书验证**:如果你确定你要连接的服务器是安全的,并且不需要严格的证书验证,你可以在 Java 代码中忽略证书验证。但这不是推荐的做法,因为可能会导致安全风险。你可以使用 `TrustManager` 接口实现自定义的信任管理器,然后将其应用于 SSL 连接。下面是一个简单的示例代码: import javax.net.ssl.*; import java.security.cert.X509Certificate; public class SSLCertificateIgnore { public static void main(String[] args) throws Exception { // Create a trust manager that does not validate certificate chains TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; // Install the all-trusting trust manager SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, null); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); // Now you can access the HTTPS endpoint without certificate validation } } 请注意,这种方法会降低安全性,因为它会使你的应用程序容易受到中间人攻击。因此,最好的做法是第一种方法,即添加正确的证书到信任库中。 ...

April 21, 2024 · 1 min · 146 words · Bridge Li

网站变成全局灰色

去年有个时期,国内各大网站纷纷变成了全局灰色,当时说这个事的时候,有同事不知道怎么实现的,认为是设计师重新做了一套 UI,前端程序员紧急上线的,其实并不用,说起来也非常简单,只需要在前端加入如下代码即可: html { -webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); filter: progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); }

March 26, 2023 · 1 min · 17 words · Bridge Li

AntPathMatcher 路径匹配

公司项目使用 AntPathMatcher 路径匹配是否登陆,之前没有接触过,刚好趁这次机会学习了一番。 一、基本规则 1、? 匹配一个字符(除过操作系统默认的文件分隔符) 2、* 匹配0个或多个字符 3、** 匹配0个或多个目录 4、{spring:[a-z]+} 将正则表达式 [a-z]+ 匹配到的值,赋值给名为 spring 的路径变量 PS:必须是完全匹配才行,在 SpringMVC 中只有完全匹配才会进入 controller 层的方法 二、注意事项: 1、匹配文件路径,需要匹配某目录下及其各级子目录下所有的文件,使用 /*/* 而非 *.*,因为有的文件不一定含有文件后缀 2、匹配文件路径,使用 AntPathMatcher 创建一个对象时,需要注意 AntPathMatcher 也有有参构造,传递路径分隔符参数 pathSeparator,对于文件路径的匹配来说,可以根据不同的操作系统来传递各自的文件分隔符,以此防止匹配文件路径错误 3、最长匹配规则(has more characters),即越精确的模式越会被优先匹配到。例如,URL请求 /app/dir/file.jsp,现在存在两个路径匹配模式 /*/*.jsp 和 /app/dir/*.jsp,那么会根据模式 /app/dir/*.jsp 来匹配 三、实例 可以参考若依框架:com.ruoyi.gateway.filter.AuthFilter 和 com.ruoyi.gateway.filter.XssFilter

March 27, 2022 · 1 min · 45 words · Bridge Li

身份证校验方法

我国的身份证编制是有标准的,每一位都不是随便瞎写的,就像我国的地图坐标经纬度一样,并不是真是的经纬度,而是人为加入了偏转,被称为:火星坐标系,但是工作中发现很多人并不了解,在工作中,用户输入的身份证号是否正确,我们根据这个规则是可以做初步校验的,当然真是的校验肯定是要通过公安部授权的接口,这是收费的。但是初步校验是真简单的,我个人发现有些系统并没有加入,所以今天写一篇小文章,做一个常用的工具类来校验身份证号,至于具体的规则,大家可以搜一下这个国标:GB11643-1999,代码如下: package cn.bridgeli.demo; import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Test; /** * @author BridgeLi * @date 2022/1/23 15:01 */ public class IdNoUtil { @Test public void testId() { String IdNo = ""; boolean b = validateIdNo(IdNo); Assert.assertTrue(b); } public static boolean validateIdNo(String IdNo) { if (StringUtils.isBlank(IdNo) || IdNo.length() != 18) { return false; } char[] charArray = IdNo.toCharArray(); //前十七位加权因子 int[] idCardWi = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; //这是除以11后,可能产生的11位余数对应的验证码 String[] idCardY = {"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}; int sum = 0; for (int i = 0; i < 17; i++) { int current = Integer.parseInt(String.valueOf(charArray[i])); int count = current * idCardWi[i]; sum += count; } char idCardLast = charArray[17]; int idCardMod = sum % 11; if (idCardY[idCardMod].equalsIgnoreCase(String.valueOf(idCardLast))) { return true; } else { return false; } } }

January 23, 2022 · 1 min · 144 words · Bridge Li

JWT 实际应用例子

JWT 是什么,很多网站都有例子,但是如何使用,却不是很多,今天就介绍一个很具体的、能在项目中实际应用的例子。 pom <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> Java 代码 package cn.bridgeli.demo; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class JwtUtil { static String key = "key"; /** * 用户登录成功后生成Jwt * 使用Hs256算法 私匙使用用户密码 * * @param ttlMillis jwt过期时间 * @param user 登录成功的user对象 * @return */ public static String createJWT(long ttlMillis, User user) { //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //生成JWT的时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) Map<String, Object> claims = new HashMap<>(); // claims.put("id", user.getId()); claims.put("username", user.getUsername()); // claims.put("password", user.getPassword()); //生成签发人 String subject = user.getUsername(); //下面就是在为payload添加各种标准声明和私有声明了 //这里其实就是new一个JwtBuilder,设置jwt的body JwtBuilder builder = Jwts.builder() //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。 .setId(UUID.randomUUID().toString()) //iat: jwt的签发时间 .setIssuedAt(now) //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。 .setSubject(subject) //设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, key); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); //设置过期时间 builder.setExpiration(exp); } return builder.compact(); } /** * Token的解密 * * @param token 加密后的token * @param user 用户的对象 * @return */ public static Claims parseJWT(String token, User user) { //签名秘钥,和生成的签名的秘钥一模一样 //得到DefaultJwtParser Claims claims = Jwts.parser() //设置签名的秘钥 .setSigningKey(key) //设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } /** * 校验token * 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过 * * @param token * @param user * @return */ public static Boolean isVerify(String token, User user) { //签名秘钥,和生成的签名的秘钥一模一样 //得到DefaultJwtParser Claims claims = Jwts.parser() //设置签名的秘钥 .setSigningKey(key) //设置需要解析的jwt .parseClaimsJws(token).getBody(); if (claims.get("username").equals(user.getUsername())) { return true; } return false; } }

December 26, 2021 · 2 min · 223 words · Bridge Li

关于 CPU 的缓存的证明和应用

证明: 首先,我们都知道现在的 CPU 多核技术,同时会有三级缓存(L1,L2,L3 ),如图: 缓存基本上来说就是把后面的数据加载到离自己近的地方,对于 CPU 来说,是一个字节一个字节的加载数据的吗?其实不是的,一般来说都是要一块一块的加载的,对于这样的一块一块的数据单位,我们叫做“Cache Line”,中文翻译:缓存行,一般来说,一个主流的 CPU 的 Cache Line 是 64 Bytes,也就是 8 个 64 位的整型,这就是 CPU 从内存中捞数据上来的最小数据单位。那么这个如何证明呢? package cn.bridgeli.demo; import java.util.concurrent.CountDownLatch; /** * @author BridgeLi * @date 2021/11/29 20:41 */ public class CacheLineTest { private static long loop = 1_0000_0000L; private static class T { // private volatile long x1, x2, x3, x4, x5, x6, x7; private volatile long x = 0L; // private volatile long x8, x9, x10, x11, x12, x13, x14; } private static T[] arr = new T[2]; static { arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); Thread t1 = new Thread(() -> { for (long i = 0; i < loop; i++) { arr[0].x = i; } countDownLatch.countDown(); }, "t1"); Thread t2 = new Thread(() -> { for (long i = 0; i < loop; i++) { arr[1].x = i; } countDownLatch.countDown(); }, "t2"); long currentTimeMillis = System.currentTimeMillis(); t1.start(); t2.start(); countDownLatch.await(); System.out.println(System.currentTimeMillis() &#8211; currentTimeMillis); } } 我们定义了一个长度为 2 的数组,数组中的元素是 T 类型,T 有一个属性 x,我们同时启动两个线程分别给第一个元素和第二个元素中的 x 复制从 0 到一亿减 1,这个时候我们测试他耗时多少,不同的电脑配置肯定是不同的,我的电脑大概是四千多毫秒,然后我们把 T 对象中属性 x 前后各被注释调的一行打开再跑一次看看,变成了大概 700 毫秒,相差整整 6 倍!这是为何? ...

November 29, 2021 · 2 min · 279 words · Bridge Li