我们在开发微信小程序时经常需要获取用户微信用户名以及头像信息,微信提供了专门的接口API用于返回这些信息,但是与接口获取接口需要经过许多验证步骤,现在记录如下。
只有通过微信平台验证的域名才能访问微信接口,微信公众号会发送请求到我们指定的URL,我们需要作出正确的响应才能通过验证。
登录微信测试号管理页面:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
如下所示,每个微信号会分配一个测试号,对应appID,其密码为appsecret
在接口配置信息中填写验证发往的URL,并约定好Token,这里点击提交会验证失败,因为我们还没有在服务器编写对请求的响应。。
微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示
参数 | 描述 |
---|---|
signature | 微信加密签名,结合了开发者填写的token和请求timestamp、nonce |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
服务器端的验证过程如下:
WxSignUtil.checkSignature()
方法中进行校验,若校验成功则原样返回echostr
@Controller
@RequestMapping("WxVerifier")
public class VerifyController {
private static Logger log = LoggerFactory.getLogger(VerifyController.class);
@RequestMapping(method = {RequestMethod.GET})
public void doGet(HttpServletRequest request, HttpServletResponse response) {
log.debug("weixin get...");
String signature = request.getParameter("signature"); // 加密签名
String timestamp = request.getParameter("timestamp"); // 时间戳
String nonce = request.getParameter("nonce"); // 随机数
String echostr = request.getParameter("echostr"); // 随机字符串
PrintWriter out = null;
try {
out = response.getWriter();
// 通过检验signature对请求进行校验
if (WxSignUtil.checkSignature(signature, timestamp, nonce)) {
log.debug("weixin get success....");
out.print(echostr); //校验成功则原样返回echostr
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null)
out.close();
}
}
}
如下实现校验方法WxSignUtil.checkSignature()
。对三个参数进行排序,加密然后比对,返回校验结果
public class WxSignUtil {
private static String token = "shopdemo"; //约定好的token
//验证签名
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[]{token, timestamp, nonce};
// 将token、timestamp、nonce三个参数按字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串与signature对比
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
//将字节数组转换为十六进制字符串
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
//将字节转换为十六进制字符串
private static String byteToHexStr(byte mByte) {
char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
将代码同步到服务器之后,在刚才的接口配置信息点击提交后就会显示配置成功。
在测试号管理页面中找到“体验接口权限表”,在其中找到网页服务->网页账号,点击后面的修改,填写授权回调页面域名。这里的域名可以是域名或者IP地址,例如www.qq.com、39.99.252.77,域名授权之后,域名下的子页面就可以访问微信API获取用户信息。
注意前面不要有“http://”等协议头,否则会提示"该链接无法访问,code -1"。
接下来获取用户信息,一共分为3步。
1、获取用户授权code
在微信或者微信开发者工具中访问如下网页链接,用户确认授权后会携带授权code
跳转到重定向页面http://39.99.152.77/ShopDemo/wechatlogin/check
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx691fee528cce2991&redirect_uri=http://39.99.152.77/ShopDemo/wechatlogin/check&role_type=1&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect
其中
2、通过code换取网页授权access_token
在重定向的地址中,根据请求链接中code
,并且结合appID
、appSecret
去访问微信的如下接口获取access_token
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
若成功将以json的格式返回结果,其字段如下:
参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证 |
expires_in | access_token接口调用凭证超时时间,单位(秒) |
refresh_token | 用户刷新access_token |
openid | 用户唯一标识 |
scope | 用户授权的作用域,使用逗号分隔 |
3、拉取用户信息
根据上一步拿到的access_token
和openid
,再次访问的如下微信接口从而获取到用户的具体信息
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
成功后将以JSON格式返回用户信息如下:
{
"openid":"o-i6Uswvm5012blk7EGOu6TlM",
"nickname":"Tory",
"sex":1,
"language":"zh_CN",
"city":"北京",
"province":"北京",
"country":"中国",
"headimgurl":"http://thirdwx.qlogo.cn/mmopen/vi_32/qe7I5uxq9dKEmSv88osAfAiblI61jmjLBIDicbicVEib53RPJTKoZQL7JuVNZdfjjcBZj9Z0c2VkA/132",
"privilege":[
]
}
上述步骤代码实现如下,即定义路由完成对重定向“/wechatlogin/check”请求的响应,获取到用户信息后再跳转到"front/index"页面。
在其中使用LoggerFactory
进行调试信息的输出,将请求到的token、用户信息保存到日志,输出结果在tomcat/logs/webapps/debug.log文件中
@Controller
@RequestMapping("wechatlogin")
public class LoginController {
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@RequestMapping("check")
public String doGet(HttpServletRequest request, HttpServletResponse response) {
log.debug("weixin login get...");
// 1、获取微信公众号传输过来的code
String code = request.getParameter("code");
log.debug("weixin login code:" + code);
WechatUser user = null;
String openId = null;
if (null != code) {
WXAccessToken token;
try {
// 2、通过code获取access_token
token = WechatUtil.getUserAccessToken(code);
log.debug("weixin login token:" + token.toString());
String accessToken = token.getAccessToken();
openId = token.getOpenId();
// 3、通过access_token和openId获取用户昵称等信息
user = WechatUtil.getUserInfo(accessToken, openId);
log.debug("weixin login user:" + user.toString());
request.getSession().setAttribute("openId", openId);
} catch (IOException e) {
log.error("error in getUserAccessToken or getUserInfo or findByOpenId: " + e.toString());
e.printStackTrace();
}
}
if (user != null) {
// 获取到微信用户信息后跳转到到指定的页面
return "front/index";
} else {
return null;
}
}
}
在上面的代码中用到了WechatUtil.getUserAccessToken()
方法来获取并构建AccessToken
对象,其实现方法非常经典,记录如下。
首先根据appId、appsecret、code拼接成获取Token的URL,然后通过httpsRequest()
向URL发送请求并获取到json格式的token。接下来通过ObjectMapper.readValue()
方法将json映射为WXAccess
对象并返回。
public static WXAccessToken getUserAccessToken(String code) throws IOException {
String appId = "wx691fee528cce2991";
String appsecret = "cb17a213ba780f1425da5aa7d157c2c";
// 拼接获取AccessToken的URL
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId + "&secret=" + appsecret
+ "&code=" + code + "&grant_type=authorization_code";
// 向相应URL发送请求获取token
String tokenStr = httpsRequest(url, "GET", null);
log.debug("userAccessToken:" + tokenStr);
WXAccessToken token = new WXAccessToken();
ObjectMapper objectMapper = new ObjectMapper();
try {
// 将json字符串映射为对象
token = objectMapper.readValue(tokenStr, WXAccessToken.class);
} catch (JsonParseException e) {
log.error("转换用户accessToken失败: " + e.getMessage());
e.printStackTrace();
}
if (token == null) {
log.error("获取用户accessToken失败。");
return null;
}
return token;
}
JSON并不是任意对象都能直接映射,需要在定义WXAcess类时通过@JsonProperty()
注解来标记属性与json字段之间的对应关系,这样json字符串才能正确映射为Java对象
public class WXAccessToken {
// 获取到的凭证
@JsonProperty("access_token")
private String accessToken;
// 凭证有效时间,单位:秒
@JsonProperty("expires_in")
private String expiresIn;
// 表示更新令牌,用来获取下一次的访问令牌,这里没太大用处
@JsonProperty("refresh_token")
private String refreshToken;
// 该用户在此公众号下的身份标识,对于此微信号具有唯一性
@JsonProperty("openid")
private String openId;
// 表示权限范围,这里可省略
@JsonProperty("scope")
private String scope;
//getter and setter...
}
WechatUtil.getUserInfo()
的实现与上面获取AccessToken对象的过程相同,在拿到token之后向指定URL发送请求获取JSON格式的用户信息,通过ObjectMapper
映射为WechatUser
对象。
如下所示为微信用户对象WechatUser
public class WechatUser implements Serializable {
// openId,标识该公众号下面的该用户的唯一Id
@JsonProperty("openid")
private String openId;
// 用户昵称
@JsonProperty("nickname")
private String nickName;
// 性别
@JsonProperty("sex")
private int sex;
// 省份
@JsonProperty("province")
private String province;
// 城市
@JsonProperty("city")
private String city;
// 区
@JsonProperty("country")
private String country;
// 头像图片地址
@JsonProperty("headimgurl")
private String headimgurl;
// 语言
@JsonProperty("language")
private String language;
// 用户权限,这里没什么作用
@JsonProperty("privilege")
private String[] privilege;
//...getter and setter
}
如下所示为getUserInfo()
方法,通过访问接口获取json字符,然后通过ObjectMapper映射为WechatUser
对象并返回。这样我们就获取到了微信用户对象。
public static WechatUser getUserInfo(String accessToken, String openId) {
// 根据传入的accessToken以及openId拼接出访问微信定义的端口并获取用户信息的URL
String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openId
+ "&lang=zh_CN";
// 访问该URL获取用户信息json 字符串
String userStr = httpsRequest(url, "GET", null);
log.debug("user info :" + userStr);
WechatUser user = new WechatUser();
ObjectMapper objectMapper = new ObjectMapper();
try {
// 将json字符串转换成相应对象
user = objectMapper.readValue(userStr, WechatUser.class);
} catch (IOException e) {
log.error("获取用户信息失败: " + e.getMessage());
e.printStackTrace();
}
if (user == null) {
log.error("获取用户信息失败。");
return null;
}
return user;
}
在获取token和用户信息时都用到了httpsRequest()
发送,其实现如下所示,利用SSLContext
加密的HttpsURLConnection
来发送https请求
/**
* 发起https请求并获取结果
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return 返回的json字符串
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
StringBuffer buffer = new StringBuffer();
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] trustManagers = {new MyX509TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, trustManagers, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
// 设置请求方式(GET/POST)
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
// 当有数据需要提交时
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式,防止中文乱码
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
log.debug("https buffer:" + buffer.toString());
} catch (ConnectException ce) {
log.error("Weixin server connection timed out.");
} catch (Exception e) {
log.error("https request error:{}", e);
}
//最后将获取到的数据返回
return buffer.toString();
}
其中用到了自定义的证书信任管理器MyX509TrustManager
,只是简单实现了X509TrustManager
接口
public class MyX509TrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}