微信小程序-登录操作

        之前没事写了一个微信小程序玩,刚起步就碰壁,因为要获取用户授权,然后请求用户的个人信息,由于微信官方api的更改,wx.getUserInfo()方法无法在无授权的情况下直接使用,而且只能获取到基本的一些微信用户的信息,不包含我们需要的openId以及unionId。几经折腾,才把这个第一步给迈过去,来记录一下。

小程序-登录操作

1. 登录流程

        想要获取用户的登录信息,首先要先知道小程序的登录流程是什么,下面是官方给出的流程图。

登录流程图

1.1 第一步:获取code

1、小程序调用wx.login() 获取临时登录凭证code,并回传到开发者服务器。

2、开发者服务器以code换取用户唯一标识openid会话密钥session_key

3、之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

        关于unionId,这里需要说明一下,如果应用只限于小程序内则不需要unionId,直接通过openId可以确定用户身份,但是如果需要跨应用,如:网页应用,app应用时则需要使用到unionId作为身份标识。

UnionID获取途径:绑定了开发者帐号的小程序,可以通过下面3种途径获取UnionID。

1、调用接口wx.getUserInfo,从解密数据中获取UnionID。注意本接口需要用户授权,请开发者妥善处理用户拒绝授权后的情况。

2、如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。开发者可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。

3、如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。开发者也可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。

1.2 第二步:通过code换取个人信息

        当前台获得了用户的授权后,我们就可以获得用户的个人信息以及unionId。

前台接口:**wx.getUserInfo(Object)**。

注意:此接口现在经果调整之后,使用该接口将不再出现授权弹窗,需要使用<button open-type="getUserInfo"></button>引导用户主动进行授权操作。详情见、查看官方文档

Object参数说明:

参数名 类型 必填 说明 最低版本
withCredentials Boolean 是否带上登录信息 1.1.0
lang String 指定返回用户信息的语言,zh_CN:简体中文,zh_TW:繁体中文,en:英文。默认en 1.3.0
timeout Number 超时时间,单位ms 1.9.90
success Function 接口调用成功的回调函数
fail Function 接口调用失败的回调函数
complete Function 接口调用结束的回调函数(调用成功、失败都会执行)

注:当 withCredentials 为 true 时,要求此前有调用过 wx.login 且登录态尚未过期,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登录态,返回的数据不包含 encryptedData, iv 等敏感信息。

success返回参数说明:

参数 类型 说明
userInfo Object 用户信息对象,不包括openid等敏感信息
rawData String 不包括敏感信息的原始数据字符串,用于计算签名
signature String 使用sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息。
encryptedData String 包括敏感数据在内的完整用户信息的加密数据,详细见[用户数据的签名验证和加解密
iv String 加密算法的初始向量,详见 用户数据的签名验证和加解密

2. 代码解析

2.1 微信端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//index.js

// 自定义的登录方法,判断用户登录,是否授权
login: function (e) {
var that = this
wx.login({
success: function (r) {
//获取登录凭证
var code = r.code;
if (code) {
// 获取用户设置信息
wx.getSetting({
success: function (re) {
// 判断用户是否授权
if (re.authSetting['scope.userInfo']) {
// 已经授权了,就进行后台用户数据写入
that.register(code)
} else {
//未授权,跳转到登录页面
wx.redirectTo({
url:'XXX/XXX',
})
}
}
})
} else {
console.log("获取用户登录状态失败!" + r.errMsg)
}
},
fail: function () {
console.log("登录失败")
}
})
},

// 获取到用户的登录授权,请求后台,进行用户信息的操作
register: function (code) {
//2.调用获取用户信息接口
wx.getUserInfo({
success: function (res) {
//3.请求自己的服务器,解密用户信息,获取unionld等加密信息
wx.request({
url: 'XXX/login.do', //自己后台服务器接口地址
method: 'POST', //请求方式
// 请求头消息
header: {
'content-type': 'application/x-www-form-urlencoded'
},
// 请求接口时传的参数
data: {
encryptedData: res.encryptedData, //加密数据
iv: res.iv, //加密算法的初使向量
code: code //登录凭证
},
success: function (data) {
//4.解密成功后,获取自己服务器返回的结果
if (data.data.status == 1) {
// 解密成功之后更换登录状态
app.globalData.checkLogin = true
// 接收请求数据
var userInfo_ = data.data.userInfo;
// 设置用户信息
app.globalData.userInfo = userInfo_;
app.globalData.openId = userInfo_.openId
console.log("用户信息:", userInfo_);

//由于这里是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (app.checkLoginReadyCallback) {
app.checkLoginReadyCallback(data);
}
} else {
console.log("解密失败")
}
},
fail: function () {
console.log("系统错误")
}
})
},
fail: function () {
console.log("获取用户信息失败")
}
})
},

