内容安全
昨天搞这个搞的焦头烂额的,好在最后解决了。所以就记录一下,也是方便已经自己如果再做同样的事情,有地方可以copy
图片安全检测
private static InputStream getUrlFile(String imgUrl) {
InputStream inputStream = null;
HttpURLConnection httpURLConnection = null;
try {
URL url = new URL(imgUrl);
httpURLConnection = (HttpURLConnection) url.openConnection();
// 设置网络连接超时时间
httpURLConnection.setConnectTimeout(3000);
// 设置应用程序要从网络连接读取数据
httpURLConnection.setDoInput(true);
httpURLConnection.setRequestMethod("GET");
int responseCode = httpURLConnection.getResponseCode();
if (responseCode == 200) {
// 从服务器返回一个输入流
inputStream = httpURLConnection.getInputStream();
}
} catch (Exception e) {
e.getMessage();
}
return inputStream;
}
private static String checkImgByInputStream(InputStream inputStream) throws HttpException {
if (inputStream == null) {
return null;
}
//获取access_token
String url = String.format("https://api.weixin.qq.com/wxa/img_sec_check?access_token=" + getAccessToken());
System.out.println(url);
HttpClient httpclient = HttpClients.createDefault();
HttpPost request = new HttpPost(url);
request.addHeader("Content-Type", "application/octet-stream");
try {
byte[] byt = new byte[inputStream.available()];
inputStream.read(byt);
request.setEntity(new ByteArrayEntity(byt, ContentType.create("image/jpg")));
HttpResponse response = httpclient.execute(request);
HttpEntity entity = response.getEntity();
String resultEntity = EntityUtils.toString(entity, "UTF-8");// 转成string
System.out.println("图片校验结果: " + resultEntity);
return resultEntity;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
文字安全检测
private static String checkMsg(String content) throws HttpException {
try {
HttpClient httpclient = HttpClients.createDefault();
String url = String.format("https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + getAccessToken());
Map<String, Object> map = new HashMap<>();
map.put("content", content);
Gson gson = new Gson();
String json = gson.toJson(map);
HttpPost request = new HttpPost(url);
request.addHeader("Content-Type", "application/json; charset=utf-8");
request.setEntity(new StringEntity(json, Charset.forName("UTF-8")));
HttpResponse response = httpclient.execute(request);
HttpEntity entity = response.getEntity();
String resultEntity = EntityUtils.toString(entity, "UTF-8");// 转成string
System.out.println("文字校验结果: " + resultEntity);
if (StringUtils.isNotBlank(resultEntity)) {
return resultEntity;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
测试
最后写一个main
方法用来测试
public static void main(String[] args) throws Exception {
//唯一需要注意的就是,图片路径不能是HTTPS
String imgFilePath = "https://image.jiangongjia.com/images/zhaohuo/project/tmp_0d75ae36fd0fc0ce5fb19866d4f36683c2b20f45af778f84.jpg";
imgFilePath = imgFilePath.replace("https", "http");
System.out.println(imgFilePath);
InputStream inputStream = getUrlFile(imgFilePath);
String result = checkImgByInputStream(inputStream);
System.out.println(result);
//官方给的违规数据
System.out.println(checkMsg("特3456书yuuo莞6543李zxcz蒜7782法fgnv级"));
System.out.println(checkMsg("完2347全dfji试3726测asad感3847知qwez到"));
}
解析获取用户手机号
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-core</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
/**
* 解析获取手机号,
*
* @param sessionKey 小程序端加密iv和encData时使用的sessionKey
* @param iv 加密算法的初始向量
* @param encData 包括敏感数据在内的完整用户信息的加密数据
* @return
*/
@PostMapping("/getUserPhone")
@ApiOperation("解析获取手机号")
public R getUserPhone(@RequestBody WeChatLoginDTO weChatLoginDTO) {
try {
String userInfo = getUserInfo(weChatLoginDTO.getEncData(), weChatLoginDTO.getSessionKey(), weChatLoginDTO.getIv());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String getUserInfo(String encryptedData, String sessionkey, String iv) {
String result = "";
// 被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decode(sessionkey);
// 偏移量
byte[] ivByte = Base64.decode(iv);
try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));
// 初始化
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
result = new String(resultByte, "UTF-8");
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidParameterSpecException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
return result;
}
微信支付
微信支付,都会牵扯到一个统一下单API,所以微信的各种支付方式都大同小异
统一下单API:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
H5支付
H5支付我在前面的文章里有写,所以这里直接放上一个链接:字节跳动小程序技术摘要
APP支付
APP支付首先也是要请求统一下单API接口,此接口代码在 H5支付
所给链接 字节跳动小程序技术摘要
中有写,所以这里也不多说。
和H5支付相比,APP支付也就多了一个再签名的过程:
// 先请求统一下单API接口,具体代码看另一篇博客
String result = HttpRequest.doPostJson("https://api.mch.weixin.qq.com/pay/unifiedorder", data.toString());
//解析XML数据
XmlMapper xmlMapper = new XmlMapper();
JSONObject jsonObject = xmlMapper.readValue(result, JSONObject.class);
//获取统一下单接口返回的预支付会话标识
String prepayId = jsonObject.getString("prepay_id");
nonceStr = jsonObject.getString("nonce_str");
long timestamp = System.currentTimeMillis() / 1000;
//准备参数集再签名
Map<String, Object> map = new HashMap<String, Object>();
map.put("prepayid", prepayId);
map.put("appid", WeChatConfig.APP_ID);
map.put("timestamp", timestamp);
map.put("noncestr", nonceStr);
map.put("package", "Sign=WXPay");
//网上很多文章说这个是商户ID,不是商户号。而且他们商户ID和商户号不一样,可能是因为微信后来改版的原因,现在版本他们文章所说的商户ID已经找不到了
map.put("partnerid", WeChatConfig.MCH_ID);
//微信支付的密钥,不是小程序的APP_SECRET 这个签名类在字节跳动小程序那篇博客中有
sign = MD5.getSign(WeChatConfig.SECRET, map);
map.put("sign", sign);
//最后把map返回即可
支付回调
解析流获取参数
/**
* 支付回调
*
* @param request
* @param response
* @return
*/
@RequestMapping("/url请求路径")
@ResponseBody
public String payResult(HttpServletRequest request, HttpServletResponse response) {
response.setHeader("Content-type", "text/html;charset=UTF-8");
try {
String reqParams = StreamUtil.read(request.getInputStream());
System.out.println("payResult支付回调结果:" + reqParams);
} catch (Exception e) {
e.printStackTrace();
}
/*
按照微信官方文档标注的呢这里是需要按要求返回的
就比如返回下面这么一段xml数据
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
但是我不这么返回,好像也并没有什么影响,照样可以支付,可以退款,并不影响订单状态
*/
return "SUCCESS";
}
- StreamUtil.java
public class StreamUtil {
public static String read(InputStream is){
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
byte[] buffer = new byte[512];
while((len = is.read(buffer)) != -1){
baos.write(buffer, 0, len);
}
return new String(baos.toByteArray(), 0, baos.size(), "utf-8");
}catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
查询订单
URL地址:https://api.mch.weixin.qq.com/pay/orderquery
必要参数
同样,参数含义就不一一介绍,官方链接:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_2&index=2
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>ec2316275641faa3aacf3cc599e8730f</nonce_str>
<transaction_id>1008450740201411110005820873</transaction_id>
<sign>FDD167FAA73459FD921B144BAF4F4CA2</sign>
</xml>
签名
签名的话就用上述统一下单的那种签名方式即可
//封装数据 生成签名
Map<String, Object> map = new HashMap<>();
map.put("appid", WeChatConfig.APP_ID);
map.put("mch_id", WeChatConfig.MCH_ID);
map.put("nonce_str", nonce_str);
map.put("out_trade_no", out_trade_no);
String sign = MD5.getSign(WeChatConfig.SECRET, map);
请求
/**
* 订单交易状态查询
*
* @param request
* @param response
* @param out_trade_no 商户订单号
* @return
*/
@RequestMapping(value = "/orderQuery")
public JsonResult orderQuery(HttpServletRequest request, HttpServletResponse response, String out_trade_no) {
try {
String nonce_str = RandomStringGenerator.getRandomStringByLength(32);
//封装数据 生成签名
Map<String, Object> map = new HashMap<>();
map.put("appid", WeChatConfig.APP_ID);
map.put("mch_id", WeChatConfig.MCH_ID);
map.put("nonce_str", nonce_str);
map.put("out_trade_no", out_trade_no);
String sign = MD5.getSign(WeChatConfig.SECRET, map);
String url = "https://api.mch.weixin.qq.com/pay/orderquery";
//封装xml请求参数
StringBuffer data = new StringBuffer();
data.append("<xml>");
data.append(" <appid>" + "<![CDATA[" + WeChatConfig.APP_ID + "]]>" + "</appid>");
data.append(" <mch_id>" + WeChatConfig.MCH_ID + "</mch_id>");
data.append(" <nonce_str>" + "<![CDATA[" + nonce_str + "]]>" + "</nonce_str>");
data.append(" <out_trade_no>" + "<![CDATA[" + out_trade_no + "]]>" + "</out_trade_no>");
data.append(" <sign>" + "<![CDATA[" + sign + "]]>" + "</sign>");
data.append("</xml>");
String result = HttpRequest.doPostJson(url, data.toString());
//解析判断返回结果 再返回即可
}
退款
接口链接:https://api.mch.weixin.qq.com/secapi/pay/refund
证书
退款和之前的支付以及查询不一样,微信退款需要证书
证书怎么弄这里也不介绍了,官方地址:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=4_3
必要参数
签名获取方式同上即可
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>6cefdb308e1e2e8aabd48cf79e546a02</nonce_str>
<out_refund_no>1415701182</out_refund_no>
<out_trade_no>1415757673</out_trade_no>
<refund_fee>1</refund_fee>
<total_fee>1</total_fee>
<transaction_id>4006252001201705123297353072</transaction_id>
<sign>FE56DD4AA85C0EECA82C35595A69E153</sign>
</xml>
示例
//微信退款流程
String nonce_str = RandomStringGenerator.getRandomStringByLength(32);
Map<String, String> data = new HashMap<String, String>();
// data.put("appid", Configure.getAppID());
data.put("appid", "xxxxxxxxxxxxx");
data.put("mch_id", Configure.getMch_id());
data.put("nonce_str", nonce_str);
data.put("sign_type", "MD5");
data.put("out_trade_no", paymentRecord.getOutTradeNo());//商户订单号
data.put("out_refund_no", RandomStringGenerator.getRandomStringByLength(32));//商户订单号
String money = String.valueOf(paymentRecord.getTotalFee().multiply(new BigDecimal(100)).intValue());
data.put("total_fee", money);//订单金额,这边需要转成字符串类型,否则后面的签名会失败
data.put("refund_fee", money);
data.put("notify_url", Configure.getNotify_url_refund());//退改成功后的回调地址
String prestr = PayUtil.createLinkString(data); // 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
//MD5运算生成签名
String mysign = PayUtil.sign(prestr, Configure.getKey(), "utf-8").toUpperCase();
data.put("sign", mysign);
//这里的退款因为是需要用到证书,所以需要自己写其他逻辑
String result = CertHttpUtil.postData(Configure.getRefundPath(), PayUtil.GetMapToXML(data));
System.out.println("result:-----------------------------------" + result);
//这里还需要对退款状态做个判断
//使用上面说的技术,把xml格式数据转成JSONObject对象来使用
XmlMapper xmlMapper = new XmlMapper();
JSONObject jsonObject = null;
try {
jsonObject = xmlMapper.readValue(result, JSONObject.class);
System.out.println("jsonObject = " + jsonObject);
if ("SUCCESS".equals(jsonObject.get("return_code")) && "SUCCESS".equals(jsonObject.get("result_code"))) {
System.out.println("退款成功");
}
} catch (Exception e) {
e.printStackTrace();
}
CertHttpUtil
public class CertHttpUtil {
private static int socketTimeout = 10000;// 连接超时时间,默认10秒
private static int connectTimeout = 30000;// 传输超时时间,默认30秒
private static RequestConfig requestConfig;// 请求器的配置
private static CloseableHttpClient httpClient;// HTTP请求器
/**
* 通过Https往API post xml数据
* @param url API地址
* @param xmlObj 要提交的XML数据对象
* @return
*/
public static String postData(String url, String xmlObj) {
// 加载证书
try {
initCert();
} catch (Exception e) {
e.printStackTrace();
}
String result = null;
HttpPost httpPost = new HttpPost(url);
// 得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
StringEntity postEntity = new StringEntity(xmlObj, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(postEntity);
// 根据默认超时限制初始化requestConfig
requestConfig = RequestConfig.custom()
.setSocketTimeout(socketTimeout)
.setConnectTimeout(connectTimeout)
.build();
// 设置请求器的配置
httpPost.setConfig(requestConfig);
try {
HttpResponse response = null;
try {
response = httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
HttpEntity entity = response.getEntity();
try {
result = EntityUtils.toString(entity, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
} finally {
httpPost.abort();
}
return result;
}
/**
* 加载证书
*
*/
private static void initCert() throws Exception {
// 证书密码,默认为商户ID
String key = Configure.getMch_id();
// 证书的路径
String path = Configure.getCertPath();
// 指定读取证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// 读取本机存放的PKCS12证书文件
FileInputStream instream = new FileInputStream(new File(path));
try {
// 指定PKCS12的密码(商户ID)
keyStore.load(instream, key.toCharArray());
} finally {
instream.close();
}
SSLContext sslcontext = SSLContexts
.custom()
.loadKeyMaterial(keyStore, key.toCharArray())
.build();
// 指定TLS版本
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext, new String[] { "TLSv1" }, null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
// 设置httpclient的SSLSocketFactory
httpClient = HttpClients
.custom()
.setSSLSocketFactory(sslsf)
.build();
}
}
退款的话还有一个退款回调,这个回调和支付回调道理都一样 所以这里就不多写了
使用第三方API实现支付退款
源码地址:https://gitee.com/oqy/wxpay-sdk
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
准备
/**
* @author 萧一旬
* @date Create in 17:24 2021/6/26
*/
@Component
public class PayConfig implements WXPayConfig {
private String appId;
private String mchId;
private String key;
@Value("${wechat.pay.appId}")
public void setAppId(String appId) {
this.appId = appId;
}
@Value("${wechat.pay.mchId}")
public void setMchId(String mchId) {
this.mchId = mchId;
}
@Value("${wechat.pay.keyPrivate}")
public void setKey(String key) {
this.key = key;
}
@Override
public String getAppID() {
return this.appId;
}
@Override
public String getMchID() {
return this.mchId;
}
@Override
public String getKey() {
return this.key;
}
@Override
public InputStream getCertStream() {
FileInputStream fileInputStream = null;
try {
//退款证书地址 用于退款使用
fileInputStream = new FileInputStream(new File("/data/paySSL/apiclient_cert.p12"));
// fileInputStream = new FileInputStream(new File("D:\\Documents\\WeChat Files\\wxid_pn4cimlph69u22\\FileStorage\\File\\2021-06\\微信支付证书\\微信支付证书\\apiclient_cert.p12"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return fileInputStream;
}
@Override
public int getHttpConnectTimeoutMs() {
return 0;
}
@Override
public int getHttpReadTimeoutMs() {
return 0;
}
}
下单
因为这是小程序的支付,所以进行了二次签名,具体情况根据支付场景实际开发
private Map<String, String> weChatPay(MemberRecharge memberRecharge) {
try {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
WXPay wxPay = new WXPay(payConfig);
//第一次签名
Map<String, String> data = new HashMap<>();
data.put("appid", weChatPayConfig.getAppId());
data.put("mch_id", weChatPayConfig.getMchId()); //商户号
data.put("trade_type", "JSAPI"); //支付场景 APP 微信app支付 JSAPI 公众号支付 NATIVE 扫码支付
data.put("notify_url", weChatPayConfig.getRechargeNotifyUrl()); //回调地址
data.put("spbill_create_ip", "127.0.0.1"); //终端ip
// data.put("total_fee", String.valueOf(new BigDecimal(100).multiply(memberRecharge.getPaidMoney()).intValue())); //订单总金额
data.put("total_fee", "1"); //订单总金额
data.put("fee_type", "CNY"); //默认人民币
data.put("sign_type", "MD5"); //加密方式
data.put("receipt", "Y"); //需要开票
data.put("out_trade_no",""); //交易号
data.put("subject", ""); //商品描述
data.put("body", "订单 - " + memberRecharge.getOutTradeNo()); //商品描述
data.put("time_start", format.format(new Date()));
data.put("openid", "");
data.put("nonce_str", UUID.randomUUID().toString()); // 随机字符串小于32位 -10位
data.put("sign", WXPayUtil.generateSignature(data, weChatPayConfig.getKeyPrivate())); //生成签名
Map<String, String> respData = wxPay.unifiedOrder(data);//统一下单接口
//二次签名
data = new HashMap<>();
data.put("appId", weChatPayConfig.getAppId());
data.put("timeStamp", format.format(new Date()));
data.put("signType", "MD5");
data.put("nonceStr", UUID.randomUUID().toString());
data.put("package", "prepay_id=" + respData.get("prepay_id"));
data.put("paySign", WXPayUtil.generateSignature(data, weChatPayConfig.getKeyPrivate()));
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
退款
/**
* 微信支付退款
*
* @param order
* @return
*/
private Map<String, String> weChatRefund(Order order) {
WXPay wxPay = new WXPay(payConfig);
//第一次签名
Map<String, String> data = new HashMap<>();
data.put("appid", weChatPayConfig.getAppId());
data.put("mch_id", weChatPayConfig.getMchId()); //商户号
data.put("notify_url", weChatPayConfig.getRefundBackUrl()); //回调地址
data.put("total_fee", String.valueOf(new BigDecimal(100).multiply(order.getRealPay()).intValue())); //总支付金额
data.put("refund_fee", String.valueOf(new BigDecimal(100).multiply(order.getRealPay()).intValue())); //退款金额
data.put("out_trade_no", order.getOutTradeNo()); //交易号
data.put("out_refund_no", order.getOutRefundNo()); //退款订单号
data.put("nonce_str", UUID.randomUUID().toString()); // 随机字符串小于32位 -10位
try {
data.put("sign", WXPayUtil.generateSignature(data, weChatPayConfig.getKeyPrivate()));
System.out.println(data.toString());
return wxPay.refund(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
用第三方api就省去了自己拼装数据以及计算签名的过程