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

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;
    }
}

posted @ 2019-12-25 18:00  IT-小浣熊  阅读(196)  评论(0)    收藏  举报