package com.ruoyi.cms.util; import cn.hutool.core.io.file.FileReader; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Base64; import java.util.Formatter; import java.util.HashMap; import java.util.Map; @Slf4j @Component public class WechatUtil { /** * 生成signature **/ private static String appid = "wx9d1cbc11c8c40ba7"; private static String secret = "38e87cf6251945446e8ac091a0ba9ab2"; public AppWechatEntity sign(String url) { Map ret = new HashMap(); String nonceStr = create_nonce_str(); String timestamp = Long.toString(create_timestamp()); String string1; String signature = ""; //获取jsapi_ticket和过期时间 AppWechatEntity appWechatEntity = getJsapiTicket(); String jsapiTicket = appWechatEntity.getJsapi_ticket(); Long expireTime = appWechatEntity.getExpire_time(); //注意这里参数名必须全部小写,且必须有序 string1 = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; try { MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(string1.getBytes(StandardCharsets.UTF_8)); signature = byteToHex(crypt.digest()); } catch (Exception e) { e.printStackTrace(); } appWechatEntity.setAppId(appid); appWechatEntity.setExpire_time(expireTime); appWechatEntity.setNonceStr(nonceStr); appWechatEntity.setTimestamp(timestamp); appWechatEntity.setSignature(signature); return appWechatEntity; } /** * 获取jsapi_ticket **/ public AppWechatEntity getJsapiTicket() { //logger.debug("--------------开始执行getJsapiTicket方法--------------"); //定义过期时间 AppWechatEntity appWechatEntity = new AppWechatEntity(); String accessTokenString = ""; String jsapiTicketString = ""; String jsapi_ticket = ""; String access_token = ""; jsapiTicketString = readWechatTokenFile(getJsapiTicketFilePath()); if (!StringUtils.isEmpty(jsapiTicketString)) { appWechatEntity = JSONObject.parseObject(jsapiTicketString, AppWechatEntity.class); long expireTime = appWechatEntity.getExpire_time(); long curTime = create_timestamp(); if (expireTime >= curTime && StrUtil.isNotEmpty(appWechatEntity.getJsapi_ticket())) { //logger.debug("已有的jsapi_ticket=" + jsapi_ticket); return appWechatEntity; } } long timestamp = create_timestamp() + 7000;//过期时间是2小时(7200s) access_token = getAccessTokenData("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid+ "&secret=" + secret); Map accessTokenMap = new HashMap(); accessTokenMap.put("expire_time", timestamp); accessTokenMap.put("access_token", access_token); accessTokenString = JSONObject.toJSONString(accessTokenMap); jsapi_ticket = getJsapiTicketData("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + access_token + "&type=jsapi"); Map jsapiTicketMap = new HashMap(); jsapiTicketMap.put("expire_time", timestamp); jsapiTicketMap.put("jsapi_ticket", jsapi_ticket); jsapiTicketString = JSONObject.toJSONString(jsapiTicketMap); // 写文件 try { FileUtils.writeStringToFile(new File(getAccessTokenFilePath()), accessTokenString, CharsetUtil.CHARSET_UTF_8); FileUtils.writeStringToFile(new File(getJsapiTicketFilePath()), jsapiTicketString, CharsetUtil.CHARSET_UTF_8); //logger.debug("写入文件成功"); } catch (IOException e) { log.debug("写文件异常:" + e.getMessage()); e.printStackTrace(); } appWechatEntity.setJsapi_ticket(jsapi_ticket); appWechatEntity.setExpire_time(timestamp); appWechatEntity.setAccess_token(access_token); //logger.debug("--------------结束执行getJsapiTicket方法--------------"); return appWechatEntity; } public String getAccessToken() { //logger.debug("--------------开始执行getAccessToken方法--------------"); String access_token = ""; String accessTokenString = ""; AppWechatEntity appWechatEntity = new AppWechatEntity(); accessTokenString = readWechatTokenFile(getAccessTokenFilePath()); if (StringUtils.isNotEmpty(accessTokenString)) { appWechatEntity = JSONObject.parseObject(accessTokenString, AppWechatEntity.class); access_token = appWechatEntity.getAccess_token(); long expireTime = appWechatEntity.getExpire_time(); long curTime = create_timestamp(); if (expireTime >= curTime) { //logger.debug("已有的access_token=" + access_token); return access_token; } } long timestamp = create_timestamp() + 6000;//过期时间是2小时,但是可以提前进行更新,防止前端正好过期 access_token = getAccessTokenData("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret); Map accessTokenMap = new HashMap(); accessTokenMap.put("expire_time", timestamp); accessTokenMap.put("access_token", access_token); accessTokenString = JSONObject.toJSONString(accessTokenMap); // 写文件 try { FileUtils.writeStringToFile(new File(getAccessTokenFilePath()), accessTokenString, CharsetUtil.CHARSET_UTF_8); } catch (IOException e) { log.debug("写文件异常:" + e.getMessage()); e.printStackTrace(); } //logger.debug("新的access_token=" + access_token); //logger.debug("--------------结束执行getAccessToken方法--------------"); return access_token; } private String readWechatTokenFile(String filePath) { String content = ""; try { if (new File(filePath).exists()) { FileReader fileReader = new FileReader(filePath, CharsetUtil.CHARSET_UTF_8); content = fileReader.readString(); } else { new File(filePath).createNewFile(); } } catch (IOException e) { log.error("读文件异常:" + e.getMessage()); e.printStackTrace(); } return content; } private String getAccessTokenData(String url) { String str = ""; String result = HttpUtil.get(url, CharsetUtil.CHARSET_UTF_8); System.out.println("result=============="+result); if (StringUtils.isEmpty(result)) return str; str = parseData("access_token", "expires_in", result); return str; } private String getJsapiTicketData(String url) { String str = ""; String result = HttpUtil.get(url, CharsetUtil.CHARSET_UTF_8); if (StringUtils.isEmpty(result)) return str; str = parseData("ticket", "expires_in", result); return str; } private String parseData(String tokenName, String expiresInName, String data) { String tokenConent = ""; JSONObject jsonObject = JSONObject.parseObject(data); try { tokenConent = jsonObject.get(tokenName).toString(); if (StringUtils.isEmpty(tokenConent)) { log.error("token获取失败,获取结果" + data); return tokenConent; } } catch (Exception e) { log.error("token 结果解析失败,token参数名称: " + tokenName + "有效期参数名称:" + expiresInName + "token请求结果:" + data); e.printStackTrace(); return tokenConent; } return tokenConent; } /** * 获取appid和session_key * @param url * @return */ private String getAccessData(String url) { String str = ""; String result = HttpUtil.get(url, CharsetUtil.CHARSET_UTF_8); System.out.println("result=============="+result); if (StringUtils.isEmpty(result)) return str; return result; } private String byteToHex(final byte[] hash) { Formatter formatter = new Formatter(); for (byte b : hash) { formatter.format("%02x", b); } String result = formatter.toString(); formatter.close(); return result; } /** * 通过code获取微信用户的openid和session_key * * @param code 登录凭证code * @return 包含openid、session_key、unionid的JSON对象 */ public JSONObject code2Session(String code) { try { System.out.println("appid==============="+appid); System.out.println("secret================"+secret); String response = getAccessData("https://api.weixin.qq.com/sns/jscode2session?appid="+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code"); JSONObject result = JSONObject.parseObject(response); // 微信返回错误码处理 if (result.containsKey("errcode") && result.getInteger("errcode") != 0) { throw new RuntimeException("微信授权失败:" + result.getString("errmsg")); } return result; } catch (Exception e) { throw new RuntimeException("调用微信接口失败:" + e.getMessage()); } } /** * 解密微信用户手机号(用户通过 getPhoneNumber 组件授权后返回的加密数据) * @param encryptedData 微信返回的加密手机号数据 * @param sessionKey 从 code2Session 接口获取的会话密钥 * @param iv 微信返回的加密向量(与 encryptedData 配套) * @return 解密后的 JSON 对象(包含 phoneNumber、purePhoneNumber 等字段) * @throws RuntimeException 解密失败时抛出 */ public JSONObject decryptPhoneNumber(String encryptedData, String sessionKey, String iv) { try { // 1. Base64 解码(encryptedData、sessionKey、iv 均为 Base64 编码) byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData); byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey); byte[] ivBytes = Base64.getDecoder().decode(iv); // 2. 验证session_key长度(AES-128要求密钥长度为16字节) if (sessionKeyBytes.length != 16) { throw new RuntimeException("session_key长度错误,应为16字节"); } // 验证iv长度(CBC模式下iv长度必须与块大小一致,AES为16字节) if (ivBytes.length != 16) { throw new RuntimeException("iv长度错误,应为16字节"); } // 2. 初始化 AES-128-CBC 解密器(使用PKCS5Padding替换PKCS7Padding,两者在AES中效果一致) Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); // 3. 执行解密并转换为字符串 byte[] decryptedBytes = cipher.doFinal(encryptedDataBytes); String decryptedStr = new String(decryptedBytes, StandardCharsets.UTF_8); // 4. 解析为 JSON 并返回(包含手机号等信息) return JSONObject.parseObject(decryptedStr); } catch (Exception e) { throw new RuntimeException("解密用户手机号失败:" + e.getMessage(), e); } } private String create_nonce_str() { return IdUtil.simpleUUID(); } private Long create_timestamp() { return System.currentTimeMillis() / 1000; } private String getJsapiTicketFilePath() { return "/data/wechat" + "/" +appid + "_jsapiTicket.txt"; } private String getAccessTokenFilePath() { return "/data/wechat" + "/" + appid + "_accessToken.txt"; } }