文章目錄
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 +
"×tamp=" + 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;
}
}