2.2 服务器端Java代码

控制层:WXLoginController .java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package controller;

import mapper.UserMapper;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import util.HttpRequest; //自定义的网络请求的工具类(见下)
import util.RegisterUser; //自定义的注册的工具类
import util.AesCbcUtil; //自定义的AES解密工具类(见下)

import java.util.HashMap;
import java.util.Map;

/**
* @Author:范秉洋
* @Date:2019/8/25 17:00
*/
@Controller
public class WXLoginController {

@Autowired
private UserMapper userMapper;

@RequestMapping(value = "/login.do")
@ResponseBody
public Map decodeUserInfo(String encryptedData,String iv,String code){
Map map = new HashMap();

//登录凭证不能为空
if(code == null || code.length() == 0)
{
map.put("status",0);
map.put("msg","code不能为空");
return map;
}

//小程序唯一标识(在微信小程序管理后台获取)
String wxspAppid = "XXXXXXXXX";
//小程序的app secret(在微信小程序管理后台获取)
String wxspSecret = "XXXXXXXXX";
//授权(必填)
String grant_type = "authorization_code";

//********1.向微信服务器使用登录凭证code获取session_key和openid****************//
//拼接请求参数
String params = "appid="+wxspAppid + "&secret="+wxspSecret
+ "&js_code="+code + "&grant_type="+grant_type;
//请求的URL
String url = "https://api.weixin.qq.com/sns/jscode2session";
//通过请求工具类发送请求
String sr = HttpRequest.sendGet(url,params);

//解析相应内容(转换成json对象)
JSONObject json = new JSONObject(sr);

//获取会话密钥(session_key)
String session_key = json.get("session_key").toString();

//用户的唯一标识(openid)
String openid = (String)json.get("openid");

//*********2.对encryptedData加密数据进行AES解密***************************//
try{
// 根据加密数据,加密算法初始向量和session_key(密钥)通过AES解密工具类进行解密
String result = AesCbcUtil.decrypt(encryptedData,session_key,iv,"UTF-8");
// 如果解密的结果不为空或者长度大于0,则解密成功
if(null != result && result.length() > 0)
{
map.put("status",1);
map.put("msg","解密成功");
// 将解密结果转换成json格式
JSONObject userInfoJSON = new JSONObject(result);
Map userInfo = new HashMap();
//用户openId
userInfo.put("openId",userInfoJSON.get("openId"));
//用户昵称
userInfo.put("nickName",userInfoJSON.get("nickName"));
//用户性别,0:未知,1:男;2:女。
userInfo.put("gender",userInfoJSON.get("gender"));
//用户所在城市
userInfo.put("city",userInfoJSON.get("city"));
//用户所在省份
userInfo.put("province",userInfoJSON.get("province"));
//用户所在国家
userInfo.put("country",userInfoJSON.get("country"));
//用户头像地址
userInfo.put("avatarUrl",userInfoJSON.get("avatarUrl"));


//注册验证,如果是首次登录,将信息通过注册工具类写入数据库,如果不是,则进行信息更新
//这个就不在给出具体代码,根据个人的具体情况进行将用户信息封装好写入数据库
RegisterUser.Register(userMapper,userInfoJSON);

//解密unionId & openId
//这个信息是只给符合条件的用户下发,如不符合,则没有这个数据,
//在调用时需要做相应的判断,否则直接取值会报错,
if(!userInfoJSON.isNull("unionId")){
userInfo.put("unionID",userInfoJSON.get("unionId"));
}
map.put("userInfo",userInfo);

}else{
map.put("status",0);
map.put("msg","解密失败");
}
}catch (Exception e){
e.printStackTrace();
}
//将结果返回给微信端
return map;
}
}

