微信公众号授权java实现问题记录

mac2024-03-25  43

文章目录

1. 关于网页授权access_token和普通access_token的区别2. 获取用户基础信息3. 微信公众号授权java代码实现微信公众号用户基本信息VO微信公众号AccessTokenVO微信公众号授权代码 4. 获取微信配置信息controller签名处理 5. 获取微信公众号全局唯一接口调用凭据 检查 刷新 封装类

1. 关于网页授权access_token和普通access_token的区别

1、微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token), 通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息; 2、其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。

微信公众号的全局唯一接口调用凭据access_token https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

通过code换取网页授权access_token https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

2. 获取用户基础信息

获取用户基本信息有两种方式: 一种通过全局唯一接口调用凭据access_token,调用接口https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 可以获取; 另一种通过获取code,换取的网页授权access_token,调用接口https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 获取。 两种获取方式获取的信息可参考官方文档:

通过全局唯一接口调用凭据access_token获取 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId 通过获取的code,换取网页授权access_token获取 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#3

3. 微信公众号授权java代码实现

记录本次微信公众号授权的java代码实现

微信公众号用户基本信息VO
import java.io.Serializable; import java.util.List; /** * 微信公众号用户基本信息VO * @author xnz * @date 2019/10/23 10:00 */ public class WeChatUserInfoVO implements Serializable { //用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。 private Integer subscribe; //用户的标识,对当前公众号唯一 private String openid; //用户的昵称 private String nickname; //用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 private Integer sex; //用户的语言,简体中文为zh_CN private String language; //用户所在城市 private String city; //用户所在省份 private String province; //用户所在国家 private String country; //用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 private String headimgurl; //用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间 private Long subscribe_time; //只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 private String unionid; //公众号运营者对粉丝的备注,公众号运营者可在微信公众平台用户管理界面对粉丝添加备注 private String remark; //用户所在的分组ID(兼容旧的用户分组接口) private Integer groupid; //用户被打上的标签ID列表 private List<Integer> tagid_list; //返回用户关注的渠道来源,ADD_SCENE_SEARCH 公众号搜索,ADD_SCENE_ACCOUNT_MIGRATION 公众号迁移,ADD_SCENE_PROFILE_CARD 名片分享,ADD_SCENE_QR_CODE 扫描二维码,ADD_SCENE_PROFILE_ LINK 图文页内名称点击,ADD_SCENE_PROFILE_ITEM 图文页右上角菜单,ADD_SCENE_PAID 支付后关注,ADD_SCENE_OTHERS 其他 private String subscribe_scene; //二维码扫码场景(开发者自定义) private Long qr_scene; //二维码扫码场景描述(开发者自定义) private String qr_scene_str; private Integer errcode; private String errmsg; public Integer getSubscribe() { return subscribe; } public void setSubscribe(Integer subscribe) { this.subscribe = subscribe; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getHeadimgurl() { return headimgurl; } public void setHeadimgurl(String headimgurl) { this.headimgurl = headimgurl; } public Long getSubscribe_time() { return subscribe_time; } public void setSubscribe_time(Long subscribe_time) { this.subscribe_time = subscribe_time; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public Integer getGroupid() { return groupid; } public void setGroupid(Integer groupid) { this.groupid = groupid; } public List<Integer> getTagid_list() { return tagid_list; } public void setTagid_list(List<Integer> tagid_list) { this.tagid_list = tagid_list; } public String getSubscribe_scene() { return subscribe_scene; } public void setSubscribe_scene(String subscribe_scene) { this.subscribe_scene = subscribe_scene; } public Long getQr_scene() { return qr_scene; } public void setQr_scene(Long qr_scene) { this.qr_scene = qr_scene; } public String getQr_scene_str() { return qr_scene_str; } public void setQr_scene_str(String qr_scene_str) { this.qr_scene_str = qr_scene_str; } public Integer getErrcode() { return errcode; } public void setErrcode(Integer errcode) { this.errcode = errcode; } public String getErrmsg() { return errmsg; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } }
微信公众号AccessTokenVO
import java.io.Serializable; /** * 微信公众号AccessTokenVO * @author xnz * @date 2019/10/22 17:39 */ public class WeChatAccessTokenVO implements Serializable { private String access_token; private Integer expires_in; private String refresh_token; private String openid; private String scope; private Integer errcode; private String errmsg; public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public Integer getExpires_in() { return expires_in; } public void setExpires_in(Integer expires_in) { this.expires_in = expires_in; } public String getRefresh_token() { return refresh_token; } public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public Integer getErrcode() { return errcode; } public void setErrcode(Integer errcode) { this.errcode = errcode; } public String getErrmsg() { return errmsg; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } }
微信公众号授权代码
/** * 微信公众号 * @author xnz */ @Controller @RequestMapping("/weChatOfficialAccount") public class WeChatOAController extends BaseController { private Logger logger = LoggerFactory.getLogger(WeChatOAController.class); @Value("${wechat.officialAccount.authAccessTokenUrl}") private String authAccessTokenUrl; // https://api.weixin.qq.com/sns/oauth2/access_token @Value("${wechat.officialAccount.accessTokenUrl}") private String accessTokenUrl; //https://api.weixin.qq.com/cgi-bin/token @Value("${wechat.officialAccount.config.appid}") private String appid; @Value("${wechat.officialAccount.config.secret}") private String secret; @Value("${wechat.officialAccount.config.redirect_uri}") private String redirect_uri; // localhost:8989/anjhyj-api/weChatOfficialAccount/oa/auth 获取完code后回调地址 @Autowired private UserService userService; @Resource(name = "redisTemplate") private RedisTemplate<String,String> redisTemplate; /** * 用户同意授权,获取code * https://open.weixin.qq.com/connect/oauth2/authorize? * appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect * 若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。 * @param pageUrl 授权成功回调的地址 * @return */ @RequestMapping(value = "/oa/login", method = RequestMethod.GET) @ResponseBody public ModelAndView oaLogin(String pageUrl) { logger.info("==pageUrl== " + pageUrl); ModelAndView model = null; try { model = new ModelAndView(); if(StringUtils.isBlank(pageUrl)){ model.setStatus(HttpStatus.PRECONDITION_FAILED); return model; } URIBuilder url = new URIBuilder("https://open.weixin.qq.com/connect/oauth2/authorize"); url.setParameter("appid", appid); //授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理 // if(StringUtils.isEmpty(pageUrl)) { // url.setParameter("redirect_uri", redirect_uri); // }else{ url.setParameter("redirect_uri", redirect_uri + "?pageUrl=" + pageUrl); // } url.setParameter("response_type", "code"); //应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 ) url.setParameter("scope", "snsapi_userinfo"); //重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节 url.setParameter("state", "STATE"); //无论直接打开还是做页面302重定向时候,必须带此参数 #wechat_redirect System.out.println("=====拼接的地址url====== " + "redirect:" + url.toString() + "&connect_redirect=1#wechat_redirect"); model.setViewName("redirect:" + url.toString() + "&connect_redirect=1#wechat_redirect"); } catch (URISyntaxException e) { e.printStackTrace(); } return model; } /** * 微信公众号认证 * @param code * @param pageUrl 授权成功回调的地址 * @return */ @RequestMapping("/oa/auth") @ResponseBody public ModelAndView oaAuth(@RequestParam("code") String code,String pageUrl){ logger.info("==== code:" + code); ModelAndView model = null; try { model = new ModelAndView(); if(StringUtils.isAnyEmpty(code,pageUrl)){ model.setStatus(HttpStatus.PRECONDITION_FAILED); return model; } // 通过Code获取用户认证accessToken String authTokenResponse = HttpUtils.doGet(new URI(authAccessTokenUrl+String.format("?appid=%s&secret=%s&code=%s&grant_type=authorization_code",appid,secret,code))); WeChatAccessTokenVO weChatAccessTokenVO = JSONObject.toJavaObject(JSONObject.parseObject(authTokenResponse), WeChatAccessTokenVO.class); if(weChatAccessTokenVO.getErrcode() == null) { //查询数据库中当前用户 UserDTO dto = userService.queryByAccount(weChatAccessTokenVO.getOpenid()); // 获取用户信息 String accessToken = weChatAccessTokenVO.getAccess_token(); String userInfoResponse = HttpUtils.doGet(new URI("https://api.weixin.qq.com/sns/userinfo"+String.format("?access_token=%s&openid=%s&lang=zh_CN",accessToken,weChatAccessTokenVO.getOpenid()))); WeChatUserInfoVO weChatUserInfoVO = JSONObject.toJavaObject(JSONObject.parseObject(userInfoResponse), WeChatUserInfoVO.class); if(accessToken != null && weChatUserInfoVO !=null && weChatUserInfoVO.getErrcode() == null) { logger.info("微信公众号获取用户信息成功,用户信息:" + JSONObject.toJSONString(weChatUserInfoVO)); if(dto != null) { // 更新信息 checkUserStatus(dto.getStatus()); dto = userService.doUpdateUserInfo(weChatUserInfoVO.getOpenid(), null, weChatUserInfoVO.getNickname(), weChatUserInfoVO.getSex(), null, null, null, null,null); } else { //新建用户 dto = new UserDTO(); dto.setPassword(String.format("%s-%s", new Object[]{"weChatOfficialAccount", MemberType.WECHAT_MEMBER.toChannel()})); dto.setAccount(weChatAccessTokenVO.getOpenid()); dto.setAccountType(MemberType.WECHAT_MEMBER.toValue()); dto.setAppId(appid); dto.setUnionId(weChatUserInfoVO.getUnionid()); dto.setSex(weChatUserInfoVO.getSex()); dto.setName("微信公众号用户"); dto.setNickName(weChatUserInfoVO.getNickname()); userService.doBatRegister(dto); } } else { logger.info("微信公众号获取用户信息失败"); if(dto == null) { dto = new UserDTO(); dto.setPassword(String.format("%s-%s", new Object[]{"weChatOfficialAccount", MemberType.WECHAT_MEMBER.toChannel()})); dto.setName("微信公众号用户"); dto.setAccount(weChatAccessTokenVO.getOpenid()); dto.setAccountType(MemberType.WECHAT_MEMBER.toValue()); dto.setAppId(appid); dto.setSex(SexType.UNKNOW.toValue()); userService.doBatRegister(dto); } } String redirectUrl = ""; pageUrl = URLDecoder.decode(pageUrl, "utf-8"); // logger.info("pageUrl1= "+pageUrl); pageUrl = new String(java.util.Base64.getDecoder().decode(pageUrl)); // logger.info("pageUrl2= "+pageUrl); if (pageUrl.indexOf("?") == -1) { redirectUrl = pageUrl + "?headimgurl="+weChatUserInfoVO.getHeadimgurl() ; } else { redirectUrl = pageUrl + "&headimgurl="+weChatUserInfoVO.getHeadimgurl() ; } model.setViewName("redirect:" + redirectUrl); logger.info("redirectUrl="+redirectUrl); return model; } else { logger.info(String.format("微信公众号授权失败,%s", authTokenResponse)); throwApiExp(weChatAccessTokenVO.getErrcode(), "微信公众号登录失败-" + weChatAccessTokenVO.getErrmsg()); return null; } } catch (Exception e) { logger.info("微信公众号授权异常",e); } return null; } }

4. 获取微信配置信息

controller
/** * 微信公众号 * @author xnz */ @Controller @RequestMapping("/weChatOfficialAccount") public class WeChatOAController extends BaseController { private Logger logger = LoggerFactory.getLogger(WeChatOAController.class); @Value("${wechat.officialAccount.accessTokenUrl}") private String accessTokenUrl; @Value("${wechat.officialAccount.config.appid}") private String appid; @Value("${wechat.officialAccount.config.secret}") private String secret; public String jsapi_ticket_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"; @Autowired private UserService userService; @Resource(name = "redisTemplate") private RedisTemplate<String,String> redisTemplate; @Log @ApiOperation("获取Config信息") @PostMapping("/oa/getWxConfig") public Map<String, Object> getWxConfig(String url) { logger.info("===获取Config信息 参数 url==== " + url); String access_token = globalAccessTokenCheckRefresh(); String jsapi_ticket = getTicket(access_token); String nonce_str = WechatSign.create_nonce_str(); String timestamp = WechatSign.create_timestamp(); Map<String, String> ret = WechatSign.sign(jsapi_ticket,url,nonce_str,timestamp); Map map = new HashMap(); map.put("appId",appid); map.put("timestamp",timestamp); map.put("nonceStr",nonce_str); map.put("signature",ret.get("signature")); System.out.println("==map==== " + map); return ResponseBuilder.build(map); } /** * 检查 刷新 微信公众号全局AccessToken * @return */ public String globalAccessTokenCheckRefresh(){ try { String globalAccessTokenKey = "WECHAT:GLOBALACCESSTOKEN"; // 判断当前用户的微信公众号全局唯一accessToken是否存在于redis String globalAccessToken = redisTemplate.opsForValue().get("globalAccessTokenKey"); long globalAccessTokenExpire = redisTemplate.getExpire(globalAccessTokenKey); if(globalAccessToken != null && globalAccessTokenExpire >= 5*60){ return globalAccessToken; } // 重新获取公众号的全局唯一接口调用凭据accessToken String accessTokenResponse = HttpUtils.doGet(new URI(accessTokenUrl + String.format("?grant_type=client_credential&appid=%s&secret=%s",appid,secret))); WeChatAccessTokenVO accessTokenVO = JSONObject.toJavaObject(JSONObject.parseObject(accessTokenResponse), WeChatAccessTokenVO.class); if(accessTokenVO == null || accessTokenVO.getErrcode() != null) { logger.info(accessTokenResponse); return null; } //将新获取的accessToken存入redis,缓存有效期是微信返回的有效期 redisTemplate.opsForValue().set(globalAccessTokenKey,accessTokenVO.getAccess_token(),accessTokenVO.getExpires_in(), TimeUnit.SECONDS); return accessTokenVO.getAccess_token(); } catch (Exception e) { logger.info("检查刷新微信公众号全局AccessToken异常",e); } return null; } private String getTicket(String access_token){ String jsapi_ticket = ""; if (redisTemplate.hasKey("jhyj:jsapi_ticket")){ jsapi_ticket = redisTemplate.opsForValue().get("jhyj:jsapi_ticket"); }else{ String result = HttpUtil.post(String.format(jsapi_ticket_url, access_token), new HashMap<>()); int errcode = JSONObject.parseObject(result).getInteger("errcode"); if (errcode == 0){ jsapi_ticket = JSONObject.parseObject(result).getString("ticket"); redisTemplate.opsForValue().set("jhyj:jsapi_ticket",jsapi_ticket,7000, TimeUnit.SECONDS); } } return jsapi_ticket; } }
签名处理
public class WechatSign { public static Map<String, String> sign(String jsapi_ticket, String url,String nonce_str,String timestamp) { Map<String, String> ret = new HashMap<String, String>(); String string1; String signature = ""; //注意这里参数名必须全部小写,且必须有序 string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "&timestamp=" + timestamp + "&url=" + url; System.out.println(string1); try{ MessageDigest crypt = MessageDigest.getInstance("SHA-1"); crypt.reset(); crypt.update(string1.getBytes("UTF-8")); signature = byteToHex(crypt.digest()); }catch (NoSuchAlgorithmException e) { e.printStackTrace(); }catch (UnsupportedEncodingException e) { e.printStackTrace(); } ret.put("url", url); ret.put("jsapi_ticket", jsapi_ticket); ret.put("nonceStr", nonce_str); ret.put("timestamp", timestamp); ret.put("signature", signature); return ret; } public static void main(String[] args) { Formatter formatter = new Formatter(); byte b = 'a'; formatter.format("|%12x|", b); System.out.println(formatter.toString()); formatter.format("|%02x|",0x5); String result = formatter.toString(); System.out.println(result); formatter.close(); } public static String byteToHex(final byte[] hash) { Formatter formatter = new Formatter(); for (byte b : hash) { formatter.format("%02x", b); } String result = formatter.toString(); formatter.close(); return result; } public static String create_nonce_str() { return UUID.randomUUID().toString(); } public static String create_timestamp() { return Long.toString(System.currentTimeMillis() / 1000); } }

5. 获取微信公众号全局唯一接口调用凭据 检查 刷新 封装类

/** * 获取微信公众号全局唯一接口调用凭据 检查 刷新 封装类 * @author xnz * @date 2019/10/23 11:26 */ public class WeChatOAGlobalTokenCheckRefresh { private static Logger logger = LoggerFactory.getLogger(WeChatOAGlobalTokenCheckRefresh.class); @Value("${wechat.officialAccount.accessTokenUrl}") private String accessTokenUrl; //https://api.weixin.qq.com/cgi-bin/token @Value("${wechat.officialAccount.config.appid}") private String appid; @Value("${wechat.officialAccount.config.secret}") private String secret; @Resource(name = "redisTemplate") private RedisTemplate<String,String> redisTemplate; /** * 检查 刷新 微信公众号全局AccessToken * @return */ public String globalAccessTokenCheckRefresh(){ try { String globalAccessTokenKey = "WECHAT:GLOBALACCESSTOKEN"; // 判断当前用户的微信公众号全局唯一accessToken是否存在于redis String globalAccessToken = redisTemplate.opsForValue().get("globalAccessTokenKey"); long globalAccessTokenExpire = redisTemplate.getExpire(globalAccessTokenKey); if(globalAccessToken != null && globalAccessTokenExpire >= 5*60){ return globalAccessToken; } // 重新获取公众号的全局唯一接口调用凭据accessToken String accessTokenResponse = HttpUtils.doGet(new URI(accessTokenUrl+String.format("?grant_type=client_credential&appid=%s&secret=%s",appid,secret))); WeChatAccessTokenVO accessTokenVO = JSONObject.toJavaObject(JSONObject.parseObject(accessTokenResponse), WeChatAccessTokenVO.class); if(accessTokenVO == null || accessTokenVO.getErrcode() != null) { logger.info(accessTokenResponse); return null; } //将新获取的accessToken存入redis,缓存有效期是微信返回的有效期 redisTemplate.opsForValue().set(globalAccessTokenKey,accessTokenVO.getAccess_token(),accessTokenVO.getExpires_in(), TimeUnit.SECONDS); return accessTokenVO.getAccess_token(); } catch (Exception e) { logger.info("检查刷新微信公众号全局AccessToken异常",e); } return null; } }
最新回复(0)