<?php
/**
* 微信消息
* @author Devil
* @version v_0.0.1
*/
class WeiXinIMLibrary
{
private $appid;
private $appsecret;
private $token;
private $encoding_aes_key;
private $weixin_name;
private $file_dir;
/**
* [__construct 构造方法]
* @param [string] $appid [应用ID]
* @param [string] $appsecret [应用密钥]
* @param [string] $token [token]
* @param [string] $encoding_aes_key [加密串]
* @param [string] $weixin_name [服务号的微信号]
*/
private function __construct($appid, $appsecret, $token, $encoding_aes_key, $weixin_name)
{
$this->appid = $appid;
$this->appsecret = $appsecret;
$this->token = $token;
$this->encoding_aes_key = $encoding_aes_key;
$this->weixin_name = $weixin_name;
$this->file_dir = C('weixin_token_dir');
}
/**
* [Instantiate 静态方法]
* @param [string] $appid [应用ID]
* @param [string] $appsecret [应用密钥]
* @param [string] $token [token]
* @param [string] $encoding_aes_key [加密串]
* @param [string] $weixin_name [服务号的微信号]
* @return [object] [实例化对象]
*/
public static function Instantiate($appid, $appsecret, $token, $encoding_aes_key, $weixin_name)
{
$object = null;
if(!is_object($object)) $object = new self($appid, $appsecret, $token, $encoding_aes_key, $weixin_name);
return $object;
}
/**
* [Valid 接口配置校验]
*/
public function Valid()
{
$ReturnStr = empty($_GET["echostr"]) ? '' : $_GET["echostr"];
$SignaTure = empty($_GET["signature"]) ? '' : $_GET["signature"];
$TimeStamp = empty($_GET["timestamp"]) ? '' : $_GET["timestamp"];
$Nonce = empty($_GET["nonce"]) ? '' : $_GET["nonce"];
$TmpArr = array($this->token, $TimeStamp, $Nonce);
sort($TmpArr);
$TmpStr = sha1(implode($TmpArr));
if($TmpStr == $SignaTure) echo $ReturnStr;
}
/**
* [EncryptMsg 将公众平台回复用户的消息加密打包]
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
*
* @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串
* @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp
* @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce
* @return$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
* 当return返回0时有效
*
* @return int 成功0,失败返回对应的错误码
*/
public function EncryptMsg($replyMsg, $timeStamp = '', $nonce = '')
{
//加密
$encrypt = $this->Encrypt($replyMsg);
if(empty($timeStamp)) $timeStamp = time();
if(empty($nonce)) $timeStamp = md5(time().rand(0,100));
//生成安全签名
$signature = $this->GetSHA1($timeStamp, $nonce, $encrypt);
//生成发送的xml
return $this->Generate($encrypt, $signature, $timeStamp, $nonce);
}
/**
* [DecryptMsg 检验消息的真实性,并且获取解密后的明文]
* <ol>
* <li>利用收到的密文生成安全签名,进行签名验证</li>
* <li>若验证通过,则提取xml中的加密消息</li>
* <li>对消息进行解密</li>
* </ol>
*
* @param $msgSignature string 签名串,对应URL参数的msg_signature
* @param $timestamp string 时间戳 对应URL参数的timestamp
* @param $nonce string 随机串,对应URL参数的nonce
* @param $postData string 密文,对应POST请求的数据
* @param &$msg string 解密后的原文,当return返回0时有效
*
* @return int 成功0,失败返回对应的错误码
*/
public function DecryptMsg($msgSignature, $timestamp = '', $nonce, $postData)
{
if(strlen($this->encoding_aes_key) != 43) exit('encodingAesKey非法');
//提取密文
$array = $this->Extract($postData);
$encrypt = $array[0];
$touser_name = $array[1];
if(empty($timestamp)) $timestamp = time();
//验证安全签名
$signature = $this->GetSHA1($timestamp, $nonce, $encrypt);
if($signature != $msgSignature) exit('签名验证错误');
return $this->Decrypt($encrypt);
}
/**
* [Extract 提取出xml数据包中的加密消息]
* @param string $xmltext 待提取的xml字符串
* @return string 提取出的加密消息字符串
*/
public function Extract($xmltext)
{
try {
$xml = new DOMDocument();
$xml->loadXML($xmltext);
$array_e = $xml->getElementsByTagName('Encrypt');
$array_a = $xml->getElementsByTagName('ToUserName');
$encrypt = $array_e->item(0)->nodeValue;
$tousername = $array_a->item(0)->nodeValue;
return array($encrypt, $tousername);
} catch (Exception $e) {
exit('xml解析失败:'.$e->getMessage());
}
}
/**
* [Generate 生成xml消息]
* @param string $encrypt 加密后的消息密文
* @param string $signature 安全签名
* @param string $timestamp 时间戳
* @param string $nonce 随机字符串
*/
public function Generate($encrypt, $signature, $timestamp, $nonce)
{
$format = "<xml>
<Encrypt><![CDATA[%s]]></Encrypt>
<MsgSignature><![CDATA[%s]]></MsgSignature>
<TimeStamp>%s</TimeStamp>
<Nonce><![CDATA[%s]]></Nonce>
</xml>";
return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
}
/**
* [GetSHA1 用SHA1算法生成安全签名]
* @param [string] $timestamp 时间戳
* @param [string] $nonce 随机字符串
* @param [string] $encrypt 密文消息
*/
private function GetSHA1($timestamp, $nonce, $encrypt_msg)
{
try {
$all = array($encrypt_msg, $this->token, $timestamp, $nonce);
sort($all, SORT_STRING);
return sha1(implode($all));
} catch (Exception $e) {
exit('签名验证错误:'.$e->getMessage());
}
}
/**
* [Encrypt 对明文进行加密]
* @param [string] $text 需要加密的明文
* @return [string] 加密后的密文
*/
public function Encrypt($text)
{
$encoding_aes_key = base64_decode($this->encoding_aes_key . "=");
try {
//获得16位随机字符串,填充到明文之前
$random = $this->GetRandomStr();
$text = $random . pack("N", strlen($text)) . $text . $this->appid;
// 网络字节序
$size = @mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = substr($encoding_aes_key, 0, 16);
//使用自定义的填充方式对明文进行补位填充
$text = $this->Encode($text);
mcrypt_generic_init($module, $encoding_aes_key, $iv);
//加密
$encrypted = mcrypt_generic($module, $text);
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
//print(base64_encode($encrypted));
//使用BASE64对加密后的字符串进行编码
return base64_encode($encrypted);
} catch (Exception $e) {
exit('base64加密失败:'.$e->getMessage());
}
}
/**
* [Decrypt 对密文进行解密]
* @param string $encrypted 需要解密的密文
* @return string 解密得到的明文
*/
public function Decrypt($encrypted)
{
$encoding_aes_key = base64_decode($this->encoding_aes_key . "=");
try {
//使用BASE64对需要解密的字符串进行解码
$ciphertext_dec = base64_decode($encrypted);
$module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = substr($encoding_aes_key, 0, 16);
mcrypt_generic_init($module, $encoding_aes_key, $iv);
//解密
$decrypted = mdecrypt_generic($module, $ciphertext_dec);
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
} catch (Exception $e) {
exit('AES解密失败'.$e->getMessage());
}
try {
//去除补位字符
$result = $this->Decode($decrypted);
//去除16位随机字符串,网络字节序和AppId
if (strlen($result) < 16) return "";
$content = substr($result, 16, strlen($result));
$len_list = unpack("N", substr($content, 0, 4));
$xml_len = $len_list[1];
$xml_content = substr($content, 4, $xml_len);
$from_appid = substr($content, $xml_len + 4);
} catch (Exception $e) {
exit('encodingAesKey非法:'.$e->getMessage());
}
if($from_appid != $this->appid) exit('appid校验错误:'.$e->getMessage());
return $xml_content;
}
/**
* [Encode 对需要加密的明文进行填充补位]
* @param $text 需要进行填充补位操作的明文
* @return 补齐明文字符串
*/
public function Encode($text)
{
$block_size = 32;
$text_length = strlen($text);
//计算需要填充的位数
$amount_to_pad = $block_size - ($text_length % $block_size);
if ($amount_to_pad == 0)
{
$amount_to_pad = $block_size;
}
//获得补位所用的字符
$pad_chr = chr($amount_to_pad);
$tmp = "";
for ($index = 0; $index < $amount_to_pad; $index++) {
$tmp .= $pad_chr;
}
return $text . $tmp;
}
/**
* [Decode 对解密后的明文进行补位删除]
* @param decrypted 解密后的明文
* @return 删除填充补位后的明文
*/
public function Decode($text)
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > 32) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
/**
* [GetRandomStr 随机生成16位字符串]
* @return [string] 生成的字符串
*/
public function GetRandomStr()
{
$str = "";
$str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($str_pol) - 1;
for ($i = 0; $i < 16; $i++) {
$str .= $str_pol[mt_rand(0, $max)];
}
return $str;
}
/**
* [GetTextXml 文本提示内容]
* @param [string] $to_user_name [接收的用户openid]
* @param [string] $this->weixin_name [开发者微信号]
* @param [string] $String [文本内容]
* @return [string] [xml数据]
*/
public function GetTextXml($to_user_name, $String)
{
if(empty($to_user_name) || empty($this->weixin_name) || empty($String)) return '参数有误';
return '<xml>
<ToUserName><![CDATA['.$to_user_name.']]></ToUserName>
<FromUserName><![CDATA['.$this->weixin_name.']]></FromUserName>
<CreateTime>'.time().'</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA['.$String.']]></Content>
</xml>';
}
/**
* [GetImageXml 图片提示内容]
* @param [string] $to_user_name [接收的用户openid]
* @param [string] $ImageId [图片id]
* @return [string] [xml数据]
*/
public function GetImageXml($to_user_name, $ImageId)
{
if(empty($to_user_name) || empty($this->weixin_name) || empty($ImageId)) return '参数有误';
return '<xml>
<ToUserName><![CDATA['.$to_user_name.']]></ToUserName>
<FromUserName><![CDATA['.$this->weixin_name.']]></FromUserName>
<CreateTime>'.time().'</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA['.$ImageId.']]></MediaId>
</Image>
</xml>';
}
/**
* [GetImageTextXml 图文消息发送]
* @param [string] $to_user_name [接收的用户openid]
* @param [array] $data [发送的数据]
* array(array('title'=>'标题', 'desc'=>'描述', 'img'=>'图片地址http开头', 'url'=>'指向地址'), ......);
* @return [string] [xml数据]
*/
public function GetImageTextXml($to_user_name, $data)
{
if(empty($to_user_name) || empty($this->weixin_name) || empty($data) || !is_array($data)) return '参数有误';
$count = count($data);
if($count > 10) return '不能超过10条数据';
$xml = '<xml>
<ToUserName><![CDATA['.$to_user_name.']]></ToUserName>
<FromUserName><![CDATA['.$this->weixin_name.']]></FromUserName>
<CreateTime>'.time().'</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>'.$count.'</ArticleCount>
<Articles>';
for($i=0; $i<$count; $i++)
{
$title = isset($data[$i]['title']) ? trim($data[$i]['title']) : '';
$desc = isset($data[$i]['desc']) ? trim($data[$i]['desc']) : '';
$img = isset($data[$i]['img']) ? trim($data[$i]['img']) : '';
$url = isset($data[$i]['url']) ? trim($data[$i]['url']) : '';
$xml .= '<item>
<Title><![CDATA['.$title.']]></Title>
<Description><![CDATA['.$desc.']]></Description>
<PicUrl><![CDATA['.$img.']]></PicUrl>
<Url><![CDATA['.$url.']]></Url>
</item>';
}
return $xml.'</Articles></xml>';
}
/**
* [GetCustomer 客服回复]
* @param [type] $to_user_name [接收的用户openid]
*/
public function GetCustomer($to_user_name)
{
return '<xml>
<ToUserName><![CDATA['.$to_user_name.']]></ToUserName>
<FromUserName><![CDATA['.$this->weixin_name.']]></FromUserName>
<CreateTime>'.time().'</CreateTime>
<MsgType><![CDATA[transfer_customer_service]]></MsgType>
</xml>';
}
/**
* [SendTemplates 发送模版消息]
* @param [type] $touser [接收者用户openid]
* @param [type] $template_id [模版id]
* @param [type] $data [参数数据]
* @param [type] $url [指向url]
* @param [type] $topcolor [头顶边线颜色]
*/
public function SendTemplates($touser, $template_id, $data, $url = '', $topcolor = '')
{
if(empty($touser) || empty($template_id) || empty($data) || !is_array($data)) return '参数有误';
$access_token = $this->getAccessToken();
$post = array(
'touser' => $touser,
'template_id' => $template_id,
'url' => $url,
'topcolor' => $topcolor,
'data' => $data
);
$result = json_decode($this->Curl_Post('https://api.weixin.qq.com/cgi-bin/message/template/send?access_token='.$access_token, json_encode($post)), true);
return (isset($result['errcode']) && $result['errcode'] == 0);
}
/**
* [Curl_Post curl模拟post]
* @param [string] $url [请求地址]
* @param [array] $post [发送的post数据]
* @return [array] [返回的数据]
*/
private function Curl_Post($url, $post = '') {
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post,
);
$ch = curl_init($url);
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
public function getSignPackage() {
$jsapiTicket = $this->getJsApiTicket();
// 注意 URL 一定要动态获取,不能 hardcode.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$timestamp = time();
$nonceStr = $this->createNonceStr();
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
$signature = sha1($string);
$signPackage = array(
"appId" => $this->appid,
"nonceStr" => $nonceStr,
"timestamp" => $timestamp,
"url" => $url,
"signature" => $signature,
"rawString" => $string
);
return $signPackage;
}
private function createNonceStr($length = 16) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
private function getJsApiTicket() {
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
if(file_exists($this->file_dir."jsapi_ticket.json")) $data = json_decode(file_get_contents($this->file_dir."jsapi_ticket.json"));
if (empty($data) || $data->expire_time < time()) {
$accessToken = $this->getAccessToken();
// 如果是企业号用以下 URL 获取 ticket
// $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken";
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
$res = json_decode(file_get_contents($url));
$ticket = $res->ticket;
if ($ticket) {
$data->expire_time = time() + 7000;
$data->jsapi_ticket = $ticket;
file_put_contents($this->file_dir."jsapi_ticket.json", json_encode($data));
}
} else {
$ticket = $data->jsapi_ticket;
}
return $ticket;
}
public function getAccessToken() {
// access_token 应该全局存储与更新,以下代码以写入到文件中做示例
if(file_exists($this->file_dir."access_token.json")) $data = json_decode(file_get_contents($this->file_dir."access_token.json"));
if (empty($data) || $data->expire_time < time()) {
// 如果是企业号用以下URL获取access_token
// $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appid&corpsecret=$this->appsecret";
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appid&secret=$this->appsecret";
$res = json_decode(file_get_contents($url));
$access_token = $res->access_token;
if(!empty($access_token))
{
$data->expire_time = time() + 7000;
$data->access_token = $access_token;
file_put_contents($this->file_dir."access_token.json", json_encode($data));
}
} else {
$access_token = $data->access_token;
}
return $access_token;
}
}
?>