準(zhǔn)備工作
1、在本地進(jìn)行聯(lián)調(diào)時(shí),為讓微信端能夠訪問(wèn)到本地服務(wù),需要進(jìn)行內(nèi)網(wǎng)穿透,參考《本地服務(wù)器內(nèi)網(wǎng)穿透實(shí)現(xiàn)(NATAPP)》
2、配置網(wǎng)頁(yè)授權(quán)獲取用戶(hù)基本信息
,用于告訴微信發(fā)起授權(quán)的后端服務(wù)器地址
- 正式公眾號(hào):在微信公眾號(hào)請(qǐng)求用戶(hù)網(wǎng)頁(yè)授權(quán)之前,開(kāi)發(fā)者需要先到公眾平臺(tái)官網(wǎng)中的“開(kāi)發(fā) - 接口權(quán)限 - 網(wǎng)頁(yè)服務(wù) - 網(wǎng)頁(yè)帳號(hào) - 網(wǎng)頁(yè)授權(quán)獲取用戶(hù)基本信息”進(jìn)行配置操作;
- 測(cè)試沙箱環(huán)境:在 測(cè)試環(huán)境 中,進(jìn)行配置網(wǎng)頁(yè)授權(quán)
授權(quán)說(shuō)明
微信授權(quán)時(shí),分為snsapi_base
和snsapi_userinfo
兩種授權(quán)方式
- snsapi_base: 用來(lái)獲取進(jìn)入頁(yè)面的用戶(hù)的 openid 的,并且是靜默授權(quán)并自動(dòng)跳轉(zhuǎn)到回調(diào)頁(yè)的。用戶(hù)感知的就是直接進(jìn)入了回調(diào)頁(yè)(往往是業(yè)務(wù)頁(yè)面,不會(huì)有對(duì)于的認(rèn)為操作);
- snsapi_userinfo: 是用來(lái)獲取用戶(hù)的基本信息的。但這種授權(quán)需要用戶(hù)手動(dòng)同意,并且由于用戶(hù)同意過(guò),所以無(wú)須關(guān)注,就可在授權(quán)后獲取該用戶(hù)的基本信息。
網(wǎng)頁(yè)授權(quán)流程最主要分三步:
1、引導(dǎo)用戶(hù)進(jìn)入授權(quán)頁(yè)面同意授權(quán),獲取code
2、通過(guò) code 換取網(wǎng)頁(yè)授權(quán)access_token(與基礎(chǔ)支持中的access_token不同)和openid
3、通過(guò)網(wǎng)頁(yè)授權(quán)access_token和 openid 獲取用戶(hù)基本信息(支持 UnionID 機(jī)制)
后端接口流程說(shuō)明:當(dāng)前Demo,后端只會(huì)對(duì)前端暴漏一個(gè)接口,微信授權(quán)回調(diào)地址直接重定向到后端地址,由后端進(jìn)行后續(xù)授權(quán)以及獲取用戶(hù)信息操作;
暴漏給前端的接口只需要傳入兩個(gè)參數(shù),分別為:socpe(授權(quán)類(lèi)型)、baseUrl(授權(quán)成功后重定向到前端的頁(yè)面地址)
比如:
1、前端調(diào)用`wechant/code`接口,傳入`socpe=snsapi_userinfo、baseUrl=http://lhz.com/h5`
2、微信回調(diào)的授權(quán)地址,直接為后端地址
3、獲取code、通過(guò)code獲取access_token的流程由后端完成
4、后端獲取到openid信息后,重定向到`http://lhz.com/h5`,比如`return "redirect:" + baseUrl + "?openid=" + openid;`
yaml配置
wx:
# 來(lái)源于測(cè)試平臺(tái)
appid: wx79ec4331f29311b9
secret: 1c79a199560f94096f26b8caa2a73a08
apiUrl: https://api.weixin.qq.com/
openApiUrl: https://open.weixin.qq.com/
authRedirectUri: http://6uks3d.natappfree.cc/wechat/auth
接口常量定義
InterfaceConstant:
public interface InterfaceConstant {
/**
* 用戶(hù)同意授權(quán),獲取code
*/
String OAUTH2_AUTHORIZE = "connect/oauth2/authorize";
/**
* 通過(guò) code 換取網(wǎng)頁(yè)授權(quán)access_token
*/
String OAUTH2_ACCESS_TOKEN = "sns/oauth2/access_token";
/**
* 獲取用戶(hù)信息
*/
String OAUTH2_USERINFO = "sns/userinfo";
}
定義工具類(lèi)
MapUtils:
public class MapUtils {
/**
* Map轉(zhuǎn)換為 Entity
*
* @param params 包含參數(shù)的Map
* @param t 需要賦值的實(shí)體
* @param <T> 類(lèi)型
*/
public static <T> T mapToEntity(Map<String, Object> params, T t) {
if (null == params) {
return t;
}
Class<?> clazz = t.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
try {
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
String name = declaredField.getName();
if (null != params.get(name)) {
declaredField.set(t, params.get(name));
}
}
} catch (Exception e) {
throw new RuntimeException("屬性設(shè)置失?。?);
}
return t;
}
/**
* 將對(duì)象轉(zhuǎn)換為HashMap
*
* @param t 轉(zhuǎn)換為Map的對(duì)象
* @param <T> 轉(zhuǎn)換為Map的類(lèi)
* @return Map
*/
public static <T> Map<String, Object> entityToMap(T t) {
Class<?> clazz = t.getClass();
List<Field> allField = getAllField(clazz);
Map<String, Object> hashMap = new LinkedHashMap<>(allField.size());
try {
for (Field declaredField : allField) {
declaredField.setAccessible(true);
Object o = declaredField.get(t);
if (null != o) {
hashMap.put(declaredField.getName(), o);
}
}
} catch (Exception e) {
throw new RuntimeException("屬性獲取失?。?);
}
return hashMap;
}
/**
* 獲取所有屬性
*
* @param clazz class
* @param <T> 泛型
* @return List<Field>
*/
public static <T> List<Field> getAllField(Class<T> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> superClazz = clazz;
while (null != superClazz) {
fields.addAll(Arrays.asList(superClazz.getDeclaredFields()));
superClazz = superClazz.getSuperclass();
}
return fields;
}
/**
* 將Map參數(shù)轉(zhuǎn)換為字符串
*
* @param map
* @return
*/
public static String mapToString(Map<String, Object> map) {
StringBuffer sb = new StringBuffer();
map.forEach((key, value) -> {
sb.append(key).append("=").append(value.toString()).append("&");
});
String str = sb.toString();
str = str.substring(0, str.length() - 1);
return str;
}
/**
* 將Bean對(duì)象轉(zhuǎn)換Url請(qǐng)求的字符串
*
* @param t
* @param <T>
* @return
*/
public static <T> String getUrlByBean(T t) {
String pre = "?";
Map<String, Object> map = entityToMap(t);
return pre + mapToString(map);
}
}
用戶(hù)授權(quán)獲取Code
接口說(shuō)明:
由于授權(quán)操作安全等級(jí)較高,所以在發(fā)起授權(quán)請(qǐng)求時(shí),微信會(huì)對(duì)授權(quán)鏈接做正則強(qiáng)匹配校驗(yàn),如果鏈接的參數(shù)順序不對(duì),授權(quán)頁(yè)面將無(wú)法正常訪問(wèn);鏈接屬性如下:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
重定向說(shuō)明:
通過(guò)微信接口
connect/oauth2/authorize
獲取授權(quán)code
后,微信將進(jìn)行一次重定向回調(diào),回調(diào)地址就是請(qǐng)求設(shè)置的redirect_uri
值;
需要注意的是,微信重定向回調(diào)地址,一定要在微信公眾號(hào)平臺(tái)進(jìn)行配置
定義請(qǐng)求實(shí)體類(lèi) Oauth2AuthorizeRep:
@Data
public class Oauth2AuthorizeRep {
/**
* 公眾號(hào)的唯一標(biāo)識(shí)
*/
private String appid;
/**
* 授權(quán)后重定向的回調(diào)鏈接地址, 請(qǐng)使用 urlEncode 對(duì)鏈接進(jìn)行處理
*/
private String redirect_uri;
/**
* 返回類(lèi)型,請(qǐng)?zhí)顚?xiě)code
*/
private String response_type = "code";
/**
* 應(yīng)用授權(quán)作用域,snsapi_base (不彈出授權(quán)頁(yè)面,直接跳轉(zhuǎn),只能獲取用戶(hù)openid)
* snsapi_userinfo (彈出授權(quán)頁(yè)面,可通過(guò) openid 拿到昵稱(chēng)、性別、所在地。并且, 即使在未關(guān)注的情況下,只要用戶(hù)授權(quán),也能獲取其信息 )
*/
private String scope;
/**
* 重定向后會(huì)帶上 state 參數(shù),開(kāi)發(fā)者可以填寫(xiě)a-zA-Z0-9的參數(shù)值,最多128字節(jié)
*/
private String state;
}
Controller層示例:
@Slf4j
@Api(tags = "公眾號(hào)/訂閱號(hào)開(kāi)發(fā)")
@Controller
public class WeChantController {
/**
* 由后端來(lái)進(jìn)行授權(quán)操作(需要在微信頁(yè)面打開(kāi))
*
* @param baseUrl 前端頁(yè)面地址 用于授權(quán)完成后,后端重定向到前端頁(yè)面
* @param scope 應(yīng)用授權(quán)作用域,此處為了模擬兩種情況,進(jìn)行傳值:
* snsapi_base (不彈出授權(quán)頁(yè)面,直接跳轉(zhuǎn),只能獲取用戶(hù)openid)
* snsapi_userinfo (彈出授權(quán)頁(yè)面,可通過(guò) openid 拿到昵稱(chēng)、性別、所在地。 即使在未關(guān)注的情況下,只要用戶(hù)授權(quán),也能獲取其信息 )
* @return
*/
@GetMapping(value = "/code")
@ApiOperation(value = "用戶(hù)請(qǐng)求進(jìn)行授權(quán)及獲取信息", notes = "用戶(hù)請(qǐng)求進(jìn)行授權(quán)及獲取信息")
public String code(@RequestParam("baseUrl") String baseUrl, String scope) throws UnsupportedEncodingException {
log.info("------ 用戶(hù)請(qǐng)求進(jìn)行授權(quán)及獲取信息 ------");
//通過(guò)code獲取用戶(hù)其信息
String url = weChantService.getAuthCode(baseUrl, scope);
return "redirect:" + url;
}
}
Service層示例:
@Service
@Slf4j
public class WeChantService {
/**
* 獲取用戶(hù)授權(quán)碼
*
* @param baseUrl
* @param scope
* @return
*/
public String getAuthCode(String baseUrl, String scope) throws UnsupportedEncodingException {
String appId = wxBean.getAppid();
// 設(shè)置回調(diào)地址 http://6uks3d.natappfree.cc/wechat/auth,該地址為后端地址
String redirectUri = wxBean.getAuthRedirectUri();
// urlEncode處理
redirectUri = URLEncoder.encode(redirectUri, "utf-8");
// 組裝url,在url中讓state屬性存方baseUrl的值
String url = wxBean.getOpenApiUrl() + InterfaceConstant.OAUTH2_AUTHORIZE;
// 封裝url請(qǐng)求參數(shù)
Oauth2AuthorizeRep rep = new Oauth2AuthorizeRep();
rep.setAppid(appId);
rep.setRedirect_uri(redirectUri);
rep.setScope(scope);
// 設(shè)置回調(diào)參數(shù),需要進(jìn)行urlEncode處理
Map<String, String> stateMap = new HashMap<>(4);
stateMap.put("baseUrl", baseUrl);
stateMap.put("scope", scope);
String stateMapStr = JSON.toJSONString(stateMap);
stateMapStr = new String(Base64.getEncoder().encode(stateMapStr.getBytes(StandardCharsets.UTF_8)));
rep.setState(stateMapStr);
// 參數(shù)的順序必須是:appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
url = url + MapUtils.getUrlByBean(rep) + "#wechat_redirect";
// 重定向url,微信會(huì)自動(dòng)訪問(wèn)redirectUri,進(jìn)行回調(diào)
return url;
}
}
請(qǐng)求示例:需要在微信中打開(kāi)以下鏈接(實(shí)際情況是前端在微信環(huán)境中進(jìn)行調(diào)用),這里進(jìn)行Demo演示,手動(dòng)在微信中進(jìn)行訪問(wèn):
http://6uks3d.natappfree.cc/wechat/code?baseUrl=https://www.baidu.com&scope=snsapi_base
其中http://6uks3d.natappfree.cc
為通過(guò)NatApp
配置的內(nèi)網(wǎng)穿透域名,保證微信功能訪問(wèn)到本地項(xiàng)目;
通過(guò)Code 換取授權(quán)access_token及用戶(hù)信息
接口說(shuō)明:
由于公眾號(hào)的 secret 和獲取到的access_token安全級(jí)別都非常高,必須只保存在服務(wù)器,不允許傳給客戶(hù)端。
該接口就是用戶(hù)授權(quán)獲取Code
中定義的redirectUri
值,微信重定向時(shí)會(huì)進(jìn)行調(diào)用
需要注意的是,微信重定向回調(diào)地址,一定要在微信公眾號(hào)平臺(tái)進(jìn)行配置
定義請(qǐng)求實(shí)體類(lèi) Oauth2AccessTokenRep:
@Data
public class Oauth2AccessTokenRep {
/**
* 公眾號(hào)的唯一標(biāo)識(shí)
*/
private String appid;
/**
* 公眾號(hào)的appsecret
*/
private String secret;
/**
* 填寫(xiě)第一步獲取的 code 參數(shù)
*/
private String code;
/**
* 填寫(xiě)為authorization_code
*/
private String grant_type = "authorization_code";
}
定義響應(yīng)實(shí)體類(lèi) Oauth2AccessTokenRes:
@Data
public class Oauth2AccessTokenRes {
/**
* 網(wǎng)頁(yè)授權(quán)接口調(diào)用憑證,注意:此access_token與基礎(chǔ)支持的access_token不同
*/
private String access_token;
/**
* access_token接口調(diào)用憑證超時(shí)時(shí)間,單位(秒)
*/
private Integer expires_in;
/**
* 用戶(hù)刷新access_token
*/
private String refresh_token;
/**
* 用戶(hù)唯一標(biāo)識(shí),請(qǐng)注意,在未關(guān)注公眾號(hào)時(shí),用戶(hù)訪問(wèn)公眾號(hào)的網(wǎng)頁(yè),也會(huì)產(chǎn)生一個(gè)用戶(hù)和公眾號(hào)唯一的OpenID
*/
private String openid;
/**
* 用戶(hù)授權(quán)的作用域,使用逗號(hào)(,)分隔
*/
private String scope;
/**
* 是否為快照頁(yè)模式虛擬賬號(hào),只有當(dāng)用戶(hù)是快照頁(yè)模式虛擬賬號(hào)時(shí)返回,值為1
*/
private String is_snapshotuser;
/**
* 用戶(hù)統(tǒng)一標(biāo)識(shí)(針對(duì)一個(gè)微信開(kāi)放平臺(tái)帳號(hào)下的應(yīng)用,同一用戶(hù)的 unionid 是唯一的),只有當(dāng) scope 為"snsapi_userinfo"時(shí)返回
* 并且公眾號(hào)與微信開(kāi)放平臺(tái)進(jìn)行了綁定才會(huì)返回
*/
private String unionid;
}
Controller層示例:
該接口接收微信的授權(quán)回調(diào),獲取
code及state
值,并且在完成授權(quán)后,重定向到指定的前端頁(yè)面
@Slf4j
@Controller
public class WeChantController {
/**
* @param code
* @param state 存放的前端頁(yè)面地址,授權(quán)后回調(diào)用
* @return
*/
@GetMapping(value = "/auth")
@ApiOperation(value = "前端根據(jù)code獲取信息", notes = "前端根據(jù)code獲取信息")
public String auth(@RequestParam(value = "code", required = false) String code, @RequestParam(value = "state", required = false) String state) throws UnsupportedEncodingException {
log.info("------ 回顯Code:{} ------", code);
// 解析回傳的 state值
state = new String(Base64.getDecoder().decode(state.getBytes(StandardCharsets.UTF_8)));
Map map = JSON.parseObject(state, Map.class);
String baseUrl = map.get("baseUrl").toString();
String scope = map.get("scope").toString();
// 通過(guò)code獲取用戶(hù)openid
String openid = weChantService.getUserAuth(code, scope);
// 直接跳轉(zhuǎn)到前端地址
return "redirect:" + baseUrl + "?openid=" + openid;
}
}
Service層示例:
@Service
@Slf4j
public class WeChantService {
/**
* 用戶(hù)授權(quán),并且返回openid返回給前端
*
* @param code
* @param scope
* @return
*/
public String getUserAuth(String code, String scope) {
String appId = wxBean.getAppid();
String secret = wxBean.getSecret();
String url = wxBean.getApiUrl() + InterfaceConstant.OAUTH2_ACCESS_TOKEN;
// 封裝url請(qǐng)求參數(shù)
Oauth2AccessTokenRep rep = new Oauth2AccessTokenRep();
rep.setAppid(appId);
rep.setSecret(secret);
rep.setCode(code);
url = url + MapUtils.getUrlByBean(rep);
Map map = restHttpRequest.doHttp(url, HttpMethod.GET, null);
if (map == null) {
throw new RuntimeException("授權(quán)失敗!");
}
Oauth2AccessTokenRes res = new Oauth2AccessTokenRes();
MapUtils.mapToEntity(map, res);
log.info("Oauth2AccessTokenRes:" + JSON.toJSONString(res));
// 獲取access_token過(guò)期時(shí)間
long expiresToken = res.getExpires_in() - 100;
String access_token = res.getAccess_token();
String openid = res.getOpenid();
log.info("------ access_token:{} ------", access_token);
log.info("------ openid:{} ------", openid);
// 根據(jù)openid和access_token獲取用戶(hù)信息,如果"snsapi_userinfo"授權(quán)方式,再調(diào)用接口獲取用戶(hù)信息
if (scope.equals(SNS_API_USERINFO)) {
getAndInsertUserInfo(openid, access_token);
}
return openid;
}
}
獲取用戶(hù)信息
定義請(qǐng)求實(shí)體類(lèi) Oauth2UserInfoRep:
@Data
public class Oauth2UserInfoRep {
/**
* 網(wǎng)頁(yè)授權(quán)接口調(diào)用憑證,注意:此access_token與基礎(chǔ)支持的access_token不同
*/
private String access_token;
/**
* 用戶(hù)的唯一標(biāo)識(shí)
*/
private String openid;
/**
* 返回國(guó)家地區(qū)語(yǔ)言版本,zh_CN 簡(jiǎn)體,zh_TW 繁體,en 英語(yǔ)
*/
private String lang = "zh_CN";
}
定義響應(yīng)實(shí)體類(lèi) Oauth2UserInfoRes:
@Data
public class Oauth2UserInfoRes {
/**
* 用戶(hù)昵稱(chēng)
*/
private String nickname;
/**
* 用戶(hù)的唯一標(biāo)識(shí)
*/
private String openid;
/**
* 用戶(hù)的性別,值為1時(shí)是男性,值為2時(shí)是女性,值為0時(shí)是未知
*/
private Integer sex;
/**
* 用戶(hù)個(gè)人資料填寫(xiě)的省份
*/
private String province;
/**
* 普通用戶(hù)個(gè)人資料填寫(xiě)的城市
*/
private String city;
/**
* 國(guó)家,如中國(guó)為CN
*/
private String country;
/**
* 用戶(hù)頭像,最后一個(gè)數(shù)值代表正方形頭像大?。ㄓ?、46、64、96、132數(shù)值可選,0代表640*640正方形頭像),
* 用戶(hù)沒(méi)有頭像時(shí)該項(xiàng)為空。若用戶(hù)更換頭像,原有頭像 URL 將失效。
*/
private String headimgurl;
/**
* 用戶(hù)特權(quán)信息,json 數(shù)組,如微信沃卡用戶(hù)為(chinaunicom)
*/
private List<String> privilege;
/**
* 只有在用戶(hù)將公眾號(hào)綁定到微信開(kāi)放平臺(tái)帳號(hào)后,才會(huì)出現(xiàn)該字段。
*/
private String unionid;
}
Service層示例:
該接口在后端通過(guò)
code
獲取accessToken
及openid
后,直接調(diào)用該方法;
@Service
@Slf4j
public class WeChantService {
private void getAndInsertUserInfo(String openid, String accessToken) {
// 獲取用戶(hù)信息
String url = wxBean.getApiUrl() + InterfaceConstant.OAUTH2_USERINFO;
Oauth2UserInfoRep rep = new Oauth2UserInfoRep();
rep.setAccess_token(accessToken);
rep.setOpenid(openid);
url = url + MapUtils.getUrlByBean(rep);
Map userMap = restHttpRequest.doHttp(url, HttpMethod.GET, null);
Oauth2UserInfoRes res = new Oauth2UserInfoRes();
MapUtils.mapToEntity(userMap, res);
// 打印信息
log.info("UserInfo:" + JSON.toJSONString(res));
}
}
流程測(cè)試
注意:由于使用了
NatApp
進(jìn)行免費(fèi)的內(nèi)網(wǎng)穿透,可以會(huì)出現(xiàn)域名變更情況,如果域名發(fā)生了變更需要重新在公眾號(hào)平臺(tái)配置網(wǎng)頁(yè)授權(quán)域名。
1、微信中訪問(wèn)后端接口,地址:http://6uks3d.natappfree.cc/wechat/code?baseUrl=https://www.baidu.com&scope=snsapi_userinfo文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-416500.html
2、彈出授權(quán)頁(yè)面如下
3、同意授權(quán)后,重定向到指定的頁(yè)面(測(cè)試時(shí)使用了百度頁(yè)面
)
4、查看百度頁(yè)面的鏈接信息,發(fā)現(xiàn)url后面帶上了openid,就表示授權(quán)及重定向成功,比如:https://www.baidu.com/?openid=oTnaY6332ssfv4WiQBU0dES-WxJg
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-416500.html
到了這里,關(guān)于微信公眾號(hào)開(kāi)發(fā)——實(shí)現(xiàn)用戶(hù)微信網(wǎng)頁(yè)授權(quán)流程的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!