网络请求工具类:HttpRequest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;

/**
* @Author:范秉洋
* @Date:2019/8/25 17:31
*/
public class HttpRequest {

/**
* 向指定url发送GTE方法的请求
* @param url 发送请求的url
* @param param 请求参数,参数形式是name1=value1&name2=value2
* @return URL 所代表远程资源的响应结果
*/
public static String sendGet(String url,String param){
String result = "";
BufferedReader in = null;
try{
String urlNameString = url + "?" + param;
URL realUrl = new URL(urlNameString);
//打开和URL之间的链接
URLConnection connection = realUrl.openConnection();
//设置通用的请求属性
connection.setRequestProperty("accept","*/*");
connection.setRequestProperty("connection","Keep-Alive");
connection.setRequestProperty("user-agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");

//建立实际的链接
connection.connect();
//获取所用响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
//遍历所有的响应头字段
//for(String key:map.keySet()){
// System.out.println(key + "--->" + map.get(key));
//}
//定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null){
result += line;
}
}catch (Exception e){
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
//使用finally块来关闭输入流
finally {
try{
if(in != null){
in.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
return result;
}


/**
* 向指定url发送POST方法的请求
* @param url 发送请求的url
* @param param 请求参数,参数形式是name1=value1&name2=value2
* @return URL 所代表远程资源的响应结果
*/
public static String sendPost(String url,String param){
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try{
URL realURl = new URL(url);
//打开和URL之间的链接
URLConnection conn = realURl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept","*/*");
conn.setRequestProperty("connection","Keep-Alive");
conn.setRequestProperty("user-agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
//发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
//获取URLConnection对象对应的输入流
out = new PrintWriter((conn.getOutputStream()));
//发送请求参数
out.print(param);
//flush输出流的缓冲
out.flush();
//定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while((line = in.readLine()) != null){
result += line;
}
}catch (Exception e){
System.out.println("发送POST请求出现异常!" + e);
e.printStackTrace();
}
finally {
try{
if(out != null){
out.close();
}
if(in != null){
in.close();
}
}catch (IOException ex){
ex.printStackTrace();
}
}
return result;
}

}


AES解密工具类:AesCbcUtil.java

注意:重点标识的这个jar包commons.codec.jar,需要根据自己的jdk版本做对应的引入,我的是1.8的jdk,引入的是1.6的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package util;

import org.apache.commons.codec.binary.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.InvalidParameterSpecException;
import java.security.*;

import org.bouncycastle.jce.provider.BouncyCastleProvider;


/**
* @Author:范秉洋
* @Date:2019/8/25 17:25
*/
public class AesCbcUtil {

static {
//BouncyCastle是一个开源的加解密解决方案,主页在http://www.bouncycastle.org/
Security.addProvider(new BouncyCastleProvider());
}

/**
* AES解密
*
* @param data //密文,被加密的数据
* @param key //秘钥
* @param iv //偏移量
* @param encodingFormat //解密后的结果需要进行的编码
* @return
* @throws Exception
*/
public static String decrypt(String data, String key, String iv, String encodingFormat) throws Exception

//被加密的数据
byte[] dataByte = Base64.decodeBase64(data);
//加密秘钥
byte[] keyByte = Base64.decodeBase64(key);
//偏移量
byte[] ivByte = Base64.decodeBase64(iv);


try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");

SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");

AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));

cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化

byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
String result = new String(resultByte, encodingFormat);
return result;
}
return null;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidParameterSpecException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

return null;
}
}

3. 总结

        以上就是关于小程序登录过程的解析,最主要的就是通过用户授权之后获取用户的加密信息,通过加密数据、加密算法初始向量和登录凭证在后台进行解密,从而获得用户的完成信息,之后在进行开发者的逻辑操作对用户的个人信息进行操作和处理。上面提到的还有一点就是wx.getUserInfo(Object)方法不会在自动弹出授权窗口了,需要开发者自定义登录按钮来引导用户进行登录授权。