微信公衆號授權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;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章