微信公共號開發教程java版——微信網頁授權(八)
一:微信網頁授權介紹
關於網頁授權回撥域名的說明
1、在微信公眾號請求使用者網頁授權之前,開發者需要先到公眾平臺官網中的“開發 - 介面許可權 - 網頁服務 - 網頁帳號 - 網頁授權獲取使用者基本資訊”的配置選項中,修改授權回撥域名。請注意,這裡填寫的是域名(是一個字串),而不是URL,因此請勿加 http:// 等協議頭;
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機制)
二:網頁授權的實現
1.使用者同意授權,獲取code
在確保微信公眾賬號擁有授權作用域(scope引數)的許可權的前提下(服務號獲得高階介面後,預設擁有scope引數中的snsapi_base和snsapi_userinfo),引導關注者開啟如下頁面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“該連結無法訪問”,請檢查引數是否填寫錯誤,是否擁有scope引數對應的授權作用域許可權。
尤其注意:由於授權操作安全等級較高,所以在發起授權請求時,微信會對授權連結做正則強匹配校驗,如果連結的引數順序不對,授權頁面將無法正常訪問
參考連結(請在微信客戶端中開啟此連結體驗):
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
尤其注意:跳轉回調redirect_uri,應當使用https連結來確保授權code的安全性。
引數說明
引數 | 是否必須 | 說明 |
---|---|---|
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說明 : code作為換取access_token的票據,每次使用者授權帶上的code將不一樣,code只能使用一次,5分鐘未被使用自動過期。
錯誤返回碼說明如下:
返回碼 | 說明 |
---|---|
10003 | redirect_uri域名與後臺配置不一致 |
10004 | 此公眾號被封禁 |
10005 | 此公眾號並沒有這些scope的許可權 |
10006 | 必須關注此測試號 |
10009 | 操作太頻繁了,請稍後重試 |
10010 | scope不能為空 |
10011 | redirect_uri不能為空 |
10012 | appid不能為空 |
10013 | state不能為空 |
10015 | 公眾號未授權第三方平臺,請檢查授權狀態 |
10016 | 不支援微信開放平臺的Appid,請使用公眾號Appid |
2. 通過網頁授權獲取的使用者資訊
使用者資訊類:SNSUserInfo類
package com.wyj.wechart.pojo;
import java.util.List;
/**
* 通過網頁授權獲取的使用者資訊
*
*
* @author:WangYuanJun
* @date:2018年1月24日 下午3:09:02
*/
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;
}
}
3.憑證實體類
package com.wyj.wechart.pojo;
/**
*
* 憑證
*
* @author:WangYuanJun
* @date:2018年1月23日 下午3:19:14
*/
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;
}
}
4.網頁授權資訊 WeixinOauth2Token類
package com.wyj.wechart.pojo;
/**
* 網頁授權資訊
*
*
* @author:WangYuanJun
* @date:2018年1月24日 下午3:10:03
*/
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;
}
}
5.微信使用者的基本資訊WeixinUserInfo類
package com.wyj.wechart.pojo;
/**
* 微信使用者的基本資訊
*
*
* @author:WangYuanJun
* @date:2018年1月24日 上午10:05:42
*/
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;
}
}
6.獲取網頁授權憑證及獲取使用者資訊
package com.wyj.wechart.utils;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.wyj.wechart.pojo.SNSUserInfo;
import com.wyj.wechart.pojo.WeixinOauth2Token;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
/**
*
*
*
* @author:WangYuanJun
* @date:2018年1月24日 下午3:19:44
*/
public class AdvancedUtil {
private static Logger log = LoggerFactory.getLogger(AdvancedUtil.class);
/**
* 獲取網頁授權憑證
*
* @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;
}
}
7.封裝https請求類 CommonUtil 類
https請求的工具
package com.wyj.wechart.utils;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
/**
* 證書信任管理器(用於https請求)
* 這個證書管理器的作用就是讓它信任我們指定的證書,下面的程式碼意味著信任所有證書,不管是否權威機構頒發。
*
* @author:WangYuanJun
* @date:2018年1月23日 下午3:22:19
*/
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;
}
8.寫授權類:
替換成自己的appid 和 金鑰
package com.wyj.wechart.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.wyj.wechart.pojo.SNSUserInfo;
import com.wyj.wechart.pojo.WeixinOauth2Token;
import com.wyj.wechart.utils.AdvancedUtil;
/**
* 授權後的回撥請求處理
*
*
* @author:WangYuanJun
* @date:2018年1月27日 下午5:31:09
*/
@Controller
@RequestMapping("/oauth")
public class OAuthController {
@RequestMapping
public ModelAndView index(String code,String state){
ModelAndView mv = new ModelAndView("/index");
// 使用者同意授權
if (!"authdeny".equals(code)) {
// 獲取網頁授權access_token
WeixinOauth2Token weixinOauth2Token = AdvancedUtil.getOauth2AccessToken("wx17fdedc3d6d0b68e", "c3b3d919d65a781ba7db58d9d8dfb515", code);
// 網頁授權介面訪問憑證
String accessToken = weixinOauth2Token.getAccessToken();
// 使用者標識
String openId = weixinOauth2Token.getOpenId();
// 獲取使用者資訊
SNSUserInfo snsUserInfo = AdvancedUtil.getSNSUserInfo(accessToken, openId);
// 設定要傳遞的引數
mv.addObject("snsUserInfo", snsUserInfo);
mv.addObject("state", state);
}
return mv;
}
}
9.授權後,顯示資訊的頁面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OAuth2.0網頁授權</title>
</head>
<body>
<#if snsUserInfo??>
<table width="100%" cellspacing="0" cellpadding="0">
<tr><td width="20%">屬性</td><td width="80%">值</td></tr>
<tr><td>OpenID</td><td>${snsUserInfo.openId}</td></tr>
<tr><td>暱稱</td><td>${snsUserInfo.nickname}</td></tr>
<tr><td>性別</td><td>${snsUserInfo.sex}</td></tr>
<tr><td>國家</td><td>${snsUserInfo.country}</td></tr>
<tr><td>省份</td><td>${snsUserInfo.province}</td></tr>
<tr><td>城市</td><td>${snsUserInfo.city}</td></tr>
<tr><td>頭像</td><td>${snsUserInfo.headImgUrl}</td></tr>
<!-- <tr><td>特權</td><td>${snsUserInfo.privilegeList}</td></tr> -->
<tr><td>state:</td><td>${state}</td></tr>
</table>
<#else>
<p>使用者不同意授權,未獲取到使用者資訊!</p>
</#if>
</body>
</html>
10.application.properties配置
server.port=80
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.request-context-attribute=request
spring.freemarker.template-loader-path=classpath:/templates
spring.freemarker.suffix=.html
spring.mvc.static-path-pattern=/static/**
11.替換官方的連結成我們的方法路徑:
官方的請求連結:
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轉碼
/**
* URL編碼(utf-8)
*
* @param source
* @return
*/
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
package com.wyj.wechart.test;
import com.wyj.wechart.utils.CommonUtil;
/**
* URL轉碼
*
*
* @author:WangYuanJun
* @date:2018年1月27日 下午5:35:02
*/
public class TransCodeUrlTest {
/**
* 生成URL編碼
*
* @param args
*/
public static void main(String[] args) {
String source = "http://6400cc45.ngrok.io/oauth";
System.out.println(CommonUtil.urlEncodeUTF8(source));
}
}
12.修改網頁授權獲取使用者基本資訊
微信公共平臺->測試號管理->體驗介面許可權表->網頁服務->網頁帳號->修改
修改完成後需要重新關注
13.測試效果:
複製上面替換好的連結,然後丟進瀏覽器,然後用微信來掃一掃。