微信公众号授权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 +
"×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;
}
}

浙公网安备 33010602011771号