目录
- 准备工作
- 授权说明
- yaml配置
- 接口常量定义
- 定义工具类
- 用户授权获取Code
- 通过Code 换取授权access_token及用户信息
- 获取用户信息
- 流程测试
准备工作
1、在本地进行联调时,为让微信端能够访问到本地服务,需要进行内网穿透,参考《本地服务器内网穿透实现(NATAPP)》
2、配置网页授权获取用户基本信息
,用于告诉微信发起授权的后端服务器地址
- 正式公众号:在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”进行配置操作;
- 测试沙箱环境:在 测试环境 中,进行配置网页授权
授权说明
微信授权时,分为snsapi_base
和snsapi_userinfo
两种授权方式
- snsapi_base: 用来获取进入页面的用户的 openid 的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面,不会有对于的认为操作);
- snsapi_userinfo: 是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
网页授权流程最主要分三步:
1、引导用户进入授权页面同意授权,获取code
2、通过 code 换取网页授权access_token(与基础支持中的access_token不同)和openid
3、通过网页授权access_token和 openid 获取用户基本信息(支持 UnionID 机制)
后端接口流程说明:
当前Demo,后端只会对前端暴漏一个接口,微信授权回调地址直接重定向到后端地址,由后端进行后续授权以及获取用户信息操作;
暴漏给前端的接口只需要传入两个参数,分别为:socpe(授权类型)、baseUrl(授权成功后重定向到前端的页面地址)
比如:
1、前端调用`wechant/code`接口,传入`socpe=snsapi_userinfo、baseUrl=http://lhz.com/h5`
2、微信回调的授权地址,直接为后端地址
3、获取code、通过code获取access_token的流程由后端完成
4、后端获取到openid信息后,重定向到`http://lhz.com/h5`,比如`return "redirect:" + baseUrl + "?openid=" + openid;`
yaml配置
wx:
# 来源于测试平台
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 {
/**
* 用户同意授权,获取code
*/
String OAUTH2_AUTHORIZE = "connect/oauth2/authorize";
/**
* 通过 code 换取网页授权access_token
*/
String OAUTH2_ACCESS_TOKEN = "sns/oauth2/access_token";
/**
* 获取用户信息
*/
String OAUTH2_USERINFO = "sns/userinfo";
}
定义工具类
MapUtils:
public class MapUtils {
/**
* Map转换为 Entity
*
* @param params 包含参数的Map
* @param t 需要赋值的实体
* @param <T> 类型
*/
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("属性设置失败!");
}
return t;
}
/**
* 将对象转换为HashMap
*
* @param t 转换为Map的对象
* @param <T> 转换为Map的类
* @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参数转换为字符串
*
* @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对象转换Url请求的字符串
*
* @param t
* @param <T>
* @return
*/
public static <T> String getUrlByBean(T t) {
String pre = "?";
Map<String, Object> map = entityToMap(t);
return pre + mapToString(map);
}
}
用户授权获取Code
接口说明:
由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问;链接属性如下:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
重定向说明:
通过微信接口
connect/oauth2/authorize
获取授权code
后,微信将进行一次重定向回调,回调地址就是请求设置的redirect_uri
值;
需要注意的是,微信重定向回调地址,一定要在微信公众号平台进行配置
定义请求实体类 Oauth2AuthorizeRep:
@Data
public class Oauth2AuthorizeRep {
/**
* 公众号的唯一标识
*/
private String appid;
/**
* 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
*/
private String redirect_uri;
/**
* 返回类型,请填写code
*/
private String response_type = "code";
/**
* 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid)
* snsapi_userinfo (弹出授权页面,可通过 openid 拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )
*/
private String scope;
/**
* 重定向后会带上 state 参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
*/
private String state;
}
Controller层示例:
@Slf4j
@Api(tags = "公众号/订阅号开发")
@Controller
public class WeChantController {
/**
* 由后端来进行授权操作(需要在微信页面打开)
*
* @param baseUrl 前端页面地址 用于授权完成后,后端重定向到前端页面
* @param scope 应用授权作用域,此处为了模拟两种情况,进行传值:
* snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid)
* snsapi_userinfo (弹出授权页面,可通过 openid 拿到昵称、性别、所在地。 即使在未关注的情况下,只要用户授权,也能获取其信息 )
* @return
*/
@GetMapping(value = "/code")
@ApiOperation(value = "用户请求进行授权及获取信息", notes = "用户请求进行授权及获取信息")
public String code(@RequestParam("baseUrl") String baseUrl, String scope) throws UnsupportedEncodingException {
log.info("------ 用户请求进行授权及获取信息 ------");
//通过code获取用户其信息
String url = weChantService.getAuthCode(baseUrl, scope);
return "redirect:" + url;
}
}
Service层示例:
@Service
@Slf4j
public class WeChantService {
/**
* 获取用户授权码
*
* @param baseUrl
* @param scope
* @return
*/
public String getAuthCode(String baseUrl, String scope) throws UnsupportedEncodingException {
String appId = wxBean.getAppid();
// 设置回调地址 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请求参数
Oauth2AuthorizeRep rep = new Oauth2AuthorizeRep();
rep.setAppid(appId);
rep.setRedirect_uri(redirectUri);
rep.setScope(scope);
// 设置回调参数,需要进行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);
// 参数的顺序必须是:appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
url = url + MapUtils.getUrlByBean(rep) + "#wechat_redirect";
// 重定向url,微信会自动访问redirectUri,进行回调
return url;
}
}
请求示例:
需要在微信中打开以下链接(实际情况是前端在微信环境中进行调用),这里进行Demo演示,手动在微信中进行访问:
http://6uks3d.natappfree.cc/wechat/code?baseUrl=https://www.baidu.com&scope=snsapi_base
其中http://6uks3d.natappfree.cc
为通过NatApp
配置的内网穿透域名,保证微信功能访问到本地项目;
通过Code 换取授权access_token及用户信息
接口说明:
由于公众号的 secret 和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。
该接口就是用户授权获取Code
中定义的redirectUri
值,微信重定向时会进行调用
需要注意的是,微信重定向回调地址,一定要在微信公众号平台进行配置
定义请求实体类 Oauth2AccessTokenRep:
@Data
public class Oauth2AccessTokenRep {
/**
* 公众号的唯一标识
*/
private String appid;
/**
* 公众号的appsecret
*/
private String secret;
/**
* 填写第一步获取的 code 参数
*/
private String code;
/**
* 填写为authorization_code
*/
private String grant_type = "authorization_code";
}
定义响应实体类 Oauth2AccessTokenRes:
@Data
public class Oauth2AccessTokenRes {
/**
* 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
*/
private String access_token;
/**
* access_token接口调用凭证超时时间,单位(秒)
*/
private Integer expires_in;
/**
* 用户刷新access_token
*/
private String refresh_token;
/**
* 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
*/
private String openid;
/**
* 用户授权的作用域,使用逗号(,)分隔
*/
private String scope;
/**
* 是否为快照页模式虚拟账号,只有当用户是快照页模式虚拟账号时返回,值为1
*/
private String is_snapshotuser;
/**
* 用户统一标识(针对一个微信开放平台帐号下的应用,同一用户的 unionid 是唯一的),只有当 scope 为"snsapi_userinfo"时返回
* 并且公众号与微信开放平台进行了绑定才会返回
*/
private String unionid;
}
Controller层示例:
该接口接收微信的授权回调,获取
code及state
值,并且在完成授权后,重定向到指定的前端页面
@Slf4j
@Controller
public class WeChantController {
/**
* @param code
* @param state 存放的前端页面地址,授权后回调用
* @return
*/
@GetMapping(value = "/auth")
@ApiOperation(value = "前端根据code获取信息", notes = "前端根据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();
// 通过code获取用户openid
String openid = weChantService.getUserAuth(code, scope);
// 直接跳转到前端地址
return "redirect:" + baseUrl + "?openid=" + openid;
}
}
Service层示例:
@Service
@Slf4j
public class WeChantService {
/**
* 用户授权,并且返回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请求参数
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("授权失败!");
}
Oauth2AccessTokenRes res = new Oauth2AccessTokenRes();
MapUtils.mapToEntity(map, res);
log.info("Oauth2AccessTokenRes:" + JSON.toJSONString(res));
// 获取access_token过期时间
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);
// 根据openid和access_token获取用户信息,如果"snsapi_userinfo"授权方式,再调用接口获取用户信息
if (scope.equals(SNS_API_USERINFO)) {
getAndInsertUserInfo(openid, access_token);
}
return openid;
}
}
获取用户信息
定义请求实体类 Oauth2UserInfoRep:
@Data
public class Oauth2UserInfoRep {
/**
* 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
*/
private String access_token;
/**
* 用户的唯一标识
*/
private String openid;
/**
* 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
*/
private String lang = "zh_CN";
}
定义响应实体类 Oauth2UserInfoRes:
@Data
public class Oauth2UserInfoRes {
/**
* 用户昵称
*/
private String nickname;
/**
* 用户的唯一标识
*/
private String openid;
/**
* 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
*/
private Integer sex;
/**
* 用户个人资料填写的省份
*/
private String province;
/**
* 普通用户个人资料填写的城市
*/
private String city;
/**
* 国家,如中国为CN
*/
private String country;
/**
* 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),
* 用户没有头像时该项为空。若用户更换头像,原有头像 URL 将失效。
*/
private String headimgurl;
/**
* 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
*/
private List<String> privilege;
/**
* 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
*/
private String unionid;
}
Service层示例:
该接口在后端通过
code
获取accessToken
及openid
后,直接调用该方法;
@Service
@Slf4j
public class WeChantService {
private void getAndInsertUserInfo(String openid, String accessToken) {
// 获取用户信息
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));
}
}
流程测试
注意:由于使用了
NatApp
进行免费的内网穿透,可以会出现域名变更情况,如果域名发生了变更需要重新在公众号平台配置网页授权域名。
1、微信中访问后端接口,地址:http://6uks3d.natappfree.cc/wechat/code?baseUrl=https://www.baidu.com&scope=snsapi_userinfo
2、弹出授权页面如下
3、同意授权后,重定向到指定的页面(测试时使用了百度页面
)
4、查看百度页面的链接信息,发现url后面带上了openid,就表示授权及重定向成功,比如:https://www.baidu.com/?openid=oTnaY6332ssfv4WiQBU0dES-WxJg