├── .gitignore ├── src └── main │ ├── resources │ └── META-INF │ │ └── spring.factories │ └── java │ └── com │ └── github │ └── developer │ └── weapons │ ├── model │ ├── official │ │ ├── QrCodeActionName.java │ │ ├── MenuMsgItem.java │ │ ├── FanData.java │ │ ├── OfficialUserQuery.java │ │ ├── OfficalAccessTokenQuery.java │ │ ├── OfficialQrCode.java │ │ ├── Fan.java │ │ ├── OfficialQrCodeCreate.java │ │ ├── MessageTypeEnum.java │ │ ├── MenuMsg.java │ │ ├── OfficialMaterial.java │ │ ├── OfficialMenu.java │ │ ├── OfficialNews.java │ │ ├── OfficialUserInfo.java │ │ ├── OfficialAutoReplyMessage.java │ │ ├── OfficialTemplateMessage.java │ │ └── OfficialCustomMessage.java │ ├── WechatEnText.java │ ├── component │ │ ├── ComponentVerifyInfo.java │ │ ├── ComponentMessageArticle.java │ │ ├── ComponentAuthInfo.java │ │ ├── ComponentMessage.java │ │ ├── ComponentAuthorizerInfo.java │ │ └── ComponentTextMessage.java │ └── Token.java │ ├── exception │ └── WechatException.java │ ├── config │ ├── WechatComponentProperties.java │ └── WechatConfiguration.java │ ├── util │ ├── aes │ │ ├── ByteGroup.java │ │ ├── PKCS7Encoder.java │ │ ├── SHA1.java │ │ ├── AesException.java │ │ ├── XMLParse.java │ │ └── WXBizMsgCrypt.java │ ├── XmlUtils.java │ └── FileUtils.java │ └── service │ ├── WechatBaseService.java │ ├── WechatComponentService.java │ └── WechatOfficialService.java ├── deploy.sh ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | .idea/ -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.github.developer.weapons.config.WechatConfiguration -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/QrCodeActionName.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | public enum QrCodeActionName { 4 | QR_STR_SCENE, QR_LIMIT_STR_SCENE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/MenuMsgItem.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class MenuMsgItem { 7 | private String id; 8 | private String content; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/FanData.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class FanData { 9 | private List openid; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialUserQuery.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OfficialUserQuery { 7 | private String accessToken; 8 | private String openId; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/WechatEnText.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class WechatEnText { 7 | private String Encrypt; 8 | private String MsgSignature; 9 | private String TimeStamp; 10 | private String Nonce; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentVerifyInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ComponentVerifyInfo { 7 | private String signature; 8 | private String timestamp; 9 | private String nonce; 10 | private String body; 11 | } -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficalAccessTokenQuery.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | @Data 7 | @Accessors(chain = true) 8 | public class OfficalAccessTokenQuery { 9 | private String appid; 10 | private String secret; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialQrCode.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class OfficialQrCode { 8 | private String ticket; 9 | @JSONField(name = "expire_seconds") 10 | private int expireSeconds; 11 | private String url; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/Fan.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class Fan { 8 | private Long total; 9 | private Long count; 10 | @JSONField(name = "next_openid") 11 | private String nextOpenId; 12 | private FanData data; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialQrCodeCreate.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | @Data 7 | @Accessors(chain = true) 8 | public class OfficialQrCodeCreate { 9 | private String accessToken; 10 | private String scene; 11 | private QrCodeActionName actionName; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentMessageArticle.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | @Data 7 | @Accessors(chain = true) 8 | public class ComponentMessageArticle { 9 | private String Title; 10 | private String Description; 11 | private String PicUrl; 12 | private String Url; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/MessageTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | public enum MessageTypeEnum { 4 | TEXT("text") 5 | ,IMAGE("image"), 6 | MSGMENU("msgmenu") 7 | ; 8 | private String type; 9 | 10 | public String getType() { 11 | return type; 12 | } 13 | 14 | MessageTypeEnum(String type) { 15 | this.type = type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/Token.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class Token { 8 | @JSONField(name = "access_token") 9 | private String accessToken; 10 | 11 | public T withToken(String token) { 12 | this.accessToken = token; 13 | return (T) this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentAuthInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ComponentAuthInfo { 7 | /** 8 | * 授权订阅号的 token 9 | */ 10 | private String token; 11 | 12 | /** 13 | * 授权订阅号的账号 id 14 | */ 15 | private String appId; 16 | 17 | /** 18 | * 刷新 TOKEN 19 | */ 20 | private String authorizerRefreshToken; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/MenuMsg.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | @Data 9 | public class MenuMsg { 10 | @JSONField(name = "head_content") 11 | private String headContent; 12 | 13 | @JSONField(name = "tail_content") 14 | private String tailContent; 15 | 16 | private List list; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialMaterial.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.github.developer.weapons.model.Token; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.io.File; 8 | 9 | @Data 10 | @Accessors(chain = true) 11 | public class OfficialMaterial extends Token { 12 | private String url; 13 | private String mediaId; 14 | private File file; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialMenu.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.github.developer.weapons.model.Token; 4 | import lombok.Data; 5 | 6 | /** 7 | * 公众号模板消息 8 | */ 9 | @Data 10 | public class OfficialMenu { 11 | /** 12 | * 类型 13 | */ 14 | private String type; 15 | /** 16 | * 名称 17 | */ 18 | private String name; 19 | 20 | /** 21 | * jian 22 | */ 23 | private String key; 24 | } 25 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | rm -rf developer-weapons-repository 2 | mkdir developer-weapons-repository 3 | cd developer-weapons-repository 4 | git init 5 | git remote add origin git@github.com:developer-weapons/repository.git 6 | git pull origin master 7 | git branch --set-upstream-to=origin/master master 8 | cd - 9 | mvn deploy -DaltDeploymentRepository=developer-weapons-repository::default::file:developer-weapons-repository 10 | cd developer-weapons-repository 11 | git add . 12 | git commit -m "deploy" 13 | git push 14 | cd - 15 | rm -rf developer-weapons-repository -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/exception/WechatException.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.exception; 2 | 3 | public class WechatException extends RuntimeException { 4 | private String message; 5 | 6 | public WechatException(String message) { 7 | this.message = message; 8 | } 9 | 10 | 11 | public WechatException(String message, Throwable e) { 12 | super(message, e); 13 | this.message = message; 14 | } 15 | 16 | @Override 17 | public String getMessage() { 18 | return message; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/config/WechatComponentProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | /** 7 | * 官方文档 8 | * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html 9 | */ 10 | @Data 11 | @ConfigurationProperties(prefix = "wechat.component") 12 | public class WechatComponentProperties { 13 | private String appId; 14 | private String token; 15 | private String aesKey; 16 | private String secret; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/ByteGroup.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.util.aes; 2 | 3 | import java.util.ArrayList; 4 | 5 | class ByteGroup { 6 | ArrayList byteContainer = new ArrayList(); 7 | 8 | public byte[] toBytes() { 9 | byte[] bytes = new byte[byteContainer.size()]; 10 | for (int i = 0; i < byteContainer.size(); i++) { 11 | bytes[i] = byteContainer.get(i); 12 | } 13 | return bytes; 14 | } 15 | 16 | public ByteGroup addBytes(byte[] bytes) { 17 | for (byte b : bytes) { 18 | byteContainer.add(b); 19 | } 20 | return this; 21 | } 22 | 23 | public int size() { 24 | return byteContainer.size(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * Created by codedrinker on 2019/4/27. 11 | */ 12 | @Data 13 | @Accessors(chain = true) 14 | public class ComponentMessage { 15 | private String ToUserName; 16 | private String FromUserName; 17 | private Long CreateTime; 18 | private String MsgType; 19 | private String Content; 20 | private String MsgId; 21 | private String MediaId; 22 | private Integer ArticleCount; 23 | private List Articles; 24 | 25 | public ComponentMessage addArticle(ComponentMessageArticle article) { 26 | if (Articles == null) { 27 | Articles = new ArrayList<>(); 28 | } 29 | Articles.add(article); 30 | return this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/config/WechatConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.config; 2 | 3 | import com.github.developer.weapons.service.WechatComponentService; 4 | import com.github.developer.weapons.service.WechatOfficialService; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | @EnableConfigurationProperties({WechatComponentProperties.class}) 12 | public class WechatConfiguration { 13 | @Bean 14 | @ConditionalOnMissingBean 15 | public WechatComponentService wechatComponentService() { 16 | return new WechatComponentService(); 17 | } 18 | 19 | @Bean 20 | @ConditionalOnMissingBean 21 | public WechatOfficialService wechatOfficialService() { 22 | return new WechatOfficialService(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialNews.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.github.developer.weapons.model.Token; 5 | import lombok.Data; 6 | import lombok.experimental.Accessors; 7 | 8 | @Data 9 | @Accessors(chain = true) 10 | public class OfficialNews extends Token { 11 | 12 | private String title; 13 | 14 | @JSONField(name = "thumb_media_id") 15 | private String thumbMediaId; 16 | 17 | private String author; 18 | 19 | private String digest; 20 | 21 | @JSONField(name = "show_cover_pic") 22 | private Integer showCoverPic = 0; 23 | 24 | private String content; 25 | @JSONField(name = "content_source_url") 26 | 27 | private String contentSourceUrl; 28 | 29 | @JSONField(name = "need_open_comment") 30 | private Integer needOpenComment = 0; 31 | 32 | @JSONField(name = "only_fans_can_comment") 33 | private Integer onlyFansCanComment = 1; 34 | 35 | @JSONField(name = "media_id") 36 | private String mediaId; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentAuthorizerInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ComponentAuthorizerInfo { 7 | /** 8 | * 原始ID 9 | */ 10 | private String userName; 11 | /** 12 | * 授权方 appid 13 | */ 14 | private String authorizerAppid; 15 | /** 16 | * 昵称 17 | */ 18 | private String nickName; 19 | /** 20 | * 头像 21 | */ 22 | private String headImg; 23 | /** 24 | * 二维码 25 | */ 26 | private String qrcodeUrl; 27 | /** 28 | * 刷新 TOKEN 29 | */ 30 | private String authorizerRefreshToken; 31 | 32 | /** 33 | * 公众号认证类型 34 | * -1 未认证 35 | * 0 微信认证 36 | * 1 新浪微博认证 37 | * 2 腾讯微博认证 38 | * 3 已资质认证通过但还未通过名称认证 39 | * 4 已资质认证通过、还未通过名称认证,但通过了新浪微博认证 40 | * 5 已资质认证通过、还未通过名称认证,但通过了腾讯微博认证 41 | */ 42 | private Integer verifyTypeInfo; 43 | 44 | /** 45 | * 公众号类型 46 | * 0 订阅号 47 | * 1 由历史老帐号升级后的订阅号 48 | * 2 服务号 49 | */ 50 | private Integer serviceTypeInfo; 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OfficialUserInfo { 7 | /** 8 | * 是否订阅当前订阅号 9 | * 10 | * @return 11 | */ 12 | private boolean isSubscribe() { 13 | return 0 != subscribe; 14 | } 15 | 16 | /** 17 | * 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。 18 | */ 19 | private Integer subscribe; 20 | /** 21 | * 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 22 | */ 23 | private Integer sex; 24 | /** 25 | * 用户的标识,对当前公众号唯一 26 | */ 27 | private String openid; 28 | /** 29 | * 用户的昵称 30 | */ 31 | private String nickname; 32 | /** 33 | * 用户的语言,简体中文为zh_CN 34 | */ 35 | private String language; 36 | /** 37 | * 用户所在国家 38 | */ 39 | private String country; 40 | /** 41 | * 用户所在城市 42 | */ 43 | private String city; 44 | /** 45 | * 用户所在省份 46 | */ 47 | private String province; 48 | /** 49 | * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 50 | */ 51 | private String headimgurl; 52 | /** 53 | * 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段 54 | */ 55 | private String unionid; 56 | /** 57 | * 公众号运营者对粉丝的备注,公众号运营者可在微信公众平台用户管理界面对粉丝添加备注 58 | */ 59 | private String remark; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/component/ComponentTextMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.component; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Created by codedrinker on 2019/4/27. 7 | */ 8 | @Data 9 | public class ComponentTextMessage { 10 | private String ToUserName; 11 | private String FromUserName; 12 | private Long CreateTime; 13 | private String MsgType; 14 | private String Content; 15 | private String MsgId; 16 | 17 | public static ComponentTextMessage build() { 18 | return new ComponentTextMessage(); 19 | } 20 | 21 | public ComponentTextMessage withToUserName(String userName) { 22 | this.ToUserName = userName; 23 | return this; 24 | } 25 | 26 | public ComponentTextMessage withFromUserName(String fromUserName) { 27 | this.FromUserName = fromUserName; 28 | return this; 29 | } 30 | 31 | public ComponentTextMessage withCreateTime(Long createTime) { 32 | this.CreateTime = createTime; 33 | return this; 34 | } 35 | 36 | public ComponentTextMessage withMsgType(String msgType) { 37 | this.MsgType = msgType; 38 | return this; 39 | } 40 | 41 | public ComponentTextMessage withContent(String content) { 42 | this.Content = content; 43 | return this; 44 | } 45 | 46 | public ComponentTextMessage withMsgId(String msgId) { 47 | this.MsgId = msgId; 48 | return this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/XmlUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.util; 2 | 3 | import com.github.developer.weapons.model.component.ComponentMessageArticle; 4 | import com.github.developer.weapons.model.component.ComponentTextMessage; 5 | import com.thoughtworks.xstream.XStream; 6 | import org.dom4j.Document; 7 | import org.dom4j.DocumentException; 8 | import org.dom4j.Element; 9 | import org.dom4j.io.SAXReader; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * Created by codedrinker on 2019/4/27. 19 | */ 20 | public class XmlUtils { 21 | public static Map xmlToMap(String xml) throws IOException, DocumentException { 22 | Map map = new HashMap<>(); 23 | SAXReader reader = new SAXReader(); 24 | Document doc = reader.read(new ByteArrayInputStream(xml.getBytes("utf-8"))); 25 | Element root = doc.getRootElement(); 26 | List list = root.elements(); 27 | for (Element e : list) { 28 | map.put(e.getName(), e.getText()); 29 | } 30 | return map; 31 | } 32 | 33 | public static String objectToXml(ComponentTextMessage message) { 34 | XStream xs = new XStream(); 35 | xs.alias("xml", message.getClass()); 36 | return xs.toXML(message); 37 | } 38 | 39 | public static String objectToXml(Object message) { 40 | XStream xs = new XStream(); 41 | xs.alias("xml", message.getClass()); 42 | xs.alias("item", ComponentMessageArticle.class); 43 | return xs.toXML(message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/PKCS7Encoder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 对公众平台发送给公众账号的消息加解密示例代码. 3 | * 4 | * @copyright Copyright (c) 1998-2014 Tencent Inc. 5 | */ 6 | 7 | // ------------------------------------------------------------------------ 8 | 9 | package com.github.developer.weapons.util.aes; 10 | 11 | import java.nio.charset.Charset; 12 | import java.util.Arrays; 13 | 14 | /** 15 | * 提供基于PKCS7算法的加解密接口. 16 | */ 17 | class PKCS7Encoder { 18 | static Charset CHARSET = Charset.forName("utf-8"); 19 | static int BLOCK_SIZE = 32; 20 | 21 | /** 22 | * 获得对明文进行补位填充的字节. 23 | * 24 | * @param count 需要进行填充补位操作的明文字节个数 25 | * @return 补齐用的字节数组 26 | */ 27 | static byte[] encode(int count) { 28 | // 计算需要填充的位数 29 | int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); 30 | if (amountToPad == 0) { 31 | amountToPad = BLOCK_SIZE; 32 | } 33 | // 获得补位所用的字符 34 | char padChr = chr(amountToPad); 35 | String tmp = new String(); 36 | for (int index = 0; index < amountToPad; index++) { 37 | tmp += padChr; 38 | } 39 | return tmp.getBytes(CHARSET); 40 | } 41 | 42 | /** 43 | * 删除解密后明文的补位字符 44 | * 45 | * @param decrypted 解密后的明文 46 | * @return 删除补位字符后的明文 47 | */ 48 | static byte[] decode(byte[] decrypted) { 49 | int pad = (int) decrypted[decrypted.length - 1]; 50 | if (pad < 1 || pad > 32) { 51 | pad = 0; 52 | } 53 | return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); 54 | } 55 | 56 | /** 57 | * 将数字转化成ASCII码对应的字符,用于对明文进行补码 58 | * 59 | * @param a 需要转化的数字 60 | * @return 转化得到的字符 61 | */ 62 | static char chr(int a) { 63 | byte target = (byte) (a & 0xFF); 64 | return (char) target; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/SHA1.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 对公众平台发送给公众账号的消息加解密示例代码. 3 | * 4 | * @copyright Copyright (c) 1998-2014 Tencent Inc. 5 | */ 6 | 7 | // ------------------------------------------------------------------------ 8 | 9 | package com.github.developer.weapons.util.aes; 10 | 11 | import java.security.MessageDigest; 12 | import java.util.Arrays; 13 | 14 | /** 15 | * SHA1 class 16 | * 17 | * 计算公众平台的消息签名接口. 18 | */ 19 | class SHA1 { 20 | 21 | /** 22 | * 用SHA1算法生成安全签名 23 | * @param token 票据 24 | * @param timestamp 时间戳 25 | * @param nonce 随机字符串 26 | * @param encrypt 密文 27 | * @return 安全签名 28 | * @throws AesException 29 | */ 30 | public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException 31 | { 32 | try { 33 | String[] array = new String[] { token, timestamp, nonce, encrypt }; 34 | StringBuffer sb = new StringBuffer(); 35 | // 字符串排序 36 | Arrays.sort(array); 37 | for (int i = 0; i < 4; i++) { 38 | sb.append(array[i]); 39 | } 40 | String str = sb.toString(); 41 | // SHA1签名生成 42 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 43 | md.update(str.getBytes()); 44 | byte[] digest = md.digest(); 45 | 46 | StringBuffer hexstr = new StringBuffer(); 47 | String shaHex = ""; 48 | for (int i = 0; i < digest.length; i++) { 49 | shaHex = Integer.toHexString(digest[i] & 0xFF); 50 | if (shaHex.length() < 2) { 51 | hexstr.append(0); 52 | } 53 | hexstr.append(shaHex); 54 | } 55 | return hexstr.toString(); 56 | } catch (Exception e) { 57 | e.printStackTrace(); 58 | throw new AesException(AesException.ComputeSignatureError); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/AesException.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.util.aes; 2 | 3 | @SuppressWarnings("serial") 4 | public class AesException extends Exception { 5 | 6 | public final static int OK = 0; 7 | public final static int ValidateSignatureError = -40001; 8 | public final static int ParseXmlError = -40002; 9 | public final static int ComputeSignatureError = -40003; 10 | public final static int IllegalAesKey = -40004; 11 | public final static int ValidateAppidError = -40005; 12 | public final static int EncryptAESError = -40006; 13 | public final static int DecryptAESError = -40007; 14 | public final static int IllegalBuffer = -40008; 15 | //public final static int EncodeBase64Error = -40009; 16 | //public final static int DecodeBase64Error = -40010; 17 | //public final static int GenReturnXmlError = -40011; 18 | 19 | private int code; 20 | 21 | private static String getMessage(int code) { 22 | switch (code) { 23 | case ValidateSignatureError: 24 | return "签名验证错误"; 25 | case ParseXmlError: 26 | return "xml解析失败"; 27 | case ComputeSignatureError: 28 | return "sha加密生成签名失败"; 29 | case IllegalAesKey: 30 | return "SymmetricKey非法"; 31 | case ValidateAppidError: 32 | return "appid校验失败"; 33 | case EncryptAESError: 34 | return "aes加密失败"; 35 | case DecryptAESError: 36 | return "aes解密失败"; 37 | case IllegalBuffer: 38 | return "解密后得到的buffer非法"; 39 | // case EncodeBase64Error: 40 | // return "base64加密错误"; 41 | // case DecodeBase64Error: 42 | // return "base64解密错误"; 43 | // case GenReturnXmlError: 44 | // return "xml生成失败"; 45 | default: 46 | return null; // cannot be 47 | } 48 | } 49 | 50 | public int getCode() { 51 | return code; 52 | } 53 | 54 | AesException(int code) { 55 | super(getMessage(code)); 56 | this.code = code; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## wechat-spring-boot-starter 2 | 为对接微信提供快速 API 能力,包括微信、微信小程序、微信第三方平台 3 | 4 | ## 支持列表 5 | 6 | ### 1.0.0 7 | #### 第三方平台 8 | - 获取验证票据和通用消息等解密接口 9 | `com.github.developer.weapons.service.WechatComponentService.getVerifiedInfo` 10 | - 根据 ticket 获取 component_token 11 | `com.github.developer.weapons.service.WechatComponentService.getComponentToken` 12 | - 根据 component_token 获取 pre_auth_code 13 | `com.github.developer.weapons.service.WechatComponentService.getPreAuthCode` 14 | - 根据回调得到的 auth_code 获取授权信息 15 | `com.github.developer.weapons.service.WechatComponentService.getAuth` 16 | - 根据授权信息获取详细资料 17 | `com.github.developer.weapons.service.WechatComponentService.getAuthorizerInfo` 18 | - 根据回复消息自动加密 19 | `com.github.developer.weapons.service.WechatComponentService.encryptMsg` 20 | - 根据条件获取授权链接 21 | `com.github.developer.weapons.service.WechatComponentService.generateLoginUrl` 22 | 23 | ### 1.2.0 24 | #### 第三方平台 25 | - 根据 token 和 openid 获取用户资料 26 | `com.github.developer.weapons.service.WechatOfficialService.getUserInfo` 27 | 28 | ### 1.2.8 29 | #### 公众号 30 | - 发送模板消息 31 | `com.github.developer.weapons.service.WechatOfficialService.sendTemplateMsg` 32 | - 发送消息 33 | `com.github.developer.weapons.service.WechatOfficialService.sendMsg` 34 | 35 | ### 1.3.0 36 | #### 公众号 37 | - 获取 accessToken 38 | `com.github.developer.weapons.service.WechatOfficialService.getAccessToken` 39 | - 创建带参数的二维码 40 | `com.github.developer.weapons.service.WechatOfficialService.createQrCode` 41 | 42 | ## Usage 43 | 1. 在 `pom.xml` 里面添加公开仓库 44 | ```xml 45 | 46 | 47 | developer-weapons-repository 48 | https://raw.githubusercontent.com/developer-weapons/repository/master 49 | 50 | 51 | ``` 52 | 2. 引入对应的包版本 53 | ```xml 54 | 55 | com.github.developer.weapons 56 | wechat-spring-boot-starter 57 | 1.3.0 58 | 59 | ``` -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialAutoReplyMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.github.developer.weapons.model.Token; 4 | import com.github.developer.weapons.util.XmlUtils; 5 | import lombok.Getter; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * 公众号被动回复消息 11 | */ 12 | public class OfficialAutoReplyMessage extends Token { 13 | /** 14 | * 发送用户 15 | */ 16 | @Getter 17 | private String ToUserName; 18 | /** 19 | * 发送的订阅号 20 | */ 21 | @Getter 22 | private String FromUserName; 23 | 24 | /** 25 | * 创建时间 26 | */ 27 | @Getter 28 | private Long CreateTime; 29 | 30 | /** 31 | * 消息类型 32 | */ 33 | @Getter 34 | private MessageTypeEnum MsgType; 35 | 36 | /** 37 | * 内容 38 | */ 39 | @Getter 40 | private String Content; 41 | 42 | /** 43 | * 当内容是消息类型的时候设置发送内容 44 | * 45 | * @param content 46 | * @return 47 | */ 48 | public OfficialAutoReplyMessage withContent(String content) { 49 | this.Content = content; 50 | return this; 51 | } 52 | 53 | 54 | /** 55 | * 设置 ToUserName 56 | * 57 | * @param toUserName 58 | * @return 59 | */ 60 | public OfficialAutoReplyMessage withToUserName(String toUserName) { 61 | this.ToUserName = toUserName; 62 | return this; 63 | } 64 | 65 | /** 66 | * 设置 FromUserName 67 | * 68 | * @param fromUserName 69 | * @return 70 | */ 71 | public OfficialAutoReplyMessage withFromUserName(String fromUserName) { 72 | this.FromUserName = fromUserName; 73 | return this; 74 | } 75 | 76 | /** 77 | * 设置 CreateTime 78 | * 79 | * @param createTime 80 | * @return 81 | */ 82 | public OfficialAutoReplyMessage withCreateTime(Long createTime) { 83 | this.CreateTime = createTime; 84 | return this; 85 | } 86 | 87 | 88 | /** 89 | * 设置类型 90 | * 91 | * @param msgtype 92 | * @return 93 | */ 94 | public OfficialAutoReplyMessage withMsgtype(MessageTypeEnum msgtype) { 95 | this.MsgType = msgtype; 96 | return this; 97 | } 98 | 99 | /** 100 | * 直接获取实例的静态方法 101 | * 102 | * @return 103 | */ 104 | public static OfficialAutoReplyMessage build() { 105 | OfficialAutoReplyMessage officialAutoReplyMessage = new OfficialAutoReplyMessage(); 106 | officialAutoReplyMessage.withCreateTime(new Date().getTime()); 107 | return officialAutoReplyMessage; 108 | } 109 | 110 | /** 111 | * 转化 xml 格式 112 | * 113 | * @return 114 | */ 115 | public String toXml() { 116 | return XmlUtils.objectToXml(this); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.net.URL; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @Slf4j 14 | public class FileUtils { 15 | private static List suffix = new ArrayList<>(); 16 | 17 | static { 18 | suffix.add(".png"); 19 | suffix.add(".bmp"); 20 | suffix.add(".gif"); 21 | suffix.add(".jpeg"); 22 | suffix.add(".jpg"); 23 | } 24 | 25 | public static boolean hasSuffix(String fileName) { 26 | for (String s : suffix) { 27 | if (StringUtils.endsWithIgnoreCase(fileName, s)) { 28 | return true; 29 | } 30 | } 31 | return false; 32 | } 33 | 34 | public static String newUUIDFileName(String fileName) { 35 | if (StringUtils.isBlank(fileName)) { 36 | return fileName; 37 | } 38 | if (hasSuffix(fileName)) { 39 | String[] filePaths = fileName.split("\\."); 40 | if (filePaths.length > 1) { 41 | return UUID.randomUUID().toString().replace("-", "") + "." + filePaths[filePaths.length - 1]; 42 | } else { 43 | return newUUIDPNGFileName(); 44 | } 45 | } else { 46 | return newUUIDPNGFileName(); 47 | } 48 | } 49 | 50 | public static String newUUIDPNGFileName() { 51 | return UUID.randomUUID().toString().replace("-", "") + ".png"; 52 | } 53 | 54 | public static String newLocalFileName(String fileName) { 55 | String path = System.getProperty("user.dir") + File.separator; 56 | return newLocalFileName(path, fileName); 57 | } 58 | 59 | public static String newLocalFileName(String path, String fileName) { 60 | return path + newUUIDFileName(fileName); 61 | } 62 | 63 | public static File newFile(String url) { 64 | File file; 65 | String localFile = newLocalFileName(url); 66 | log.info("FILE_UTILS_NEW_INFO, url : {}, localFile : {}", url, localFile); 67 | file = new File(localFile); 68 | try { 69 | org.apache.commons.io.FileUtils.copyURLToFile(new URL(url), file); 70 | } catch (IOException e) { 71 | log.error("FILE_UTILS_NEW_ERROR, url : {}", url); 72 | return null; 73 | } 74 | return file; 75 | } 76 | 77 | public static void deleteFile(File file) { 78 | try { 79 | file.delete(); 80 | } catch (Exception e) { 81 | } 82 | } 83 | 84 | public static void main(String[] args) { 85 | System.out.println(FileUtils.newLocalFileName("http://luckydraw.cn-bj.ufileos.com/ffb5134b-8070-4d65-be0f-c3b1a0050dbc.jpg")); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialTemplateMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.github.developer.weapons.model.Token; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 公众号模板消息 12 | */ 13 | public class OfficialTemplateMessage extends Token { 14 | /** 15 | * 接收消息的 OPENID 16 | */ 17 | @Getter 18 | private String touser; 19 | 20 | /** 21 | * 模板id 22 | */ 23 | @Getter 24 | private String template_id; 25 | 26 | /** 27 | * 跳转链接 28 | */ 29 | @Getter 30 | private String url; 31 | 32 | /** 33 | * 小程序 34 | */ 35 | @Getter 36 | private Map miniprogram; 37 | 38 | /** 39 | * 内容 40 | */ 41 | @Getter 42 | private Map> data; 43 | 44 | /** 45 | * 设置小程序 46 | * 47 | * @param appid 48 | * @param pagepath 49 | * @return 50 | */ 51 | public OfficialTemplateMessage withMiniProgram(String appid, String pagepath) { 52 | if (miniprogram == null) { 53 | miniprogram = new HashMap<>(); 54 | } 55 | miniprogram.put("appid", appid); 56 | miniprogram.put("pagepath", pagepath); 57 | return this; 58 | } 59 | 60 | /** 61 | * 设置内容 62 | * 63 | * @param k 64 | * @param v 65 | * @param color 66 | * @return 67 | */ 68 | public OfficialTemplateMessage withData(String k, String v, String color) { 69 | if (data == null) { 70 | data = new HashMap<>(); 71 | } 72 | Map value = new HashMap<>(); 73 | value.put("value", v); 74 | if (StringUtils.isNotBlank(color)) { 75 | value.put("color", color); 76 | } 77 | data.put(k, value); 78 | return this; 79 | } 80 | 81 | /** 82 | * 设置 toUser 83 | * 84 | * @param touser 85 | * @return 86 | */ 87 | public OfficialTemplateMessage withTouser(String touser) { 88 | this.touser = touser; 89 | return this; 90 | } 91 | 92 | /** 93 | * 设置 templateId 94 | * 95 | * @param templateId 96 | * @return 97 | */ 98 | public OfficialTemplateMessage withTemplateId(String templateId) { 99 | this.template_id = templateId; 100 | return this; 101 | } 102 | 103 | /** 104 | * 设置 url 105 | * 106 | * @param url 107 | * @return 108 | */ 109 | public OfficialTemplateMessage withUrL(String url) { 110 | this.url = url; 111 | return this; 112 | } 113 | 114 | /** 115 | * 直接获取实例的静态方法 116 | * 117 | * @return 118 | */ 119 | public static OfficialTemplateMessage build() { 120 | return new OfficialTemplateMessage(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/XMLParse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 对公众平台发送给公众账号的消息加解密示例代码. 3 | * 4 | * @copyright Copyright (c) 1998-2014 Tencent Inc. 5 | */ 6 | 7 | // ------------------------------------------------------------------------ 8 | 9 | package com.github.developer.weapons.util.aes; 10 | 11 | import com.github.developer.weapons.model.WechatEnText; 12 | import com.github.developer.weapons.util.XmlUtils; 13 | import org.w3c.dom.Document; 14 | import org.w3c.dom.Element; 15 | import org.w3c.dom.NodeList; 16 | import org.xml.sax.InputSource; 17 | 18 | import javax.xml.parsers.DocumentBuilder; 19 | import javax.xml.parsers.DocumentBuilderFactory; 20 | import java.io.StringReader; 21 | 22 | /** 23 | * XMLParse class 24 | *

25 | * 提供提取消息格式中的密文及生成回复消息格式的接口. 26 | */ 27 | class XMLParse { 28 | 29 | /** 30 | * 提取出xml数据包中的加密消息 31 | * 32 | * @param xmltext 待提取的xml字符串 33 | * @return 提取出的加密消息字符串 34 | * @throws AesException 35 | */ 36 | public static Object[] extract(String xmltext) throws AesException { 37 | Object[] result = new Object[3]; 38 | try { 39 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 40 | dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 41 | dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); 42 | dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 43 | dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 44 | dbf.setXIncludeAware(false); 45 | dbf.setExpandEntityReferences(false); 46 | DocumentBuilder db = dbf.newDocumentBuilder(); 47 | StringReader sr = new StringReader(xmltext); 48 | InputSource is = new InputSource(sr); 49 | Document document = db.parse(is); 50 | 51 | Element root = document.getDocumentElement(); 52 | NodeList nodelist1 = root.getElementsByTagName("Encrypt"); 53 | // NodeList nodelist2 = root.getElementsByTagName("ToUserName"); 54 | result[0] = 0; 55 | result[1] = nodelist1.item(0).getTextContent(); 56 | // result[2] = nodelist2.item(0).getTextContent(); 57 | return result; 58 | } catch (Exception e) { 59 | throw new AesException(AesException.ParseXmlError); 60 | } 61 | } 62 | 63 | /** 64 | * 生成xml消息 65 | * 66 | * @param encrypt 加密后的消息密文 67 | * @param signature 安全签名 68 | * @param timestamp 时间戳 69 | * @param nonce 随机字符串 70 | * @return 生成的xml字符串 71 | */ 72 | public static String generate(String encrypt, String signature, String timestamp, String nonce) { 73 | WechatEnText enText = new WechatEnText(); 74 | enText.setEncrypt(encrypt); 75 | enText.setMsgSignature(signature); 76 | enText.setTimeStamp(timestamp); 77 | enText.setNonce(nonce); 78 | return XmlUtils.objectToXml(enText); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.1.2.RELEASE 11 | 12 | 13 | 14 | com.github.developer.weapons 15 | wechat-spring-boot-starter 16 | 1.3.0 17 | 18 | 19 | developer-weapons-repository 20 | /Users/codedrinker/Code/developer-weapons-repository 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-autoconfigure 27 | 28 | 29 | 30 | 31 | 32 | org.apache.commons 33 | commons-lang3 34 | 3.9 35 | 36 | 37 | org.projectlombok 38 | lombok 39 | 1.18.6 40 | provided 41 | 42 | 43 | commons-io 44 | commons-io 45 | 2.6 46 | 47 | 48 | commons-codec 49 | commons-codec 50 | 1.9 51 | 52 | 53 | 54 | 55 | dom4j 56 | dom4j 57 | 1.6.1 58 | 59 | 60 | com.thoughtworks.xstream 61 | xstream 62 | 1.4.11.1 63 | 64 | 65 | 66 | 67 | com.squareup.okhttp3 68 | okhttp 69 | 3.14.1 70 | 71 | 72 | com.alibaba 73 | fastjson 74 | 1.2.57 75 | 76 | 77 | 78 | 79 | org.slf4j 80 | slf4j-api 81 | 1.7.25 82 | 83 | 84 | ch.qos.logback 85 | logback-classic 86 | 1.2.3 87 | 88 | 89 | org.slf4j 90 | slf4j-log4j12 91 | 1.7.2 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/model/official/OfficialCustomMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.model.official; 2 | 3 | import com.github.developer.weapons.model.Token; 4 | import lombok.Getter; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 公众号自定义消息 12 | */ 13 | public class OfficialCustomMessage extends Token { 14 | /** 15 | * 接收消息的 OPENID 16 | */ 17 | @Getter 18 | private String touser; 19 | 20 | /** 21 | * 消息类型 22 | */ 23 | @Getter 24 | private MessageTypeEnum msgtype; 25 | 26 | /** 27 | * 文本消息内容 28 | */ 29 | @Getter 30 | private Map text; 31 | 32 | 33 | /** 34 | * 图片消息内容 35 | */ 36 | @Getter 37 | private Map image; 38 | 39 | /** 40 | * 列表 41 | */ 42 | @Getter 43 | private MenuMsg msgmenu; 44 | 45 | /** 46 | * 设置头文字 47 | * 48 | * @param content 49 | * @return 50 | */ 51 | public OfficialCustomMessage withHeadContent(String content) { 52 | if (msgmenu == null) { 53 | msgmenu = new MenuMsg(); 54 | } 55 | msgmenu.setHeadContent(content); 56 | return this; 57 | } 58 | 59 | /** 60 | * 设置尾文字 61 | * 62 | * @param content 63 | * @return 64 | */ 65 | public OfficialCustomMessage withTailContent(String content) { 66 | if (msgmenu == null) { 67 | msgmenu = new MenuMsg(); 68 | } 69 | msgmenu.setTailContent(content); 70 | return this; 71 | } 72 | 73 | /** 74 | * 设置按钮 75 | * 76 | * @return 77 | */ 78 | public OfficialCustomMessage withMenu(String id, String content) { 79 | if (msgmenu == null) { 80 | msgmenu = new MenuMsg(); 81 | } 82 | if (msgmenu.getList() == null) { 83 | msgmenu.setList(new ArrayList<>()); 84 | } 85 | MenuMsgItem menuMsgItem = new MenuMsgItem(); 86 | menuMsgItem.setId(id); 87 | menuMsgItem.setContent(content); 88 | msgmenu.getList().add(menuMsgItem); 89 | return this; 90 | } 91 | 92 | /** 93 | * 当内容是消息类型的时候设置发送内容 94 | * 95 | * @param content 96 | * @return 97 | */ 98 | public OfficialCustomMessage withContent(String content) { 99 | if (text == null) { 100 | text = new HashMap<>(); 101 | } 102 | text.put("content", content); 103 | return this; 104 | } 105 | 106 | /** 107 | * 图片消息的时候,进行图片设置 108 | * 109 | * @param mediaId 110 | * @return 111 | */ 112 | public OfficialCustomMessage withMediaId(String mediaId) { 113 | if (image == null) { 114 | image = new HashMap<>(); 115 | } 116 | image.put("media_id", mediaId); 117 | return this; 118 | } 119 | 120 | 121 | /** 122 | * 设置 toUser 123 | * 124 | * @param touser 125 | * @return 126 | */ 127 | public OfficialCustomMessage withTouser(String touser) { 128 | this.touser = touser; 129 | return this; 130 | } 131 | 132 | 133 | /** 134 | * 设置类型 135 | * 136 | * @param msgtype 137 | * @return 138 | */ 139 | public OfficialCustomMessage withMsgtype(MessageTypeEnum msgtype) { 140 | this.msgtype = msgtype; 141 | return this; 142 | } 143 | 144 | /** 145 | * 直接获取实例的静态方法 146 | * 147 | * @return 148 | */ 149 | public static OfficialCustomMessage build() { 150 | return new OfficialCustomMessage(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/service/WechatBaseService.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.service; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.github.developer.weapons.exception.WechatException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import okhttp3.*; 7 | 8 | import java.io.IOException; 9 | 10 | @Slf4j 11 | public class WechatBaseService { 12 | protected OkHttpClient okHttpClient = new OkHttpClient(); 13 | 14 | protected String post(String url, JSONObject body) { 15 | Request request = new Request.Builder() 16 | .addHeader("body-type", "application/json") 17 | .url(url) 18 | .post(RequestBody.create(MediaType.parse("application/json"), body.toJSONString())) 19 | .build(); 20 | try { 21 | Response execute = okHttpClient.newCall(request).execute(); 22 | if (execute.isSuccessful()) { 23 | assert execute.body() != null; 24 | String result = execute.body().string(); 25 | log.debug("WechatBaseService post, url : {}, body: {}, result : {}", url, body, result); 26 | return result; 27 | } 28 | log.error("WechatBaseService post error, url : {}, body: {}, message : {}", url, body, execute.message()); 29 | throw new WechatException("post to wechat endpoint " + url + " error " + execute.message()); 30 | } catch (IOException e) { 31 | throw new WechatException("post to wechat endpoint " + url + " error ", e); 32 | } 33 | } 34 | 35 | protected String post(String url, String body) { 36 | Request request = new Request.Builder() 37 | .addHeader("body-type", "application/json") 38 | .url(url) 39 | .post(RequestBody.create(MediaType.parse("application/json"), body)) 40 | .build(); 41 | try { 42 | Response execute = okHttpClient.newCall(request).execute(); 43 | if (execute.isSuccessful()) { 44 | assert execute.body() != null; 45 | String result = execute.body().string(); 46 | log.debug("WechatBaseService post, url : {}, body: {}, result : {}", url, body, result); 47 | return result; 48 | } 49 | log.error("WechatBaseService post error, url : {}, body: {}, message : {}", url, body, execute.message()); 50 | throw new WechatException("post to wechat endpoint " + url + " error " + execute.message()); 51 | } catch (IOException e) { 52 | log.error("WechatBaseService post error, url : {}, body: {}", url, body, e); 53 | throw new WechatException("post to wechat endpoint " + url + " error ", e); 54 | } 55 | } 56 | 57 | protected String get(String url) { 58 | Request request = new Request.Builder() 59 | .addHeader("body-type", "application/json") 60 | .url(url) 61 | .get() 62 | .build(); 63 | try { 64 | Response execute = okHttpClient.newCall(request).execute(); 65 | if (execute.isSuccessful()) { 66 | assert execute.body() != null; 67 | String result = execute.body().string(); 68 | log.debug("WechatBaseService get, url : {}, result : {}", url, result); 69 | return result; 70 | } 71 | log.error("WechatBaseService get error, url : {}, message : {}", url, execute.message()); 72 | throw new WechatException("get to wechat endpoint " + url + " error " + execute.message()); 73 | } catch (IOException e) { 74 | log.error("WechatBaseService get error, url : {}", url, e); 75 | throw new WechatException("get to wechat endpoint " + url + " error ", e); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/util/aes/WXBizMsgCrypt.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 对公众平台发送给公众账号的消息加解密示例代码. 3 | * 4 | * @copyright Copyright (c) 1998-2014 Tencent Inc. 5 | */ 6 | 7 | // ------------------------------------------------------------------------ 8 | 9 | /** 10 | * 针对org.apache.commons.codec.binary.Base64, 11 | * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) 12 | * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi 13 | */ 14 | package com.github.developer.weapons.util.aes; 15 | 16 | import org.apache.commons.codec.binary.Base64; 17 | 18 | import javax.crypto.Cipher; 19 | import javax.crypto.spec.IvParameterSpec; 20 | import javax.crypto.spec.SecretKeySpec; 21 | import java.nio.charset.Charset; 22 | import java.util.Arrays; 23 | import java.util.Random; 24 | 25 | /** 26 | * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串). 27 | *

    28 | *
  1. 第三方回复加密消息给公众平台
  2. 29 | *
  3. 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
  4. 30 | *
31 | * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 32 | *
    33 | *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: 34 | * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. 35 | *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. 36 | *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. 37 | *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. 38 | *
39 | */ 40 | public class WXBizMsgCrypt { 41 | static Charset CHARSET = Charset.forName("utf-8"); 42 | Base64 base64 = new Base64(); 43 | byte[] aesKey; 44 | String token; 45 | String appId; 46 | 47 | /** 48 | * 构造函数 49 | * @param token 公众平台上,开发者设置的token 50 | * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey 51 | * @param appId 公众平台appid 52 | * 53 | * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 54 | */ 55 | public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException { 56 | if (encodingAesKey.length() != 43) { 57 | throw new AesException(AesException.IllegalAesKey); 58 | } 59 | 60 | this.token = token; 61 | this.appId = appId; 62 | aesKey = Base64.decodeBase64(encodingAesKey + "="); 63 | } 64 | 65 | // 生成4个字节的网络字节序 66 | byte[] getNetworkBytesOrder(int sourceNumber) { 67 | byte[] orderBytes = new byte[4]; 68 | orderBytes[3] = (byte) (sourceNumber & 0xFF); 69 | orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF); 70 | orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF); 71 | orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF); 72 | return orderBytes; 73 | } 74 | 75 | // 还原4个字节的网络字节序 76 | int recoverNetworkBytesOrder(byte[] orderBytes) { 77 | int sourceNumber = 0; 78 | for (int i = 0; i < 4; i++) { 79 | sourceNumber <<= 8; 80 | sourceNumber |= orderBytes[i] & 0xff; 81 | } 82 | return sourceNumber; 83 | } 84 | 85 | // 随机生成16位字符串 86 | String getRandomStr() { 87 | String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 88 | Random random = new Random(); 89 | StringBuffer sb = new StringBuffer(); 90 | for (int i = 0; i < 16; i++) { 91 | int number = random.nextInt(base.length()); 92 | sb.append(base.charAt(number)); 93 | } 94 | return sb.toString(); 95 | } 96 | 97 | /** 98 | * 对明文进行加密. 99 | * 100 | * @param text 需要加密的明文 101 | * @return 加密后base64编码的字符串 102 | * @throws AesException aes加密失败 103 | */ 104 | String encrypt(String randomStr, String text) throws AesException { 105 | ByteGroup byteCollector = new ByteGroup(); 106 | byte[] randomStrBytes = randomStr.getBytes(CHARSET); 107 | byte[] textBytes = text.getBytes(CHARSET); 108 | byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length); 109 | byte[] appidBytes = appId.getBytes(CHARSET); 110 | 111 | // randomStr + networkBytesOrder + text + appid 112 | byteCollector.addBytes(randomStrBytes); 113 | byteCollector.addBytes(networkBytesOrder); 114 | byteCollector.addBytes(textBytes); 115 | byteCollector.addBytes(appidBytes); 116 | 117 | // ... + pad: 使用自定义的填充方式对明文进行补位填充 118 | byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); 119 | byteCollector.addBytes(padBytes); 120 | 121 | // 获得最终的字节流, 未加密 122 | byte[] unencrypted = byteCollector.toBytes(); 123 | 124 | try { 125 | // 设置加密模式为AES的CBC模式 126 | Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); 127 | SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); 128 | IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); 129 | cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); 130 | 131 | // 加密 132 | byte[] encrypted = cipher.doFinal(unencrypted); 133 | 134 | // 使用BASE64对加密后的字符串进行编码 135 | String base64Encrypted = base64.encodeToString(encrypted); 136 | 137 | return base64Encrypted; 138 | } catch (Exception e) { 139 | e.printStackTrace(); 140 | throw new AesException(AesException.EncryptAESError); 141 | } 142 | } 143 | 144 | /** 145 | * 对密文进行解密. 146 | * 147 | * @param text 需要解密的密文 148 | * @return 解密得到的明文 149 | * @throws AesException aes解密失败 150 | */ 151 | String decrypt(String text) throws AesException { 152 | byte[] original; 153 | try { 154 | // 设置解密模式为AES的CBC模式 155 | Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); 156 | SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES"); 157 | IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); 158 | cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); 159 | 160 | // 使用BASE64对密文进行解码 161 | byte[] encrypted = Base64.decodeBase64(text); 162 | 163 | // 解密 164 | original = cipher.doFinal(encrypted); 165 | } catch (Exception e) { 166 | e.printStackTrace(); 167 | throw new AesException(AesException.DecryptAESError); 168 | } 169 | 170 | String xmlContent, from_appid; 171 | try { 172 | // 去除补位字符 173 | byte[] bytes = PKCS7Encoder.decode(original); 174 | 175 | // 分离16位随机字符串,网络字节序和AppId 176 | byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); 177 | 178 | int xmlLength = recoverNetworkBytesOrder(networkOrder); 179 | 180 | xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET); 181 | from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), 182 | CHARSET); 183 | } catch (Exception e) { 184 | e.printStackTrace(); 185 | throw new AesException(AesException.IllegalBuffer); 186 | } 187 | 188 | // appid不相同的情况 189 | if (!from_appid.equals(appId)) { 190 | throw new AesException(AesException.ValidateAppidError); 191 | } 192 | return xmlContent; 193 | 194 | } 195 | 196 | /** 197 | * 将公众平台回复用户的消息加密打包. 198 | *
    199 | *
  1. 对要发送的消息进行AES-CBC加密
  2. 200 | *
  3. 生成安全签名
  4. 201 | *
  5. 将消息密文和安全签名打包成xml格式
  6. 202 | *
203 | * 204 | * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串 205 | * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp 206 | * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce 207 | * 208 | * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 209 | * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 210 | */ 211 | public String encryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException { 212 | // 加密 213 | String encrypt = encrypt(getRandomStr(), replyMsg); 214 | 215 | // 生成安全签名 216 | if (timeStamp == "") { 217 | timeStamp = Long.toString(System.currentTimeMillis()); 218 | } 219 | 220 | String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt); 221 | 222 | // System.out.println("发送给平台的签名是: " + signature[1].toString()); 223 | // 生成发送的xml 224 | String result = XMLParse.generate(encrypt, signature, timeStamp, nonce); 225 | return result; 226 | } 227 | 228 | /** 229 | * 检验消息的真实性,并且获取解密后的明文. 230 | *
    231 | *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. 232 | *
  3. 若验证通过,则提取xml中的加密消息
  4. 233 | *
  5. 对消息进行解密
  6. 234 | *
235 | * 236 | * @param msgSignature 签名串,对应URL参数的msg_signature 237 | * @param timeStamp 时间戳,对应URL参数的timestamp 238 | * @param nonce 随机串,对应URL参数的nonce 239 | * @param postData 密文,对应POST请求的数据 240 | * 241 | * @return 解密后的原文 242 | * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 243 | */ 244 | public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData) 245 | throws AesException { 246 | 247 | // 密钥,公众账号的app secret 248 | // 提取密文 249 | Object[] encrypt = XMLParse.extract(postData); 250 | 251 | // 验证安全签名 252 | String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString()); 253 | 254 | // 和URL中的签名比较是否相等 255 | // System.out.println("第三方收到URL中的签名:" + msg_sign); 256 | // System.out.println("第三方校验签名:" + signature); 257 | if (!signature.equals(msgSignature)) { 258 | throw new AesException(AesException.ValidateSignatureError); 259 | } 260 | 261 | // 解密 262 | String result = decrypt(encrypt[1].toString()); 263 | return result; 264 | } 265 | 266 | /** 267 | * 验证URL 268 | * @param msgSignature 签名串,对应URL参数的msg_signature 269 | * @param timeStamp 时间戳,对应URL参数的timestamp 270 | * @param nonce 随机串,对应URL参数的nonce 271 | * @param echoStr 随机串,对应URL参数的echostr 272 | * 273 | * @return 解密之后的echostr 274 | * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 275 | */ 276 | public String verifyUrl(String msgSignature, String timeStamp, String nonce, String echoStr) 277 | throws AesException { 278 | String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr); 279 | 280 | if (!signature.equals(msgSignature)) { 281 | throw new AesException(AesException.ValidateSignatureError); 282 | } 283 | 284 | String result = decrypt(echoStr); 285 | return result; 286 | } 287 | 288 | } -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/service/WechatComponentService.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.service; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.github.developer.weapons.config.WechatComponentProperties; 6 | import com.github.developer.weapons.exception.WechatException; 7 | import com.github.developer.weapons.model.component.*; 8 | import com.github.developer.weapons.util.XmlUtils; 9 | import com.github.developer.weapons.util.aes.AesException; 10 | import com.github.developer.weapons.util.aes.WXBizMsgCrypt; 11 | import com.sun.corba.se.impl.ior.OldJIDLObjectKeyTemplate; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | import java.util.Date; 16 | import java.util.Map; 17 | import java.util.UUID; 18 | 19 | /** 20 | * 微信第三方平台服务 21 | */ 22 | public class WechatComponentService extends WechatBaseService { 23 | 24 | @Autowired 25 | private WechatComponentProperties wechatComponentProperties; 26 | 27 | private WXBizMsgCrypt wxBizMsgCrypt; 28 | 29 | /** 30 | * 通过 verifyInfo 获取授权的订阅号信息 31 | * 32 | * @param verifyInfo 33 | * @return 34 | */ 35 | public Map getVerifiedInfo(ComponentVerifyInfo verifyInfo) { 36 | beforeCalling(); 37 | String decodeMsg = null; 38 | try { 39 | decodeMsg = wxBizMsgCrypt.decryptMsg(verifyInfo.getSignature(), verifyInfo.getTimestamp(), verifyInfo.getNonce(), verifyInfo.getBody()); 40 | Map maps = XmlUtils.xmlToMap(decodeMsg); 41 | String appId = maps.get("AppId"); 42 | String infoType = maps.get("InfoType"); 43 | if ("component_verify_ticket".equals(infoType)) { 44 | if (!StringUtils.equals(wechatComponentProperties.getAppId(), appId)) { 45 | throw new WechatException("component_verify_ticket does not match appId : " + wechatComponentProperties.getAppId()); 46 | } 47 | } 48 | return maps; 49 | } catch (Exception e) { 50 | throw new WechatException("component_verify_ticket error", e); 51 | } 52 | } 53 | 54 | /** 55 | * 根据授权账号的信息,获取授权账号的详细内容 56 | * 57 | * @param componentToken 58 | * @param authorizerAppId 59 | * @return 60 | */ 61 | public ComponentAuthInfo refreshAccessToken(String componentToken, String authorizerAppId, String authorizerRefreshToken) { 62 | if (componentToken == null) { 63 | throw new WechatException("componentToken is missing"); 64 | } 65 | String url = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=" + componentToken; 66 | JSONObject bodyObject = new JSONObject(); 67 | bodyObject.put("component_appid", wechatComponentProperties.getAppId()); 68 | bodyObject.put("authorizer_appid", authorizerAppId); 69 | bodyObject.put("authorizer_refresh_token", authorizerRefreshToken); 70 | String post = post(url, bodyObject); 71 | JSONObject jsonObject = JSON.parseObject(post); 72 | ComponentAuthorizerInfo authorizerInfo = new ComponentAuthorizerInfo(); 73 | String accessToken = jsonObject.getString("authorizer_access_token"); 74 | String refreshToken = jsonObject.getString("authorizer_refresh_token"); 75 | ComponentAuthInfo componentAuthInfo = new ComponentAuthInfo(); 76 | componentAuthInfo.setAuthorizerRefreshToken(refreshToken); 77 | componentAuthInfo.setToken(accessToken); 78 | return componentAuthInfo; 79 | } 80 | 81 | /** 82 | * 根据授权账号的信息,获取授权账号的详细内容 83 | * 84 | * @param componentToken 85 | * @param authorizerAppId 86 | * @return 87 | */ 88 | public ComponentAuthorizerInfo getAuthorizerInfo(String componentToken, String authorizerAppId) { 89 | if (componentToken == null) { 90 | throw new WechatException("componentToken is missing"); 91 | } 92 | String url = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=" + componentToken; 93 | JSONObject bodyObject = new JSONObject(); 94 | bodyObject.put("component_appid", wechatComponentProperties.getAppId()); 95 | bodyObject.put("authorizer_appid", authorizerAppId); 96 | String post = post(url, bodyObject); 97 | JSONObject jsonObject = JSON.parseObject(post); 98 | ComponentAuthorizerInfo authorizerInfo = new ComponentAuthorizerInfo(); 99 | JSONObject authorizer_info = jsonObject.getJSONObject("authorizer_info"); 100 | JSONObject authorization_info = jsonObject.getJSONObject("authorization_info"); 101 | authorizerInfo.setQrcodeUrl(authorizer_info.getString("qrcode_url")); 102 | authorizerInfo.setUserName(authorizer_info.getString("user_name")); 103 | authorizerInfo.setNickName(authorizer_info.getString("nick_name")); 104 | authorizerInfo.setHeadImg(authorizer_info.getString("head_img")); 105 | authorizerInfo.setVerifyTypeInfo(authorizer_info.getJSONObject("verify_type_info").getInteger("id")); 106 | authorizerInfo.setServiceTypeInfo(authorizer_info.getJSONObject("service_type_info").getInteger("id")); 107 | authorizerInfo.setAuthorizerAppid(authorization_info.getString("authorizer_appid")); 108 | authorizerInfo.setAuthorizerRefreshToken(authorization_info.getString("authorizer_refresh_token")); 109 | return authorizerInfo; 110 | } 111 | 112 | /** 113 | * 根据token 和授权的 auth code 获取授权账号的信息 114 | * 115 | * @param componentToken 116 | * @param authCode 117 | * @return 118 | */ 119 | public ComponentAuthInfo getAuth(String componentToken, String authCode) { 120 | if (componentToken == null) { 121 | throw new WechatException("componentToken is missing"); 122 | } 123 | String url = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=" + componentToken; 124 | JSONObject bodyObject = new JSONObject(); 125 | bodyObject.put("component_appid", wechatComponentProperties.getAppId()); 126 | bodyObject.put("authorization_code", authCode); 127 | String post = post(url, bodyObject); 128 | JSONObject jsonObject = JSON.parseObject(post); 129 | JSONObject authorizationInfo = jsonObject.getJSONObject("authorization_info"); 130 | if (authorizationInfo != null) { 131 | ComponentAuthInfo authorizerToken = new ComponentAuthInfo(); 132 | authorizerToken.setToken(authorizationInfo.getString("authorizer_access_token")); 133 | authorizerToken.setAppId(authorizationInfo.getString("authorizer_appid")); 134 | authorizerToken.setAuthorizerRefreshToken(authorizationInfo.getString("authorizer_refresh_token")); 135 | return authorizerToken; 136 | } else { 137 | throw new WechatException("getAuth error with " + post); 138 | } 139 | } 140 | 141 | /** 142 | * 根据 token 获取 preauthcode 用户授权订阅号 143 | * 144 | * @param componentToken 145 | * @return 146 | */ 147 | public String getPreAuthCode(String componentToken) { 148 | if (componentToken == null) { 149 | throw new WechatException("componentToken is missing"); 150 | } 151 | String url = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=" + componentToken; 152 | JSONObject bodyObject = new JSONObject(); 153 | bodyObject.put("component_appid", wechatComponentProperties.getAppId()); 154 | String post = post(url, bodyObject); 155 | JSONObject jsonObject = JSON.parseObject(post); 156 | if (StringUtils.isNotBlank(jsonObject.getString("pre_auth_code"))) { 157 | return jsonObject.getString("pre_auth_code"); 158 | } else { 159 | throw new WechatException("obtain component_access_token error with " + post); 160 | } 161 | } 162 | 163 | /** 164 | * 根据缓存好的 Ticket 获取应用的 conponent_token 165 | * 166 | * @param ticket 167 | * @return 168 | */ 169 | public String getComponentToken(String ticket) { 170 | if (wechatComponentProperties.getSecret() == null) { 171 | throw new WechatException("wechat.component.secret is missing"); 172 | } 173 | 174 | String url = "https://api.weixin.qq.com/cgi-bin/component/api_component_token"; 175 | JSONObject bodyObject = new JSONObject(); 176 | bodyObject.put("component_appid", wechatComponentProperties.getAppId()); 177 | bodyObject.put("component_appsecret", wechatComponentProperties.getSecret()); 178 | bodyObject.put("component_verify_ticket", ticket); 179 | String post = post(url, bodyObject); 180 | JSONObject jsonObject = JSON.parseObject(post); 181 | if (StringUtils.isNotBlank(jsonObject.getString("component_access_token"))) { 182 | return jsonObject.getString("component_access_token"); 183 | } else { 184 | throw new WechatException("obtain component_access_token error with " + post); 185 | } 186 | } 187 | 188 | 189 | /** 190 | * 根据回复消息进行自动加密 191 | * 192 | * @param componentMessage 193 | * @return 194 | */ 195 | public String encryptMsg(Object componentMessage) { 196 | beforeCalling(); 197 | String str = XmlUtils.objectToXml(componentMessage); 198 | try { 199 | return wxBizMsgCrypt.encryptMsg(str, String.valueOf(System.currentTimeMillis()), UUID.randomUUID().toString().replace("-", "")); 200 | } catch (AesException e) { 201 | throw new WechatException("encryptMsg error", e); 202 | } 203 | } 204 | 205 | /** 206 | * 根据 componentToken 生成登录地址 207 | * 208 | * @param componentToken 209 | * @param redirectUri 210 | * @return 211 | */ 212 | public String generateLoginUrl(String componentToken, String redirectUri) { 213 | String url = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&auth_type=1&pre_auth_code=%s&redirect_uri=%s"; 214 | String authCode = getPreAuthCode(componentToken); 215 | return String.format(url, wechatComponentProperties.getAppId(), authCode, redirectUri); 216 | } 217 | 218 | private void beforeCalling() { 219 | if (wechatComponentProperties.getAesKey() == null) { 220 | throw new WechatException("wechat.component.aesKey is missing"); 221 | } 222 | if (wechatComponentProperties.getToken() == null) { 223 | throw new WechatException("wechat.component.token is missing"); 224 | } 225 | if (wechatComponentProperties.getAppId() == null) { 226 | throw new WechatException("wechat.component.appId is missing"); 227 | } 228 | try { 229 | wxBizMsgCrypt = new WXBizMsgCrypt(wechatComponentProperties.getToken(), wechatComponentProperties.getAesKey(), wechatComponentProperties.getAppId()); 230 | } catch (AesException e) { 231 | throw new WechatException("AesException " + e.getMessage()); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/main/java/com/github/developer/weapons/service/WechatOfficialService.java: -------------------------------------------------------------------------------- 1 | package com.github.developer.weapons.service; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.github.developer.weapons.exception.WechatException; 6 | import com.github.developer.weapons.model.Token; 7 | import com.github.developer.weapons.model.official.*; 8 | import com.github.developer.weapons.util.FileUtils; 9 | import com.github.developer.weapons.util.XmlUtils; 10 | import lombok.extern.slf4j.Slf4j; 11 | import okhttp3.MediaType; 12 | import okhttp3.Request; 13 | import okhttp3.RequestBody; 14 | import okhttp3.Response; 15 | import org.apache.commons.codec.digest.DigestUtils; 16 | import org.apache.commons.io.Charsets; 17 | import org.apache.commons.io.IOUtils; 18 | import org.apache.commons.lang3.StringUtils; 19 | import org.dom4j.Document; 20 | import org.dom4j.Element; 21 | import org.dom4j.io.SAXReader; 22 | 23 | import java.io.*; 24 | import java.net.HttpURLConnection; 25 | import java.net.URL; 26 | import java.util.Arrays; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | 32 | /** 33 | * 微信订阅号 34 | */ 35 | @Slf4j 36 | public class WechatOfficialService extends WechatBaseService { 37 | private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36"; 38 | 39 | /** 40 | * 验证请求是否正确 41 | * 42 | * @param signature 43 | * @param timestamp 44 | * @param nonce 45 | * @param token 46 | * @return 47 | */ 48 | public boolean isValid(String signature, String timestamp, String nonce, String token) { 49 | String[] values = {token, timestamp, nonce}; 50 | Arrays.sort(values); 51 | String value = values[0] + values[1] + values[2]; 52 | String sign = DigestUtils.shaHex(value); 53 | return StringUtils.equals(signature, sign); 54 | } 55 | 56 | /** 57 | * 非加密的信息流转换为map 58 | * 59 | * @param is 60 | * @return 61 | */ 62 | public Map toMap(InputStream is) { 63 | Map map = new HashMap<>(); 64 | try { 65 | SAXReader reader = new SAXReader(); 66 | Document doc = reader.read(is); 67 | Element root = doc.getRootElement(); 68 | List list = root.elements(); 69 | for (Element e : list) { 70 | map.put(e.getName(), e.getText()); 71 | } 72 | is.close(); 73 | return map; 74 | } catch (Exception e) { 75 | throw new WechatException("toMap error with" + e.getMessage()); 76 | } 77 | } 78 | 79 | /** 80 | * 转换构建好的类型为 String 81 | * 82 | * @param officialAutoReplyMessage 83 | * @return 84 | */ 85 | public String toMsg(OfficialAutoReplyMessage officialAutoReplyMessage) { 86 | return XmlUtils.objectToXml(officialAutoReplyMessage); 87 | } 88 | 89 | /** 90 | * 获取订阅号 token 91 | * https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET 92 | * 93 | * @param query 94 | * @return 95 | */ 96 | public Token getAccessToken(OfficalAccessTokenQuery query) { 97 | if (query.getAppid() == null) { 98 | throw new WechatException("appid is missing"); 99 | } 100 | if (query.getSecret() == null) { 101 | throw new WechatException("secret is missing"); 102 | } 103 | String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", query.getAppid(), query.getSecret()); 104 | String body = get(url); 105 | if (StringUtils.isNotBlank(body)) { 106 | Token token = JSON.parseObject(body, Token.class); 107 | return token; 108 | } else { 109 | throw new WechatException("obtain token error with " + body); 110 | } 111 | } 112 | 113 | /** 114 | * 通过 officialUserQuery 获取用户资料 115 | * https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 116 | * 117 | * @param officialUserQuery 118 | * @return 119 | */ 120 | public OfficialUserInfo getUserInfo(OfficialUserQuery officialUserQuery) { 121 | if (officialUserQuery.getAccessToken() == null) { 122 | throw new WechatException("accessToken is missing"); 123 | } 124 | if (officialUserQuery.getOpenId() == null) { 125 | throw new WechatException("openId is missing"); 126 | } 127 | String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + officialUserQuery.getAccessToken() + "&openid=" + officialUserQuery.getOpenId() + "&lang=zh_CN"; 128 | String body = get(url); 129 | if (StringUtils.isNotBlank(body)) { 130 | OfficialUserInfo officialUserInfo = JSON.parseObject(body, OfficialUserInfo.class); 131 | return officialUserInfo; 132 | } else { 133 | throw new WechatException("obtain user info error with " + body); 134 | } 135 | } 136 | 137 | /** 138 | * https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID 139 | * 140 | * @param accessToken 141 | * @param nextOpenId 142 | * @return 143 | */ 144 | public Fan getFans(String accessToken, String nextOpenId) { 145 | if (accessToken == null) { 146 | throw new WechatException("accessToken is missing"); 147 | } 148 | String url = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=" + accessToken; 149 | if (StringUtils.isNotBlank(nextOpenId)) { 150 | url += "&next_openid=" + nextOpenId; 151 | } 152 | String body = get(url); 153 | if (StringUtils.isNotBlank(body)) { 154 | Fan fan = JSON.parseObject(body, Fan.class); 155 | return fan; 156 | } else { 157 | throw new WechatException("obtain fan info error with " + body); 158 | } 159 | } 160 | 161 | 162 | /** 163 | * 发送消息 164 | * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN 165 | * 166 | * @param officialCustomMessage 167 | * @return 168 | */ 169 | public String sendMsg(OfficialCustomMessage officialCustomMessage) { 170 | if (officialCustomMessage.getAccessToken() == null) { 171 | throw new WechatException("accessToken is missing"); 172 | } 173 | if (officialCustomMessage.getTouser() == null) { 174 | throw new WechatException("openId is missing"); 175 | } 176 | String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + officialCustomMessage.getAccessToken(); 177 | return post(url, JSON.toJSONString(officialCustomMessage)); 178 | } 179 | 180 | /** 181 | * 发送模板消息 182 | * POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN 183 | * 184 | * @param officialTemplateMessage 185 | * @return 186 | */ 187 | public String sendTemplateMsg(OfficialTemplateMessage officialTemplateMessage) { 188 | if (officialTemplateMessage.getAccessToken() == null) { 189 | throw new WechatException("accessToken is missing"); 190 | } 191 | if (officialTemplateMessage.getTouser() == null) { 192 | throw new WechatException("openId is missing"); 193 | } 194 | String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + officialTemplateMessage.getAccessToken(); 195 | String body = JSON.toJSONString(officialTemplateMessage); 196 | return post(url, body); 197 | } 198 | 199 | /** 200 | * 创建菜单 201 | * POST https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN 202 | * 203 | * @param menus 204 | * @return 205 | */ 206 | public String createMenu(String accessToken, List menus) { 207 | if (menus == null || menus.size() == 0) { 208 | throw new WechatException("menus is empty"); 209 | } 210 | if (accessToken == null) { 211 | throw new WechatException("accessToken is missing"); 212 | } 213 | String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" + accessToken; 214 | JSONObject jsonObject = new JSONObject(); 215 | jsonObject.put("button", menus); 216 | String jsonString = jsonObject.toJSONString(); 217 | log.info("createMenu : {}", jsonString); 218 | return post(url, jsonString); 219 | } 220 | 221 | /** 222 | * 添加图片素材 223 | * 224 | * @param officialMaterial 225 | * @return 226 | */ 227 | public String addMaterial(OfficialMaterial officialMaterial) { 228 | File file = null; 229 | try { 230 | file = officialMaterial.getFile() != null ? officialMaterial.getFile() : FileUtils.newFile(officialMaterial.getUrl()); 231 | String url = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=image"; 232 | URL urlGet = new URL(String.format(url, officialMaterial.getAccessToken())); 233 | HttpURLConnection conn = (HttpURLConnection) urlGet.openConnection(); 234 | conn.setDoOutput(true); 235 | conn.setDoInput(true); 236 | conn.setUseCaches(false); 237 | conn.setRequestMethod("POST"); 238 | conn.setRequestProperty("connection", "Keep-Alive"); 239 | conn.setRequestProperty("user-agent", DEFAULT_USER_AGENT); 240 | conn.setRequestProperty("Charsert", "UTF-8"); 241 | // 定义数据分隔线 242 | String BOUNDARY = "----WebKitFormBoundaryiDGnV9zdZA1eM1yL"; 243 | conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); 244 | 245 | OutputStream out = new DataOutputStream(conn.getOutputStream()); 246 | // 定义最后数据分隔线 247 | StringBuilder mediaData = new StringBuilder(); 248 | mediaData.append("--").append(BOUNDARY).append("\r\n"); 249 | mediaData.append("Content-Disposition: form-data;name=\"media\";filename=\"" + file.getName() + "\"\r\n"); 250 | mediaData.append("Content-Type:application/octet-stream\r\n\r\n"); 251 | byte[] mediaDatas = mediaData.toString().getBytes(); 252 | out.write(mediaDatas); 253 | DataInputStream fs = new DataInputStream(new FileInputStream(file)); 254 | int bytes = 0; 255 | byte[] bufferOut = new byte[1024]; 256 | while ((bytes = fs.read(bufferOut)) != -1) { 257 | out.write(bufferOut, 0, bytes); 258 | } 259 | IOUtils.closeQuietly(fs); 260 | // 多个文件时,二个文件之间加入这个 261 | out.write("\r\n".getBytes()); 262 | byte[] end_data = ("\r\n--" + BOUNDARY + "--\r\n").getBytes(); 263 | out.write(end_data); 264 | out.flush(); 265 | IOUtils.closeQuietly(out); 266 | 267 | // 定义BufferedReader输入流来读取URL的响应 268 | InputStream in = conn.getInputStream(); 269 | BufferedReader read = new BufferedReader(new InputStreamReader(in, Charsets.UTF_8)); 270 | String valueString = null; 271 | StringBuffer bufferRes = null; 272 | bufferRes = new StringBuffer(); 273 | while ((valueString = read.readLine()) != null) { 274 | bufferRes.append(valueString); 275 | } 276 | IOUtils.closeQuietly(in); 277 | // 关闭连接 278 | if (conn != null) { 279 | conn.disconnect(); 280 | } 281 | String s = bufferRes.toString(); 282 | JSONObject jsonObject = JSON.parseObject(s); 283 | return jsonObject.getString("media_id"); 284 | } catch (Exception e) { 285 | log.error("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR", e); 286 | throw new WechatException("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR" + e.getMessage()); 287 | } finally { 288 | FileUtils.deleteFile(file); 289 | } 290 | } 291 | 292 | /** 293 | * 添加文章 294 | * 295 | * @param news 296 | * @return 297 | */ 298 | public String addNews(List news) { 299 | String url = "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s"; 300 | JSONObject req = new JSONObject(); 301 | req.put("articles", news); 302 | String content = JSON.toJSONString(req); 303 | Request request = new Request.Builder() 304 | .addHeader("content-type", "application/json") 305 | .url(String.format(url, news.get(0).getAccessToken())) 306 | .post(RequestBody.create(MediaType.parse("application/json"), content)) 307 | .build(); 308 | try { 309 | Response execute = okHttpClient.newCall(request).execute(); 310 | if (execute.isSuccessful()) { 311 | String string = execute.body().string(); 312 | log.info("WECHAT_OFFICIAL_ADD_MATERIAL : {}", string); 313 | JSONObject jsonObject = JSON.parseObject(string); 314 | return jsonObject.getString("media_id"); 315 | } 316 | log.error("WECHAT_OFFICIAL_ADD_NEWS_ERROR, message : {}, response : {}", content, execute); 317 | throw new WechatException("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR" + execute); 318 | 319 | } catch (Exception e) { 320 | log.error("WECHAT_OFFICIAL_ADD_NEWS_ERROR, message : {}", content, e); 321 | throw new WechatException("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR" + e.getMessage()); 322 | } 323 | } 324 | 325 | /** 326 | * 更新文章 327 | * 328 | * @param news 329 | * @return 330 | */ 331 | public String updateNews(OfficialNews news) { 332 | String url = "https://api.weixin.qq.com/cgi-bin/material/update_news?access_token=%s"; 333 | JSONObject req = new JSONObject(); 334 | req.put("articles", news); 335 | req.put("index", 0); 336 | req.put("media_id", news.getMediaId()); 337 | String content = JSON.toJSONString(req); 338 | Request request = new Request.Builder() 339 | .addHeader("content-type", "application/json") 340 | .url(String.format(url, news.getAccessToken())) 341 | .post(RequestBody.create(MediaType.parse("application/json"), content)) 342 | .build(); 343 | try { 344 | Response execute = okHttpClient.newCall(request).execute(); 345 | if (execute.isSuccessful()) { 346 | assert execute.body() != null; 347 | String string = execute.body().string(); 348 | log.info("WECHAT_OFFICIAL_UPDATE_MATERIAL : {}", string); 349 | return string; 350 | } 351 | log.error("WECHAT_OFFICIAL_ADD_NEWS_ERROR, message : {}, response : {}", content, execute); 352 | throw new WechatException("WECHAT_OFFICIAL_UPDATE_MATERIAL_ERROR" + execute); 353 | 354 | } catch (Exception e) { 355 | log.error("WECHAT_OFFICIAL_ADD_NEWS_ERROR, message : {}", content, e); 356 | throw new WechatException("WECHAT_OFFICIAL_UPDATE_MATERIAL_ERROR" + e.getMessage()); 357 | } 358 | } 359 | 360 | /** 361 | * 创建永久二维码 362 | * 363 | * @param accessToken 364 | * @param scene 365 | * @return 366 | */ 367 | @Deprecated 368 | public String createQrCode(String accessToken, String scene) { 369 | String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s"; 370 | JSONObject req = new JSONObject(); 371 | req.put("action_name", "QR_LIMIT_STR_SCENE"); 372 | JSONObject sceneStrObj = new JSONObject(); 373 | sceneStrObj.put("scene_str", scene); 374 | JSONObject sceneObj = new JSONObject(); 375 | sceneObj.put("scene", sceneStrObj); 376 | req.put("action_info", sceneObj); 377 | String content = JSON.toJSONString(req); 378 | Request request = new Request.Builder() 379 | .addHeader("content-type", "application/json") 380 | .url(String.format(url, accessToken)) 381 | .post(RequestBody.create(MediaType.parse("application/json"), content)) 382 | .build(); 383 | try { 384 | Response execute = okHttpClient.newCall(request).execute(); 385 | if (execute.isSuccessful()) { 386 | assert execute.body() != null; 387 | String string = execute.body().string(); 388 | log.info("WECHAT_OFFICIAL_CREATE_QR_CODE : {}", string); 389 | return string; 390 | } 391 | log.error("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR, message : {}, response : {}", content, execute); 392 | throw new WechatException("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR" + execute); 393 | 394 | } catch (Exception e) { 395 | log.error("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR, message : {}", content, e); 396 | throw new WechatException("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR" + e.getMessage()); 397 | } 398 | } 399 | 400 | 401 | /** 402 | * 创建二维码 403 | * 404 | * @param officialQrCodeCreate 405 | * @return 406 | */ 407 | public OfficialQrCode createQrCode(OfficialQrCodeCreate officialQrCodeCreate) { 408 | String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s"; 409 | JSONObject req = new JSONObject(); 410 | req.put("action_name", officialQrCodeCreate.getActionName().name()); 411 | JSONObject sceneStrObj = new JSONObject(); 412 | sceneStrObj.put("scene_str", officialQrCodeCreate.getScene()); 413 | JSONObject sceneObj = new JSONObject(); 414 | sceneObj.put("scene", sceneStrObj); 415 | req.put("action_info", sceneObj); 416 | String content = JSON.toJSONString(req); 417 | Request request = new Request.Builder() 418 | .addHeader("content-type", "application/json") 419 | .url(String.format(url, officialQrCodeCreate.getAccessToken())) 420 | .post(RequestBody.create(MediaType.parse("application/json"), content)) 421 | .build(); 422 | try { 423 | Response execute = okHttpClient.newCall(request).execute(); 424 | if (execute.isSuccessful()) { 425 | assert execute.body() != null; 426 | String string = execute.body().string(); 427 | log.info("WECHAT_OFFICIAL_CREATE_QR_CODE : {}", string); 428 | return JSON.parseObject(string, OfficialQrCode.class); 429 | } 430 | log.error("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR, message : {}, response : {}", content, execute); 431 | throw new WechatException("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR" + execute); 432 | 433 | } catch (Exception e) { 434 | log.error("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR, message : {}", content, e); 435 | throw new WechatException("WECHAT_OFFICIAL_CREATE_QR_CODE_ERROR" + e.getMessage()); 436 | } 437 | } 438 | 439 | /** 440 | * 获取永久链接 441 | * 442 | * @param officialMaterial 443 | * @return 444 | */ 445 | public String getMaterialUrl(OfficialMaterial officialMaterial) { 446 | String url = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=%s"; 447 | JSONObject req = new JSONObject(); 448 | req.put("media_id", officialMaterial.getMediaId()); 449 | String content = JSON.toJSONString(req); 450 | Request request = new Request.Builder() 451 | .addHeader("content-type", "application/json") 452 | .url(String.format(url, officialMaterial.getAccessToken())) 453 | .post(RequestBody.create(MediaType.parse("application/json"), content)) 454 | .build(); 455 | try { 456 | Response execute = okHttpClient.newCall(request).execute(); 457 | if (execute.isSuccessful()) { 458 | String string = execute.body().string(); 459 | JSONObject jsonObject = JSON.parseObject(string); 460 | return jsonObject.getJSONArray("news_item").getJSONObject(0).getString("url"); 461 | } 462 | log.error("WECHAT_OFFICIAL_GET_MATERIAL_URL_ERROR, message : {}, response : {}", content, execute); 463 | throw new WechatException("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR" + execute); 464 | 465 | } catch (Exception e) { 466 | log.error("WECHAT_OFFICIAL_GET_MATERIAL_URL_ERROR, message : {}", content, e); 467 | throw new WechatException("WECHAT_OFFICIAL_ADD_MATERIAL_ERROR" + e.getMessage()); 468 | } 469 | } 470 | } 471 | --------------------------------------------------------------------------------