小程序登录——用户个人信息解密

前段时间在忙一个微信小程序的项目,这边就用户登录和用户信息的解密来做一个总结。

一、小程序登录流程

api-login.2fcc9f35

  1. 小程序向wx.login()发送请求获取到 code
  2. 小程序将自己的登录信息和code发送到开发者的服务器
  3. 服务器拿到code和申请到小程序申请的AppIDAppSecret一起发送到微信后台
  4. 微信后台返回session_idopenid给开发者服务器
  5. 服务器通过特定算法校验签名解密信息
  6. 服务器给小程序下发令牌token
  7. 小程序将token存入内存并加在每次请求的请求头中
  8. 服务器验证token的合法性,并返回业务数据

二、小程序 code 的获取

1. wx.getUserInfo()

该接口会返回用户信息和用户的明文信息编码

image-20200615180424371

  • encryptedData:包括敏感数据在内的完整用户信息的加密数据
  • iv:加密算法的初始向量
  • signature:使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息
  • userInfo:用户信息对象,不包含 openid 等敏感信息
  • errMsg:返回状态信息

2. wx.wx.login

调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。用户数据的加解密通讯需要依赖会话密钥完成。

image-20200615181400557

  • errMsg:返回状态信息
  • code:用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 auth.code2Session,使用 code 换取 openid 和 session_key 等信息

三、搭建服务器接受参数

小程序端发送参数

1
2
3
4
5
6
7
8
9
10
11
12
13
uni.request({
url: 'http://127.0.0.1:8080',
method: 'POST',
data: {
js_code: this.js_code,
encryptedData: this.encryptedData,
iv: this.iv,
rawData: this.rawData,
signature: this.signature },
success: function success(res) {
console.log(res.data);
_this.text = 'request success';
} });

后端参数接收

在这里我们以 Springboot 为例作为我们的开发者服务器后来来接受传递参数,这里只是简单测试解密就将业务代码放在 controller 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

@CrossOrigin
// 处理跨域请求
@PostMapping("/")
String index(@RequestBody Map map) {
String code = (String) map.get("js_code");
String encryptedData = (String) map.get("encryptedData");
String iv = (String) map.get("iv");
String rawData = (String) map.get("rawData");
String signature = (String) map.get("signature");
System.out.println("code: "+code);
System.out.println("encryptedData: "+encryptedData);
System.out.println("iv: "+iv);
System.out.println("rawData: "+rawData);
System.out.println("signature: "+signature);
return "ok";
}
}

获取AppIdAppSecret

在解密之前我们还需要去微信平台申请我们的小程序的一些 特征id https://mp.weixin.qq.com/

image-20200615184442671

四、密钥请求

这里我们需要访问微信后台的结合来拿到解密密钥 auth.code2Session

请求地址

1
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数

属性 类型 默认值 必填 说明
appid string 小程序 appId
secret string 小程序 appSecret
js_code string 登录时获取的 code
grant_type string 授权类型,此处只需填写 authorization_code

返回值

返回的 JSON 数据包

属性 类型 说明
openid string 用户唯一标识
session_key string 会话密钥
unionid string 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回
errcode number 错误码
errmsg string 错误信息

发送请求

1
2
3
4
5
6
7
8
9
10
11
12
String AppSecret = "你的AppSecret";
String AppID = "你的AppID";
String url = "https://api.weixin.qq.com/sns/jscode2session?appid="+AppID+"&secret="+AppSecret+"&js_code="+code+"&grant_type=authorization_code";
RestTemplate restTemplate = new RestTemplate();
// 这里我们选用的是 'RestTemplate' 来发送请求
String response = restTemplate.getForObject(url, String.class);
HashMap wx_key = JSON.parseObject(response, HashMap.class);
// 用 com.alibaba.fastjson 这个工具包
String session_key = (String) wx_key.get("session_key");
String openid = (String) wx_key.get("openid");
System.out.println("session_key: "+session_key); // session_key: 4OKemFLLt1CHQsGAqNSQlw==
System.out.println("openid: "+openid); // openid: o877d4h5yA0dqlLyGL3syXtozmr8

fastjson 工具包引入 maven依赖引入

1
2
3
4
5
><dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.37</version>
></dependency>

五、算法解密

签名校验

1
2
3
4
5
6
String signature2 = DigestUtils.sha1Hex(rawData + session_key);
// 选用 org.apache.commons.codec.digest.DigestUtils 工具类
System.out.println("signature: "+signature);
// signature: 42e47c9e0fa6a3064621425148a456a068c0a5a7
System.out.println("signature2: "+signature2);
// signature2: 42e47c9e0fa6a3064621425148a456a068c0a5a7

maven依赖引入

1
2
3
4
5
><dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
></dependency>

算法解密

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
JSONObject getUserInfo(String encryptedData, String sessionKey, String iv){
// 被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decode(sessionKey);
// 偏移量
byte[] ivByte = Base64.decode(iv);

try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
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, "UTF-8");
return JSONObject.parseObject(result);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

maven依赖引入

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.59</version>
</dependency>

然后我们将结果打印就能得到我们解密后的信息了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"country":"China",
"watermark":{
"appid":"你的AppId",
"timestamp":1592219018 // 当时时间戳
},
"gender":1,
"province":"Jiangsu",
"city":"Nanjing",
"avatarUrl":"你的头像链接地址",
"openId":"你的OpenId",
"nickName":"丁生",
"language":"zh_CN"
}
// unionid 值的获取定到微信公众平台