手把手教你做微信小程序授权登录交互
开发需求:
我们团队在开发微信小程序过程中,需要绑定微信用户的信息到数据库里,那么就需要获得用户的唯一标识openid,而微信为了安全,是禁止小程序直接访问该接口,因此我们不能直接拿到用户的openid,从而需要通过调用微信接口实现授权登录。
我是孙不坚1208,这篇文章是在2021年暑假参加山东省大学生软件设计大赛时所写,主要是第一次接触小程序开发(uni-app+springboot),遇到的问题都记下来了,后面我也会持续更新出我的专栏《微信小程序开发指南》,欢迎与我一起学习,希望我的文章能够帮助到大家。
这篇文章是基于uniapp+springboot的微信小程序授权登录交互,对uniapp不熟悉的可以去这篇两万字的博客(【前端之旅】uni-app学习笔记)了解一下。
官方登录流程图(逻辑流程):
主要步骤:
- 前端获取到code(wx.login),将code通过uni.request()接口传到后台服务器。
- 后台服务器通过code、AppID和AppSecret等参数访问官方接口(传给微信服务器),获取到OpenId及本次会话秘钥session_key等。
- 后台服务器将OpenId进行相应的业务处理并返回给前端。
注意事项
- 会话密钥
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。 - 临时登录凭证 code 只能使用一次。
一、uni.login请求临时code
在微信小程序中,使用微信开放接口:wx.login(Object object),调用接口获取登录凭证(code)
在uniapp中,我们通过uni.login(OBJECT),调用接口获取登录凭证(code)。
OBJECT 参数说明
参数名 | 类型 | 必填 | 说明 | 平台差异说明 |
---|---|---|---|---|
provider | String | 否 | 登录服务提供商,通过 uni.getProvider 获取,如果不设置则弹出登录列表选择界面 | |
scopes | String/Array | 见平台差异说明 | 授权类型,默认 auth_base。支持 auth_base(静默授权)/ auth_user(主动授权) / auth_zhima(芝麻信用) | 支付宝小程序 |
timeout | Number | 否 | 超时时间,单位ms | 微信小程序、百度小程序 |
univerifyStyle | Object | 否 | 一键登录页面样式 | App 3.0.0+ |
success | Function | 否 | 接口调用成功的回调 | |
fail | Function | 否 | 接口调用失败的回调函数 | |
complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
success 返回参数说明
参数名 | 说明 |
---|---|
authResult | 登录服务商提供的登录信息,服务商不同返回的结果不完全相同 |
code | 小程序专有,用户登录凭证。开发者需要在开发者服务器后台,使用 code 换取 openid 和 session_key 等信息 |
errMsg | 描述信息 |
示例
uni.login({
provider: 'weixin',
success: function (res) {
console.log(res.Result);
}
});
二、uni.request向后台交换数据
我们通过 uni.request(OBJECT):向后台发起请求,后台通过前台发送得到的凭证(code),需要后台发送请求到微信接口,然后微信返回一个json格式的字符串到后台,后台处理之后,进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台帐号下的唯一标识(unionid)及本次登录的会话密钥(session_key)等, 再返回到前台。
uni.request(OBJECT):发起网络请求。
在各个小程序平台运行时,网络相关的 API 在使用前需要配置域名白名单。
OBJECT 参数说明
参数名 | 类型 | 必填 | 默认值 | 说明 | 平台差异说明 |
---|---|---|---|---|---|
url | String | 是 | 开发者服务器接口地址 | ||
data | Object/String/ArrayBuffer | 否 | 请求的参数 | App(自定义组件编译模式)不支持ArrayBuffer类型 | |
header | Object | 否 | 设置请求的 header,header 中不能设置 Referer。 | App、H5端会自动带上cookie,且H5端不可手动修改 | |
method | String | 否 | GET | 有效值详见下方说明 | |
timeout | Number | 否 | 60000 | 超时时间,单位 ms | H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序 |
dataType | String | 否 | json | 如果设为 json,会尝试对返回的数据做一次 JSON.parse | |
responseType | String | 否 | text | 设置响应的数据类型。合法值:text、arraybuffer | 支付宝小程序不支持 |
sslVerify | Boolean | 否 | true | 验证 ssl 证书 | 仅App安卓端支持(HBuilderX 2.3.3+) |
withCredentials | Boolean | 否 | false | 跨域请求时是否携带凭证(cookies) | 仅H5支持(HBuilderX 2.6.15+) |
firstIpv4 | Boolean | 否 | false | DNS解析时优先使用ipv4 | 仅 App-Android 支持 (HBuilderX 2.8.0+) |
success | Function | 否 | 收到开发者服务器成功返回的回调函数 | ||
fail | Function | 否 | 接口调用失败的回调函数 | ||
complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
method 有效值
必须大写,有效值在不同平台差异说明不同。
method | App | H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 字节跳动小程序 |
---|---|---|---|---|---|---|
GET | √ | √ | √ | √ | √ | √ |
POST | √ | √ | √ | √ | √ | √ |
PUT | √ | √ | √ | x | √ | √ |
DELETE | √ | √ | √ | x | √ | x |
CONNECT | x | √ | √ | x | x | x |
HEAD | x | √ | √ | x | √ | x |
OPTIONS | √ | √ | √ | x | √ | x |
TRACE | x | √ | √ | x | x | x |
success 返回参数说明
参数 | 类型 | 说明 |
---|---|---|
data | Object/String/ArrayBuffer | 开发者服务器返回的数据 |
statusCode | Number | 开发者服务器返回的 HTTP 状态码 |
header | Object | 开发者服务器返回的 HTTP Response Header |
cookies | Array.<string> |
开发者服务器返回的 cookies,格式为字符串数组 |
data 数据说明
最终发送给服务器的数据是 String 类型,如果传入的 data 不是 String 类型,会被转换成 String。转换规则如下:
- 对于
GET
方法,会将数据转换为 query string。例如{ name: 'name', age: 18 }
转换后的结果是name=name&age=18
。 - 对于
POST
方法且header['content-type']
为application/json
的数据,会进行 JSON 序列化。 - 对于
POST
方法且header['content-type']
为application/x-www-form-urlencoded
的数据,会将数据转换为 query string。
示例
uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
data: {
text: 'uni.request'
},
header: {
'custom-header': 'hello' //自定义请求头信息
},
success: (res) => {
console.log(res.data);
this.text = 'request success';
}
});
返回值
如果希望返回一个 requestTask
对象,需要至少传入 success / fail / complete 参数中的一个。例如:
var requestTask = uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
complete: ()=> {
}
});
requestTask.abort();
如果没有传入 success / fail / complete 参数,则会返回封装后的 Promise 对象:Promise 封装
通过 requestTask
,可中断请求任务。
requestTask 对象的方法列表
方法 | 参数 | 说明 |
---|---|---|
abort | 中断请求任务 | |
offHeadersReceived | 取消监听 HTTP Response Header 事件,仅微信小程序平台 支持,文档详情 |
|
onHeadersReceived | 监听 HTTP Response Header 事件。会比请求完成事件更早,仅微信小程序平台支持,文档详情 |
示例
const requestTask = uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
data: {
name: 'name',
age: 18
},
success: function(res) {
console.log(res.data);
}
});
// 中断请求任务
requestTask.abort();
Tips
- 请求的
header
中content-type
默认为application/json
。 - 避免在
header
中使用中文,或者使用 encodeURIComponent 进行编码,否则在百度小程序报错。 - 网络请求的
超时时间
可以统一在manifest.json
中配置 networkTimeout。 - H5 端本地调试需注意跨域问题,参考:调试跨域问题解决方案
- H5端 cookie 受跨域限制(和平时开发网站时一样),旧版的 uni.request 未支持 withCredentials 配置,可以直接使用 xhr 对象或者其他类库。
- uni-app 插件市场有flyio、axios等三方封装的拦截器可用
- localhost、127.0.0.1等服务器地址,只能在电脑端运行,手机端连接时不能访问。请使用标准IP并保证手机能连接电脑网络
- 单次网络请求数据量建议控制在50K以下(仅指json数据,不含图片),过多数据应分页获取,以提升应用体验。
附:了解各种平台的ID,它们都有相同的功能。
- OpenID:在微信应用(公众号、小程序等)默认使用 OpenID,在开发中请求的接口返回的一般都是OpenID。在小程序或微信网页里不用做授权,静默情况下也能拿到 OpenID。可以说 OpenID 是微信生态里最重要的一个 ID。可以理解 OpenID 是通过 AppID 和微信用户 ID 加密得到的,其与微信应用(每个应用会有 AppID)相关,每个微信应用都会生成一个唯一的用户的识别。
- AppID 和 AppSecret:公众号和小程序都会有一个 AppID 用来标识当前的微信应用,而如果需要开发的话,接口的请求都需要用到 AppSecret。
- 微信用户 ID:微信用户 ID 是有加密的,是无法拿到的。一般我们使用微信机器人开发的话,常用的是微信号或者微信昵称作为 ID。
- UnionID:在微信开放平台里面,做了账号绑定后,就会生成一个统一的 UnionID,绑定后的微信应用(小程序、公众号等)都可以使用一个 ID。获取 UnionID 需要经过用户授权。
- UUID:主要是针对于前端的设备,比如小程序或者网页的,因为获取 OpenID 需要一定的开发,所以如果在获取不到的情况下,我们一般会给当前浏览器或者小程序生成一个随机的 ID。
- UserID:用户的真实 ID,一般是存在数据库的 ID。
三、源代码
1.调用微信API wx.login()得到code。
2.把得到的code传给后端,后端获取code请求微信小程序官方接口,返回给前端openid。
3.知道openid后进行用户相关的操作,可以用于分别用户的登录状态。
前台:在GetUserInfo中添加接口
GetUserInfo() {
var _this = this;
// 增加约束,不选协议无法进行授权
if (this.value === false) {
uni.showToast({
title: '请阅读勾选协议',
icon: 'error',
duration: 2000
});
} else {
uni.getUserProfile({
desc: '登录',
lang: 'zh_CN',
success: (res) => {
console.log('获取的信息', res.userInfo);
_this.nickName = res.userInfo.nickName;
_this.setNn(res.userInfo.nickName);
uni.getLocation({
type: 'gci02',
success: res => {
uni.reLaunch({
url: 'Login2'
});
}
});
},
fail: (res) => {
console.log('用户拒绝了授权');
uni.showToast({
title: '授权失败',
icon: 'error',
duration: 2000
});
}
});
}
}
login() {
let _this = this;
uni.showLoading({
title: '登录中...'
});
// 获取登录用户 code
uni.login({
provider: 'weixin',
success: function(res) {
if (res.code) {
let code = res.code;
console.log('用户code:', res.code);
uni.request({
url: "https://xxxxxxx:8084/wxLogin/getOpenid",
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {
code: res.code, //wx.login 登录成功后的code
role: _this.role,
},
success: function(cts) {
var openid = cts.data.openid; //openid 用户唯一标识
var userInfo = {
openid: openid,
role: _this.role
};
console.log(_this.role);
_this.saveUserInfo(userInfo);
uni.hideLoading();
uni.switchTab({
// 登录成功后的跳转
url: '../myCenter/myCenter'
});
}
});
} else {
uni.showToast({
title: '登录失败!',
duration: 2000
});
console.log('登录失败!' + res.errMsg)
}
},
});
}
}
后台:SpringBoot后台数据处理
-
首先获取的自己小程序的appid、secret,封装为一个接口
-
接口当中,拼接sql,向微信发送http请求
-
将返回的结果进行封装,封装成map集合返回给前端
@PostMapping("/getOpenid")
@ResponseBody
public Map<String, Object> getOpenId(@RequestParam("code") String code,
@RequestParam(value = "role") Integer role) throws IOException {
// 返回code
System.out.println("========== 进入wxLogin/getOpenid方法 ==========");
System.err.println("微信授权登录");
System.err.println("code值: " + code);
Map<String, Object> resultMap = new HashMap<>();
String appid = ConstUtil.getAppId();
String secret = ConstUtil.getSECRET();
// 拼接sql
String loginUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid +
"&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";
CloseableHttpClient client = null;
CloseableHttpResponse response = null;
// 创建httpGet请求
HttpGet httpGet = new HttpGet(loginUrl);
// 发送请求
client = HttpClients.createDefault();
// 执行请求
response = client.execute(httpGet);
// 得到返回数据
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity);
System.out.println("微信返回的结果" + result);
resultMap.put("data", result);
// 对返回的结果进行解析
JSONObject json_test = JSONObject.parseObject(result);
String openid = json_test.getString("openid");
String sessionKey = json_test.getString("session_key");
System.err.println("openid值: " + openid);
System.err.println("sessionKey值" + sessionKey);
Users users = usersService.getUserByOpenid(openid);
System.err.println("用户信息:" + users);
if (StringUtils.isEmpty(openid)) {
resultMap.put("state", ResponseCode.SUCCESS.getCode());
resultMap.put("message", "未获取到openid");
return resultMap;
} else {
// 判断是否为首次登陆
if (users == null) {
resultMap.put("state", ResponseCode.SUCCESS.getCode());
resultMap.put("openid", openid);
resultMap.put("sessionKey", sessionKey);
resultMap.put("message", "未查询到用户信息");
} else {
// 查询有无结果
UserRole uRes = userRoleService.getUserRole(openid, role);
// 封装对象
UserRole userRole = new UserRole(openid, role);
// 如果不是第一次登录,第二次登陆进行判断,如果没有这个身份,进行添加,如果有这个身份,不做处理
if (uRes == null) {
userRoleService.insert(userRole);
}
resultMap.put("state", ResponseCode.SUCCESS.getCode());
// 前台拿的数据是map的key
resultMap.put("openid", openid);
resultMap.put("sessionKey", sessionKey);
resultMap.put("user", users);
resultMap.put("message", "该用户已经存在");
}
response.close();
client.close();
}
return resultMap;
}