内容安全

昨天搞这个搞的焦头烂额的,好在最后解决了。所以就记录一下,也是方便已经自己如果再做同样的事情,有地方可以copy

官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html

图片安全检测

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 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到"));

}

结果如下,就说明可以了:

解析获取用户手机号


    org.codehaus.xfire
    xfire-core
    1.2.6


    org.bouncycastle
    bcprov-jdk16
    1.46
/**
 * 解析获取手机号,
 *
 * @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 map = new HashMap();
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数据
	
	  
	  
	
	
	但是我不这么返回,好像也并没有什么影响,照样可以支付,可以退款,并不影响订单状态
	*/
    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


   wx2421b1c4370ec43b
   10000100
   ec2316275641faa3aacf3cc599e8730f
   1008450740201411110005820873
   FDD167FAA73459FD921B144BAF4F4CA2

签名

签名的话就用上述统一下单的那种签名方式即可

//封装数据 生成签名
Map 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 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("");
		data.append(" " + "" + "");
		data.append(" " + WeChatConfig.MCH_ID + "");
		data.append(" " + "" + "");
		data.append("  " + "" + "");
		data.append("  " + "" + "");
		data.append("");


		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

必要参数

签名获取方式同上即可


   wx2421b1c4370ec43b
   10000100
   6cefdb308e1e2e8aabd48cf79e546a02
   1415701182
   1415757673
   1
   1
   4006252001201705123297353072
   FE56DD4AA85C0EECA82C35595A69E153

示例

//微信退款流程
 String nonce_str = RandomStringGenerator.getRandomStringByLength(32);
 Map data = new HashMap();
 //	    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



    com.github.wxpay
    wxpay-sdk
    0.0.3

准备

/**
 * @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 weChatPay(MemberRecharge memberRecharge) {
    try {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");

        WXPay wxPay = new WXPay(payConfig);
        //第一次签名
        Map 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 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 weChatRefund(Order order) {
    WXPay wxPay = new WXPay(payConfig);
    //第一次签名
    Map 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就省去了自己拼装数据以及计算签名的过程