一、说明
在wx小程序的使用过程之中,往往会涉及到使用wx用户登录小程序的功能。用于收集wx小程序当前使用者的用户信息,方便程序的使用及更多功能的扩展。
个人小程序
对于个人或个体户开发的小程序,通常不需要公司授权。个人小程序可以直接使用微信账号进行登录,无需进行公司授权。这种方式的优点是节省开发和维护成本,并且安全性得到了保障,因为安全验证完全由企鹅(TengXun)负责。企业小程序
对于企业或组织开发的小程序,通常需要进行公司授权。企业小程序需要进行主体资质认证和账号权限认证,以确保开发者的真实身份和经营资质。在认证过程中,通常需要提供对公账户信息,这是为了确保认证主体身份的真实性和资金安全。
二、官网给出的WX小程序登录时序及注意事项
- 说明
- 调用 [wx.login()] 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 [auth.code2Session]接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到wx开放平台账号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意事项:
1、会话密钥session_key
是对用户数据进行 [加密签名]的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
2、临时登录凭证 code 只能使用一次。
三、前端页面
- UI结构
<template>
<view class="login-container">
<!-- 提示登录的图标 -->
<uni-icons type="contact-filled" size="100" color="#AFAFAF"></uni-icons>
<!-- 登录按钮 -->
<!-- 可以从 @getuserinfo 事件处理函数的形参中,获取到用户的基本信息 -->
<button type="primary" class="btn-login" open-type="getUserInfo" @getuserinfo="getUserInfo">一键登录</button>
<!-- 登录提示 -->
<view class="tips-text">登录后尽享更多权益</view>
</view>
</template>
- 样式
<style lang="scss">
.login-container{
height: 750rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #FFFFFF;
position: relative;
overflow: hidden;
&::after{
content: ' ';
display: block;
border-radius: 100%;
background-color: #F8F8F8;
width: 100%;
height: 40px;
position: absolute;
left:0;
bottom:0;
transform: translateY(50%);
}
// 登录按钮的样式
.btn-login {
width: 90%;
border-radius: 100px;
margin: 15px 0;
background-color: #c00000;
}
// 按钮下方提示消息的样式
.tips-text {
font-size: 12px;
color: gray;
}
}
</style>
-
渲染效果
image.png
四、获取用户信息及临时登录凭证code
- 服务器请求接口,在getUserInfo方法中进行调用返回token与用户信息。
async authUser(wxAuth){
const result = await uni.$http.post('/user/authUser',wxAuth)
if(result.statusCode === 200){
this.userInfo = result.data.data
this.saveToken(this.userInfo.token)
}
},
- getUserInfo方法中调用authUser方法进行用户信息处理。调用login方法获取临时登录凭证code。
// 获取微信用户的基本信息
getUserInfo(e) {
// 判断是否获取用户信息成功
if(e.detail.userInfo){
const info = e.detail
//获取微信临时登录凭证CODE
uni.login({
provider: 'weixin',
onlyAuthorize: true,//不传这个参数不返回code,仅仅请求授权认证
success: (res) => {
// 登录成功
// 保存code
// 调用后端接口获取数据就行了 不要使用 uni.getUserInfo去获取用户信息,因为设置 onlyAuthorize: true 的时候是获取不到的
// 准备参数对象
const query = {
encryptedData: info.encryptedData,
iv: info.iv,
code:res.code,
rawData: info.rawData,
signature: info.signature
}
//向服务器发送请求 校验 处理用户信息
this.authUser(query)
//将用户的基本信息保存到store中
this.updateUserInfo(e.detail.userInfo)
},
fail: (err) => {
if (err.code === -8) {
return uni.showToast('客户端未安装wx')
}
if (e.detail.errMsg === 'getUserInfo:fail auth deny') {
return uni.$showMsg('您取消了登录授权!')
}
uni.showToast({
title: "wx登录授权失败",
icon: "none"
});
}
})
}else{
uni.$showMsg("您取消了登录授权!")
}
},
- getUserInfo参数e包含用户信息如下,如果需要更加详细的信息,需要向企鹅公司申请授权。
image.png
其中userInfo
字段是用户信息明文开放数据,包含了wx用户的图像地址、昵称、性别、国家、城市、省份等信息。
为防止用户信息在网络传输中被篡改又分别提供了encryptedData
、signature
两个字段,用于后端对用户信息进行校验。
encryptedData
字段是用户信息加密数据。
signature
是用户信息的加密签名。
五、后台校验与解密开放数据
WX对开放数据做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。
签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 wx.login 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。
- 官方提供的加密数据解密算法说明如下:
接口如果涉及敏感数据(如[wx.getUserInfo]当中的 openId 和 unionId),接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(encryptedData) 进行对称解密。 解密算法如下:
- 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
- 对称解密的目标密文为 Base64_Decode(encryptedData)。
- 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
- 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
六、后端接口
- 后端获取会话密钥 session_key,对开发数据进行解密。
依据官方文档说明,后端需要调用 [auth.code2Session]接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。
后端调用auth.code2Session接口需要传递如下参数: APP_ID(小程序ID)、APP_SECRET(小程序秘钥)及临时登录凭证(CODE)。
其中临时登录凭证(CODE)由前端向后端服务器发送请求时提供。APP_ID(小程序ID)与APP_SECRET(小程序秘钥)在wx开发者管理平台注册小程序时可以获得。
- 根据临时登录凭证、APP_ID、APP_SECRET获取当前会话信息。
//小程序AppId
private final String APP_ID = "xxxxxxxxxx";
//小程序App秘钥
private final String APP_SECRET = "xxxxxxxxxx";
/**
* 根据临时登录凭证、APP_ID、APP_SECRET获取当前会话信息
* @param code 临时登录凭证(code)
* @return 返回当前会话信息(session_key、openid)
*/
public String getSessionInfo(String code){
//code2Session接口请求地址与参数
String url="https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
//拼接code2Session接口请求地址与参数
url = url.replace("{0}",APP_ID).replace("{1}",APP_SECRET).replace("{2}",code);
//向wx服务器发送请求并返回结果
String result = HttpUtil.get(url);
// result:{"session_key":"xxxx","openid":"xxxx"}
return result;
}
- 根据session_key对加密数据进行解密。
/**
*对加密数据进行解密
* @param sessionKey 会话秘钥
* @param encryptedData 加密数据
* @param iv
* @return 返回解密后的用户信息
*/
public String wxDecrypt(String sessionKey,String encryptedData,String iv){
// 开始解密
byte[] encData =cn.hutool.core.codec.Base64.decode(encryptedData);
byte[] ivByte = cn.hutool.core.codec.Base64.decode(iv);
byte[] key= Base64.decode(sessionKey);
AlgorithmParameterSpec ivSpec =new IvParameterSpec(ivByte);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec =new SecretKeySpec(key, "AES");
cipher.init(cipher.DECRYPT_MODE,keySpec, ivSpec);
return new String(cipher.doFinal(encData), "UTF-8");
}
- 后端接口
@PassToken
@PostMapping("/authUser")
@ApiOperation(value = "根据WX信息进行校验、登录、签发token",notes ="请查看Models数据字典")
public ObjectRestResponse authUser(@RequestBody WxAuth wxAuth) throws Exception {
//1、根据临时登录凭证 调用getSessionInfo方法
String result = loginUserService.getSessionInfo(wxAuth.getCode());
JSONObject jsonObject = JSON.parseObject(result);
String sessionKey =(String)jsonObject.get("session_key");
//2、对数据进行解密 调用wxDecrypt方法
String info = wxEncryptUtils.wxDecrypt(sessionKey,wxAuth.getEncryptedData(),wxAuth.getIv());
LoginUser wxUser = JSON.parseObject(info,LoginUser.class);
log.info(wxUser.toString());
//TODO:查询微信用户是否存在数据库中
//TODO:如果不存在就注册否则登录
//签发ToKen
TokenDataSource tokenDataSource = new TokenDataSource();
tokenDataSource.append("id",loginUser.getId()).append("openId",loginUser.getOpenId());
String token = TokenEncryptUtils.genToken();
//将token保存到redis数据库中
redisUtil.set(loginUser.getOpenId(),token,7*24*60*60);
//向前端返回用户信息与token
Map<String, Object> resultMap = JsonUtil.objToMap(loginUser);
resultMap.put("token",token);
return new ObjectRestResponse().data(resultMap);
}
- Models数据字典
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class WxAuth {
private String iv;
private String encryptedData;
private String code;
private String rawData;
private String signature;
}