赞
踩
1.在 pom.xml 中添加微信开发 SDK 依赖:
- <dependency>
- <groupId>com.github.binarywang</groupId>
- <artifactId>weixin-java-mp</artifactId>
- <version>3.5.0</version>
- </dependency>
2.创建一个处理微信消息的 Controller:
- @RestController
- @RequestMapping("/wx/portal")
- public class WxPortalController {
- private final WxMpService wxService;
- public WxPortalController(WxMpService wxService) {
- this.wxService = wxService;
- }
- @GetMapping(produces = "text/plain;charset=utf-8")
- public String authGet(@RequestParam(name = "signature", required = false) String signature,
- @RequestParam(name = "timestamp", required = false) String timestamp,
- @RequestParam(name = "nonce", required = false) String nonce,
- @RequestParam(name = "echostr", required = false) String echostr) {
- if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
- throw new IllegalArgumentException("请求参数非法,请核实!");
- }
- if (this.wxService.checkSignature(timestamp, nonce, signature)) {
- return echostr;
- }
- return "非法请求";
- }
- @PostMapping(produces = "application/xml; charset=UTF-8")
- public String post(@RequestBody String requestBody, @RequestParam("signature") String signature,
- @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce,
- @RequestParam(name = "encrypt_type", required = false) String encType,
- @RequestParam(name = "msg_signature", required = false) String msgSignature) {
- if (!this.wxService.checkSignature(timestamp, nonce, signature)) {
- throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
- }
- String out = null;
- if (encType == null) {
- // 明文传输的消息
- WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
- WxMpXmlOutMessage outMessage = this.route(inMessage);
- if (outMessage == null) {
- return "";
- }
- out = outMessage.toXml();
- } else if ("aes".equals(encType)) {
- // aes加密的消息
- WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, this.wxService.getWxMpConfigStorage(), timestamp, nonce, msgSignature);
- WxMpXmlOutMessage outMessage = this.route(inMessage);
- if (outMessage == null) {
- return "";
- }
- out = outMessage.toEncryptedXml(this.wxService.getWxMpConfigStorage());
- }
- return out;
- }
- private WxMpXmlOutMessage route(WxMpXmlMessage message) {
- try {
- return this.wxService.route(message);
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- return null;
- }
- }
在上述代码中,我们创建了一个 WxPortalController 控制器类,在其中添加了两个方法分别用于处理 GET 请求和 POST 请求。这些方法接受微信发送过来的参数,并使用 WxMpService 对象进行验证和处理。其中,WxMpService 是对微信公众号相关接口进行封装的类,我们可以通过依赖注入的方式获取它的实例。
3.在 application.yml 中配置微信相关的信息:
- wx:
- mp:
- app-id: your-app-id
- secret: your-app-secret
- token: your-token
- aes-key: your-aes-key
在上述配置中,我们使用了 wx.mp 作为微信公众号相关配置的前缀,并在下面分别设置了 AppID、AppSecret、Token 和 AES Key 的值。这些值可以在微信公众平台中获取到。
5.启动 Spring Boot 应用程序,并将接口地址填写到微信公众平台中的开发者中心中。
例如,假设你的应用程序运行在 8080 端口,接口地址为 /wx/portal,那么你就可以将 http://your-domain.com/wx/portal 填写到微信公众平台中的开发者中心的 URL 配置中。
微信网页授权流程主要包括以下几个步骤:
1.构造授权链接:生成一个跳转到微信授权页面的链接,用户点击后将跳转到微信授权页面。
2.用户授权:用户在微信授权页面上确认授权登录,并同意授权给你的应用获取用户信息。
3.获取授权 code:微信授权页面重定向回你指定的 redirect_uri 并携带授权 code。
4.通过 code 获取 access_token:使用授权 code 调用微信接口,获取 access_token 和 openid。
5.获取用户信息:使用 access_token 和 openid 调用微信接口,获取用户的基本信息。
下面是在 Spring 中实现微信网页授权流程的示例代码:
1.构造授权链接:
- String appId = "your_app_id";
- String redirectUri = "your_redirect_uri";
- String scope = "snsapi_userinfo";
- String state = "your_state_parameter"; // 可选,可以用于防止 CSRF 攻击
- String authorizeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appId + "&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8") + "&response_type=code&scope=" + scope + "&state=" + state + "#wechat_redirect";
- // 将 authorizeUrl 返回给前端,让用户点击该链接进行授权
2.获取授权 code:
- // 在 redirectUri 对应的 Controller 方法中获取 code
- @RequestMapping("/redirectUri")
- public String handleRedirectUri(@RequestParam("code") String code, @RequestParam("state") String state) {
- // 将 code 和 state 参数保存,用于后续获取 access_token
- // 然后重定向到获取用户信息的方法
- return "redirect:/getUserInfo?code=" + code + "&state=" + state;
- }
3.通过 code 获取 access_token:
- RestTemplate restTemplate = new RestTemplate();
- String appId = "your_app_id";
- String appSecret = "your_app_secret";
- String code = "the_code_from_previous_step";
- String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code";
- ResponseEntity<String> response = restTemplate.getForEntity(accessTokenUrl, String.class);
- String responseData = response.getBody();
- // 解析 JSON 格式的响应数据,获取 access_token 和 openid
- JSONObject jsonObject = new JSONObject(responseData);
- String accessToken = jsonObject.getString("access_token");
- String openid = jsonObject.getString("openid");
用户同意授权后,微信会重定向回你指定的 redirect_uri,并在URL中带上参数 code,该 code 是临时票据,用于换取 access_token 和用户的 OpenID。
也就是说,你需要使用这个 code 去请求微信的 API 来获取 access_token。具体来说,你可以通过调用如下 API 来完成 access_token 的获取:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
其中,APPID 是你的公众号 AppID,SECRET 是你的公众号 AppSecret,CODE 是前面得到的 code。该 API 将返回一个 JSON 格式的响应,包含了 access_token 等相关信息。
需要注意的是,获取 access_token 的接口与微信网页授权接口不同,如果你需要获取用户的基本信息,还需要调用其他的 API(如 https://api.weixin.qq.com/sns/userinfo)来获取用户的信息。
4.获取用户信息:
- String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openid + "&lang=zh_CN";
- ResponseEntity<String> response = restTemplate.getForEntity(userInfoUrl, String.class);
- String responseData = response.getBody();
- // 解析 JSON 格式的响应数据,获取用户信息
- JSONObject jsonObject = new JSONObject(responseData);
- String nickname = jsonObject.getString("nickname");
- String headImgUrl = jsonObject.getString("headimgurl");
- // 其他的用户信息字段...
- // 在这里可以根据需要处理用户信息
认证接口配置
- @Api("认证接口配置")
- @Slf4j
- @AllArgsConstructor
- @RestController
- @RequestMapping("/wx/portal/{appid}")
- public class WxPortalController {
- private final WxMpService wxService;
- private final WxMpMessageRouter messageRouter;
- @ApiOperation(value = "接收微信服务器的认证消息")
- @GetMapping(produces = "text/plain;charset=utf-8")
- public String authGet(@PathVariable String appid,
- @RequestParam(name = "signature", required = false) String signature,
- @RequestParam(name = "timestamp", required = false) String timestamp,
- @RequestParam(name = "nonce", required = false) String nonce,
- @RequestParam(name = "echostr", required = false) String echostr) {
- log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,
- timestamp, nonce, echostr);
- if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
- throw new IllegalArgumentException("请求参数非法,请核实!");
- }
- if (!this.wxService.switchover(appid)) {
- throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
- }
- if (wxService.checkSignature(timestamp, nonce, signature)) {
- return echostr;
- }
- return "非法请求";
- }
- @ApiOperation(value = "接收微信请求")
- @PostMapping(produces = "application/xml; charset=UTF-8")
- public String post(@PathVariable String appid,
- @RequestBody String requestBody,
- @RequestParam("signature") String signature,
- @RequestParam("timestamp") String timestamp,
- @RequestParam("nonce") String nonce,
- @RequestParam("openid") String openid,
- @RequestParam(name = "encrypt_type", required = false) String encType,
- @RequestParam(name = "msg_signature", required = false) String msgSignature) {
- log.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
- + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
- openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
-
- if (!this.wxService.switchover(appid)) {
- throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
- }
- if (!wxService.checkSignature(timestamp, nonce, signature)) {
- throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
- }
- String out = null;
- if (encType == null) {
- // 明文传输的消息
- WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
- WxMpXmlOutMessage outMessage = this.route(inMessage);
- if (outMessage == null) {
- return "";
- }
- out = outMessage.toXml();
- } else if ("aes".equalsIgnoreCase(encType)) {
- // aes加密的消息
- WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(),
- timestamp, nonce, msgSignature);
- log.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
- WxMpXmlOutMessage outMessage = this.route(inMessage);
- if (outMessage == null) {
- return "";
- }
- out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage());
- }
- log.debug("\n组装回复信息:{}", out);
- return out;
- }
- private WxMpXmlOutMessage route(WxMpXmlMessage message) {
- try {
- return this.messageRouter.route(message);
- } catch (Exception e) {
- log.error("路由消息时出现异常!", e);
- }
- return null;
- }
- }
新增配置文件application.yml
wx: mp: useRedis: true redisConfig: host: 127.0.0.1 port: 6379 #password: timeout: 2000 configs: - appId: #测试号appid secret: #测试号appsecret token: #测试号接口配置信息填写的token aesKey: 111 # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 port: 80 logging: level: org.springframework.web: info com.wckj.binarywang.demo.wx.pay: debug com.wckj.binarywang.wxpay: debug
读取配置信息
- @Data
- @ConfigurationProperties(prefix = "wx.mp")
- public class WxMpProperties {
- /**
- * 是否使用redis存储access token
- */
- private boolean useRedis;
-
- /**
- * redis 配置
- */
- private RedisConfig redisConfig;
- @Data
- public static class RedisConfig {
- /**
- * redis服务器 主机地址
- */
- private String host;
- /**
- * redis服务器 端口号
- */
- private Integer port;
- /**
- * redis服务器 密码
- */
- private String password;
- /**
- * redis 服务连接超时时间
- */
- private Integer timeout;
- }
- /**
- * 多个公众号配置信息
- */
- private List<MpConfig> configs;
- @Data
- public static class MpConfig {
- /**
- * 设置微信公众号的appid
- */
- private String appId;
- /**
- * 设置微信公众号的app secret
- */
- private String secret;
- /**
- * 设置微信公众号的token
- */
- private String token;
- /**
- * 设置微信公众号的EncodingAESKey
- */
- private String aesKey;
- }
- @Override
- public String toString() {
- return JsonUtils.toJson(this);
- }
- }
- @AllArgsConstructor
- @Configuration
- @EnableConfigurationProperties(WxMpProperties.class)
- public class WxMpConfiguration {
- private final LogHandler logHandler;
- private final NullHandler nullHandler;
- private final KfSessionHandler kfSessionHandler;
- private final StoreCheckNotifyHandler storeCheckNotifyHandler;
- private final LocationHandler locationHandler;
- private final MenuHandler menuHandler;
- private final MsgHandler msgHandler;
- private final UnsubscribeHandler unsubscribeHandler;
- private final SubscribeHandler subscribeHandler;
- private final ScanHandler scanHandler;
- private final WxMpProperties properties;
- @Bean
- public WxMpService wxMpService() {
- // 代码里 getConfigs()处报错的同学,请注意仔细阅读项目说明,你的IDE需要引入lombok插件!!!!
- final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
- if (configs == null) {
- throw new RuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
- }
- WxMpService service = new WxMpServiceImpl();
- service.setMultiConfigStorages(configs
- .stream().map(a -> {
- WxMpDefaultConfigImpl configStorage;
- if (this.properties.isUseRedis()) {
- final WxMpProperties.RedisConfig redisConfig = this.properties.getRedisConfig();
- JedisPoolConfig poolConfig = new JedisPoolConfig();
- JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
- redisConfig.getTimeout(), redisConfig.getPassword());
- configStorage = new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), a.getAppId());
- System.out.println("JedisPool连接成功:"+redisConfig.getHost()+"\t"+redisConfig.getPort());
- } else {
- configStorage = new WxMpDefaultConfigImpl();
- }
- configStorage.setAppId(a.getAppId());
- configStorage.setSecret(a.getSecret());
- configStorage.setToken(a.getToken());
- configStorage.setAesKey(a.getAesKey());
- return configStorage;
- }).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
- return service;
- }
- @Bean
- public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
- final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
- // 记录所有事件的日志 (异步执行)
- newRouter.rule().handler(this.logHandler).next();
- // 接收客服会话管理事件
- newRouter.rule().async(false).msgType(EVENT).event(KF_CREATE_SESSION)
- .handler(this.kfSessionHandler).end();
- newRouter.rule().async(false).msgType(EVENT).event(KF_CLOSE_SESSION)
- .handler(this.kfSessionHandler).end();
- newRouter.rule().async(false).msgType(EVENT).event(KF_SWITCH_SESSION)
- .handler(this.kfSessionHandler).end();
- // 门店审核事件newRouter.rule().async(false).msgType(EVENT).event(POI_CHECK_NOTIFY).handler(this.storeCheckNotifyHandler).end();
- // 自定义菜单事件
- newRouter.rule().async(false).msgType(EVENT).event(EventType.CLICK).handler(this.menuHandler).end();
- // 点击菜单连接事件
- newRouter.rule().async(false).msgType(EVENT).event(EventType.VIEW).handler(this.nullHandler).end();
- // 关注事件
- newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
- // 取消关注事件newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
- // 上报地理位置事件
- newRouter.rule().async(false).msgType(EVENT).event(EventType.LOCATION).handler(this.locationHandler).end();
- // 接收地理位置消息newRouter.rule().async(false).msgType(XmlMsgType.LOCATION).handler(this.locationHandler).end();
- // 扫码事件newRouter.rule().async(false).msgType(EVENT).event(EventType.SCAN).handler(this.scanHandler).end();
- // 默认
- newRouter.rule().async(false).handler(this.msgHandler).end();
- return newRouter;
- }
- }
Application启动类
- @SpringBootApplication
- public class WxMpDemoApplication {
- public static void main(String[] args) {
- SpringApplication.run(WxMpDemoApplication.class, args);
- }
- }
启动项目,前往微信公众平台,将接口配置信息填写完整,appid就是测试号的appid,token可以任意填写但要与后台配置文件的保持一致。
发送消息给指定用户
这里实现服务器配置中设置的服务器URL请求接口,验证配置的令牌是否生效。
- @Slf4j
- @RestController
- public class WeChatController {
- private static final String TOKEN = "cc2fb962b7bb37833008d65e905614c8";
- private static final String APPID="wxf1e42af3ecad28c7";
- private static final String APPSECRET="cc2fb962b7bb37833008d65e905614c8";
- /**
- * 微信验证token
- *
- * @param signature
- * @param timestamp
- * @param nonce
- * @param echostr
- * @return
- */
- @GetMapping("/checkTokenNotice.do")
- public String checkToken(@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp,
- @RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr) {
- //排序
- String[] arr = {TOKEN, timestamp, nonce};
- Arrays.sort(arr);
- StringBuilder content = new StringBuilder();
- for (int i = 0; i < arr.length; i++) {
- content.append(arr[i]);
- }
- //sha1Hex 加密
- MessageDigest md = null;
- String temp = null;
- try {
- md = MessageDigest.getInstance("SHA-1");
- byte[] digest = md.digest(content.toString().getBytes());
- temp = byteToStr(digest);
- log.info("加密后的token:" + temp);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
- if ((temp.toLowerCase()).equals(signature)) {
- return echostr;
- }
- return null;
- }
- private static String byteToStr(byte[] byteArray){
- String strDigest = "";
- for (int i = 0; i < byteArray.length; i++) {
- strDigest += byteToHexStr(byteArray[i]);
- }
- return strDigest;
- }
- private static String byteToHexStr(byte mByte){
- char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A','B', 'C', 'D', 'E', 'F' };
- char[] tempArr = new char[2];
- tempArr[0] = Digit[(mByte >>> 4)& 0X0F];
- tempArr[1] = Digit[mByte & 0X0F];
- String s = new String(tempArr);
- return s;
- }
- }
这个接口是微信请求用户授权地址,具体如何触发授权,这个需由具体业务需求而定,这里不做赘述。因为我只需要用到微信用户的openID,所以本文中用到的是静默授权登录(scope=snsapi_base),即不需要微信用户显式授权,不足之处是获取的用户信息较少。所以这里需要具体业务做选择。
- @Override
- public String weChatAuth(SendSmsParam req) {
- //这个url的域名必须要进行在公众号中进行注册验证,这个地址是成功后的回调地址
- String callBackUrl = "";
- if ("uat".equals(mark)) {
- callBackUrl = backUrl + "/uat/api/installment/vip/login/getOpenid.pub?mobile=" + req.getMobile();
- } else {
- callBackUrl = backUrl + "/api/installment/vip/login/getOpenid.pub?mobile=" + req.getMobile();
- }
- // 静默授权登录(scope=snsapi_base),一种为非静默授权登录(scope=snsapi_userinfo)
- String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appID + "&redirect_uri=" + URLEncoder.encode(callBackUrl) + "&response_type=code"
- + "&scope=snsapi_base" + "&state=STATE#wechat_redirect";
- log.info("forward重定向地址{" + url + "}");
- return "redirect:" + url;
- }
这个接口是微信授权成功后的回调地址,用户授权后,获得授权码,再请求获取openId接口,得到openId,进行进一步的业务操作。
- @Override
- public void getOpenidNotice(HttpServletRequest request, HttpServletResponse response) {
- String code = request.getParameter("code");
- String mobile = request.getParameter("mobile");
- log.info("手机号码[{}]", mobile);
- try {
- request.setCharacterEncoding("UTF-8");
- //通过获取access_token获得openid和access_token
- String backResult = HttpUtil.get("https://api.weixin.qq.com/sns/oauth2/access_token?appid="
- + appID + "&secret=" + appSecret + "&code=" + code +
- "&grant_type=authorization_code");
- //根据用户Access_token和openid获取用户信息
- log.info("用户openId[{}]", backResult);
- }
- }
调用消息发送接口发送微信消息到指定用户。
- public Boolean sendWeChatMsg(List<MessageRecordDTO> messageRecordList) {
- Boolean sendFlag = true;
- //从redis中获取accessToken
- String accessToken = redisCacheService.getMap(MAP_NAME, MAP_KEY);
- if (StrUtil.isBlank(accessToken)) {
- accessToken = this.getAndSetAccessToken();
- }
- for (MessageRecordDTO messageRecordDTO : messageRecordList) {
- UserPO userPO = userManager.getById(messageRecordDTO.getUserId());
- if (ObjectUtil.isNull(userPO)) {
- continue;
- }
- // 根据配置的消息模板构建消息体发送。
- Map<String, MsgElement> dataMap = getMsgElementMap(messageRecordDTO);
- String extend2 = userPO.getExtend2();
- if (StrUtil.isNotBlank(extend2)) {
- WeChatMsgDTO weChatMsgDTO = new WeChatMsgDTO(extend2, msgTemplateId, dataMap);
- String json = JSON.toJSONString(weChatMsgDTO);
- String result = "";
- try {
- result = HttpUtil.post("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken, json);
- } catch (Exception e) {
- log.error("发送微信消息失败", e);
- sendFlag = false;
- messageRecordDTO.setSendState("2");
- if (messageRecordDTO.getFirstFlag() == null) {
- DelayTaskScheduler.putWithDefaultStrategy(
- new SyncRetryWechatNoticeTask(), new SyncRetryWechatNoticeMessage(messageRecordDTO, weChatMsgService), null);
- }
- }
- log.info("消息记录[{}],微信消息发送结果[{}]", messageRecordDTO.getId(), result);
- // 发送成功后,可继续进行业务操作
- }
- }
- return sendFlag;
- }
- private Map<String, MsgElement> getMsgElementMap(MessageRecordDTO messageRecordDTO) {
- Map<String, MsgElement> elementMap = new HashMap<>();
- elementMap.put("first", new MsgElement("亲爱的用户,您有一条新的消息", "#000000"));
- elementMap.put("keyword1", new MsgElement(ENMessageTypeEnum.getLabelByValue(messageRecordDTO.getMsgType()), "#000000"));
- elementMap.put("keyword2", new MsgElement(this.getSendMsg(messageRecordDTO), "#000000"));
- elementMap.put("keyword3", new MsgElement(DateTimeUtil.getDateTimeFormat(messageRecordDTO.getCreateTime()), "#000000"));
- return elementMap;
- }
这里涉及到每次发送时,需要accessToken,微信设置的过期时间是2小时,我的处理方式是将accessToken存放到redis中,淘汰时间设置同微信官方过期时间,交由redis来维护accessToken。实现代码如下所示:
- private String getAndSetAccessToken() {
- String accessToken = "";
- String backResult = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appID + "&secret=" + appSecret);
- if (StrUtil.isNotBlank(backResult)) {
- WeChatTokenDTO weChatTokenDTO = JSON.parseObject(backResult, WeChatTokenDTO.class);
- accessToken = weChatTokenDTO.getAccess_token();
- redisCacheService.setMap(MAP_NAME, MAP_KEY, accessToken, weChatTokenDTO.getExpires_in());
- }
- return accessToken;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。