Files
ks/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/WechatUtil.java
2025-10-21 15:52:51 +08:00

312 lines
13 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, String> 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 +
"&timestamp=" + 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";
}
}