微信公众平台开发实战Java版之如何网页授权获取用户基本信息
第一部分:微信授权获取基本信息的介绍
我们首先来看看官方的文档怎么说:
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
关于网页授权回调域名的说明
1、在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的开发者中心页配置授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加http://等协议头; 2、授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com无法进行OAuth2.0鉴权 3、如果公众号登录授权给了第三方开发者来进行管理,则不必做任何设置,由第三方代替公众号实现网页授权即可
关于网页授权的两种scope的区别说明
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面) 2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。 3、用户管理类接口中的“获取用户基本信息接口”,是在用户和公众号产生消息交互或关注后事件推送后,才能根据用户OpenID来获取用户基本信息。这个接口,包括其他微信接口,都是需要该用户(即openid)关注了公众号后,才能调用成功的。
关于网页授权access_token和普通access_token的区别
1、微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息; 2、其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。
关于UnionID机制
1、请注意,网页授权获取用户基本信息也遵循UnionID机制。即如果开发者有在多个公众号,或在公众号、移动应用之间统一用户帐号的需求,需要前往微信开放平台(open.weixin.qq.com)绑定公众号后,才可利用UnionID机制来满足上述需求。 2、UnionID机制的作用说明:如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的unionid来区分用户的唯一性,因为同一用户,对同一个微信开放平台下的不同应用(移动应用、网站应用和公众帐号),unionid是相同的。
关于特殊场景下的静默授权
1、上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知; 2、对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。
具体而言,网页授权流程分为四步:
1、引导用户进入授权页面同意授权,获取code 2、通过code换取网页授权access_token(与基础支持中的access_token不同) 3、如果需要,开发者可以刷新网页授权access_token,避免过期 4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)
第二部分:实现微信网页授权的详细方法
下面,我们来按照这个步骤来实现这个功能:
第一步:用户同意授权,获取code
在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面
参考链接(请在微信客户端中打开此链接体验)
Scope为snsapi_base
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx520c15f417810387&redirect_uri=https%3A%2F%2Fchong.qq.com%2Fphp%2Findex.php%3Fd%3D%26c%3DwxAdapter%26m%3DmobileDeal%26showwxpaytitle%3D1%26vb2ctag%3D4_2030_5_1194_60&response_type=code&scope=snsapi_base&state=123#wechat_redirect
Scope为snsapi_userinfo
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 公众号的唯一标识 |
redirect_uri | 是 | 授权后重定向的回调链接地址,请使用urlencode对链接进行处理 |
response_type | 是 | 返回类型,请填写code |
scope | 是 | 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息) |
state | 否 | 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节 |
#wechat_redirect | 是 | 无论直接打开还是做页面302重定向时候,必须带此参数 |
下图为scope等于snsapi_userinfo时的授权页面:
用户同意授权后
如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数redirect_uri?state=STATE
code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
温馨提醒:以下的省略了搭建环境和导入jar的过程,以下的方法提供参考。如果需要的话,需要看下前面的系列文章。
我们首先创建一些需要用到的pojo :
1. 通过网页授权获取的用户信息
用户信息类:SNSUserInfo类
package com.souvc.weixin.pojo; import java.util.List; /** * 类名: SNSUserInfo </br> * 描述: 通过网页授权获取的用户信息 </br> * 开发人员: fr </br> * 发布版本:V1.0 </br> */ public class SNSUserInfo { // 用户标识 private String openId; // 用户昵称 private String nickname; // 性别(1是男性,2是女性,0是未知) private int sex; // 国家 private String country; // 省份 private String province; // 城市 private String city; // 用户头像链接 private String headImgUrl; // 用户特权信息 private List<String> privilegeList; 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 int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getHeadImgUrl() { return headImgUrl; } public void setHeadImgUrl(String headImgUrl) { this.headImgUrl = headImgUrl; } public List<String> getPrivilegeList() { return privilegeList; } public void setPrivilegeList(List<String> privilegeList) { this.privilegeList = privilegeList; } }
2. 凭证实体类
/** * 类名: Token </br> * 描述: 凭证 </br> * 开发人员: fr</br> * 发布版本:V1.0 </br> */ public class Token { // 接口访问凭证 private String accessToken; // 凭证有效期,单位:秒 private int expiresIn; public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } }
3. 网页授权信息 WeixinOauth2Token类
/** * 类名: WeixinOauth2Token </br> * 描述: 网页授权信息 </br> * 开发人员: fr</br> * 发布版本:V1.0 </br> */ public class WeixinOauth2Token { // 网页授权接口调用凭证 private String accessToken; // 凭证有效时长 private int expiresIn; // 用于刷新凭证 private String refreshToken; // 用户标识 private String openId; // 用户授权作用域 private String scope; public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } public String getRefreshToken() { return refreshToken; } public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } 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; } }
4. 微信用户的基本信息WeixinUserInfo类
/** * 类名: WeixinUserInfo </br> * 描述: 微信用户的基本信息 </br> * 开发人员: fr</br> * 发布版本:V1.0 </br> */ public class WeixinUserInfo { // 用户的标识 private String openId; // 关注状态(1是关注,0是未关注),未关注时获取不到其余信息 private int subscribe; // 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间 private String subscribeTime; // 昵称 private String nickname; // 用户的性别(1是男性,2是女性,0是未知) private int sex; // 用户所在国家 private String country; // 用户所在省份 private String province; // 用户所在城市 private String city; // 用户的语言,简体中文为zh_CN private String language; // 用户头像 private String headImgUrl; public String getOpenId() { return openId; } public void setOpenId(String openId) { this.openId = openId; } public int getSubscribe() { return subscribe; } public void setSubscribe(int subscribe) { this.subscribe = subscribe; } public String getSubscribeTime() { return subscribeTime; } public void setSubscribeTime(String subscribeTime) { this.subscribeTime = subscribeTime; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public String getHeadImgUrl() { return headImgUrl; } public void setHeadImgUrl(String headImgUrl) { this.headImgUrl = headImgUrl; } }
5. 封装AdvancedUtil来实现以下方法 。
但是如何获取到token值呢?
/** * 获取网页授权凭证 * * @param appId 公众账号的唯一标识 * @param appSecret 公众账号的密钥 * @param code * @return WeixinAouth2Token */ public static WeixinOauth2Token getOauth2AccessToken(String appId, String appSecret, String code) { WeixinOauth2Token wat = null; // 拼接请求地址 String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code"; requestUrl = requestUrl.replace("APPID", appId); requestUrl = requestUrl.replace("SECRET", appSecret); requestUrl = requestUrl.replace("CODE", code); // 获取网页授权凭证 JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null); if (null != jsonObject) { try { wat = new WeixinOauth2Token(); wat.setAccessToken(jsonObject.getString("access_token")); wat.setExpiresIn(jsonObject.getInt("expires_in")); wat.setRefreshToken(jsonObject.getString("refresh_token")); wat.setOpenId(jsonObject.getString("openid")); wat.setScope(jsonObject.getString("scope")); } catch (Exception e) { wat = null; int errorCode = jsonObject.getInt("errcode"); String errorMsg = jsonObject.getString("errmsg"); log.error("获取网页授权凭证失败 errcode:{} errmsg:{}", errorCode, errorMsg); } } return wat; }
获取用户信息:
/** * 通过网页授权获取用户信息 * * @param accessToken 网页授权接口调用凭证 * @param openId 用户标识 * @return SNSUserInfo */ @SuppressWarnings( { "deprecation", "unchecked" }) public static SNSUserInfo getSNSUserInfo(String accessToken, String openId) { SNSUserInfo snsUserInfo = null; // 拼接请求地址 String requestUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"; requestUrl = requestUrl.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openId); // 通过网页授权获取用户信息 JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null); if (null != jsonObject) { try { snsUserInfo = new SNSUserInfo(); // 用户的标识 snsUserInfo.setOpenId(jsonObject.getString("openid")); // 昵称 snsUserInfo.setNickname(jsonObject.getString("nickname")); // 性别(1是男性,2是女性,0是未知) snsUserInfo.setSex(jsonObject.getInt("sex")); // 用户所在国家 snsUserInfo.setCountry(jsonObject.getString("country")); // 用户所在省份 snsUserInfo.setProvince(jsonObject.getString("province")); // 用户所在城市 snsUserInfo.setCity(jsonObject.getString("city")); // 用户头像 snsUserInfo.setHeadImgUrl(jsonObject.getString("headimgurl")); // 用户特权信息 snsUserInfo.setPrivilegeList(JSONArray.toList(jsonObject.getJSONArray("privilege"), List.class)); } catch (Exception e) { snsUserInfo = null; int errorCode = jsonObject.getInt("errcode"); String errorMsg = jsonObject.getString("errmsg"); log.error("获取用户信息失败 errcode:{} errmsg:{}", errorCode, errorMsg); } } return snsUserInfo; }
6. 封装https请求类 CommonUtil 类。
上面我们用到了一个支持发送https请求的工具:
import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; /** * 类名: MyX509TrustManager </br> * 描述:信任管理器 </br> * 开发人员: fr</br> * 发布版本:V1.0 </br> */ public class MyX509TrustManager implements X509TrustManager { // 检查客户端证书 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } // 检查服务器端证书 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } // 返回受信任的X509证书数组 public X509Certificate[] getAcceptedIssuers() { return null; } }
/** * 发送https请求 * * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */ public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) { JSONObject jsonObject = null; try { // 创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); // 从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactory ssf = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(ssf); conn.setDoOutput(true); conn.setDoInput(true); conn.setUseCaches(false); // 设置请求方式(GET/POST) conn.setRequestMethod(requestMethod); // 当outputStr不为null时向输出流写数据 if (null != outputStr) { OutputStream outputStream = conn.getOutputStream(); // 注意编码格式 outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } // 从输入流读取返回内容 InputStream inputStream = conn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; StringBuffer buffer = new StringBuffer(); while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } // 释放资源 bufferedReader.close(); inputStreamReader.close(); inputStream.close(); inputStream = null; conn.disconnect(); jsonObject = JSONObject.fromObject(buffer.toString()); } catch (ConnectException ce) { log.error("连接超时:{}", ce); } catch (Exception e) { log.error("https请求异常:{}", e); } return jsonObject; }
二、写授权类:
注意替换成自己的appid 和 密钥
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.souvc.weixin.pojo.SNSUserInfo; import com.souvc.weixin.pojo.WeixinOauth2Token; import com.souvc.weixin.util.AdvancedUtil; /** * 类名: OAuthServlet </br> * 描述: 授权后的回调请求处理 </br> * 开发人员: fr</br> * 发布版本:V1.0 </br> */ public class OAuthServlet extends HttpServlet { private static final long serialVersionUID = -1847238807216447030L; public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); // 用户同意授权后,能获取到code String code = request.getParameter("code"); String state = request.getParameter("state"); // 用户同意授权 if (!"authdeny".equals(code)) { // 获取网页授权access_token WeixinOauth2Token weixinOauth2Token = AdvancedUtil.getOauth2AccessToken("wx82b8dc4964d758fa", "951d92ccba3df8f8591f6550017858d4", code); // 网页授权接口访问凭证 String accessToken = weixinOauth2Token.getAccessToken(); // 用户标识 String openId = weixinOauth2Token.getOpenId(); // 获取用户信息 SNSUserInfo snsUserInfo = AdvancedUtil.getSNSUserInfo(accessToken, openId); // 设置要传递的参数 request.setAttribute("snsUserInfo", snsUserInfo); request.setAttribute("state", state); } // 跳转到index.jsp request.getRequestDispatcher("index.jsp").forward(request, response); } }
替换官方的链接成我们的方法路径:
官方的请求链接:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
需要修改的地方:
(1)替换自己的AppID
(2)将redirect_url换成自己的授权请求链接URL。注意这个连接需要经过UTF-8编码。
(3)需要修改scope。需要弹出页面则要修改为snsapi_userinfo 。
scope参数的解释:
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
一个小工具,编码URL用的:http://tool.chinaz.com/tools/urlencode.aspx
至此,授权后,你就可以显示用户信息拉,自己编写页面吧。
如果需要生成自己平台账号等操作,直接在获得openid后进行。