概述
1.前言
在前面的文章中支持了钉钉扫码
方式登录客户端,然后就可以控制当前登录的用户访问权限。现在主流的团队协作方式,一种是钉钉
,另外一种就是企业微信
了。
既然支持了钉钉扫码的方式
,那就需要支持企业微信扫码的方式
。
2.企业微信扫码登陆
详见官方API
仔细看了一下官方提供的API,感觉跟钉钉很像,也有很大的不同之处。两者的区别如下:
钉钉扫码分为以下几步:
- 钉钉开发者后台需要配置
appid
,redirecturi
。- 通过上面的配置项在构建钉钉扫码对象的时候,去构建一个连接,并且使用encode编码,同时支持样式的设置。
- 构建完钉钉对象,会去给
window
创建一个监听message
的函数。以便在触发扫码授权之后,能通过该callback函数拿到一些相关信息。比如:event.origin
和event.data
。其中origin为钉钉域名https://login.dingtalk.com
,而data就是临时授权码
。- 将
临时授权码
发给服务端,服务端根据临时授权码做重定向拿到真正的code值
,然后根据code
去钉钉拿到当前扫码登陆的用户。成功后返回用户的信息给前端,用于显示。由于之前没有解决重定向的问题,所以重定向交给了后台处理。后台接收到重定向302就直接不管,因为服务端没有网页,不需要处理重定向。这是一个取巧的方法。
而企业微信有些差别
企业微信扫码登陆分为下面几步:
- 企业微信开发者后台配置
appid
,redirecturi
以及agentid
。- 通过上面的配置信息,去构建一个企业微信对象,该对象中不需要构建用于重定向的连接。且不支持样式的配置。如果你想修改样式,需要自定义样式文件,来覆盖掉企业微信默认的样式。
- 根据官方文档是没有提及关于监听
message
的callback
函数的的,不知道官方是不是做了处理。如果拿不到callback函数,那么我就无法获取到企业微信返回的数据,也就拿不到code
值。- 如果能拿到code值,将code值传给服务端,服务端根据code值去企业微信拿到扫码用户的信息,并返回给前端。
问题出现在第三步。看过企业微信官方的API文档你就会知道,它没有关于message
的callback函数说明。于是,就查看了一下源码:
!function(a, b, c) {
function d(c) {
var d = b.createElement("iframe"),
e = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=" + c.appid + "&agentid=" + c.agentid + "&redirect_uri=" + c.redirect_uri + "&state=" + c.state + "&login_type=jssdk";
e += c.style ? "&style=" + c.style: "",
e += c.href ? "&href=" + c.href: "",
d.src = e,
d.frameBorder = "0",
d.allowTransparency = "true",
d.scrolling = "no",
d.width = "300px",
d.height = "400px";
var f = b.getElementById(c.id);
f.innerHTML = "",
f.appendChild(d),
d.onload = function() {
d.contentWindow.postMessage && a.addEventListener && (a.addEventListener("message",
function(b) {
b.data && b.origin.indexOf("work.weixin.qq.com") > -1 && (a.location.href = b.data)
}), d.contentWindow.postMessage("ask_usePostMessage", "*"))
}
}
a.WwLogin = d
} (window, document);
其实从源码中可以看出来,它其实是创建了一个frame,然后把企业微信二维码作为src嵌入到frame中,同时设置了一些样式。
在最后的函数中可以看出来,他其实也是做了创建关于message
的监听事件。
d.contentWindow.postMessage
可能是指,扫码后发送的消息。
a.addEventListener
是判断有无监听函数。
如果上面两个都为true,那么就会创建一个监听函数,并且在监听函数中拿到data
和origin
数据。
了解这些细节,就可以动手写代码了。
2.1 创建企业微信对象
由于在企业微信开发者平台配置的appid
,redirecturi
以及agentid
都是存在服务端的,所以第一步应该是去服务端获取这三个值。具体的代码我就不写了,就是一个请求的事情。
拿到这个三个值,就可以在点击企业微信扫码
跳转到扫码页面上去构建这个扫码对象了。
由于使用的是VUE
,可以通过this.$router.push
的方式将值放到query中,然后在扫码页面,通过this.$route.query
去获取。
登陆页面login.vue
query = {
loginVo: this.loginVo,
redirecturi: result.Redurl,
appid: result.Qrappid,
agentid: result.Qragentid,
};
this.$router.push({
path: this.scantype === "dingding" ? "/scan" : "/wechatscan",
query: query,
});
企业微信扫码wechatscan.vue
,在created函数中赋值操作
created() {
this.loginVo = this.$route.query.loginVo;
this.redirecturi = this.$route.query.redirecturi;
this.appid = this.$route.query.appid;
this.agentid = this.$route.query.agentid;
},
因为创建扫码对象需要绑定在一个页面DOM容器上,所以需要等页面渲染之后,才能创建这个对象,否则在创建对象的时候会出现容器不存在的错误信息。所以,我们需要在mounted
生命周期函数中去创建这个扫码对象:
创建扫码容器:
<div id="wechat_container"></div>
创建扫码对象:
mounted() {
// 创建一个企业登陆对象,由于需要渲染挂载到指定的div上,所以需要确保该div已被渲染,否则会出现错误
window.WwLogin({
id: "wechat_container",
appid: this.appid,
agentid: this.agentid,
redirect_uri: encodeURIComponent(this.redirecturi),
});
// 创建扫码监听事件,需要在不用的时候进行清除
if (typeof window.addEventListener != "undefined") {
window.addEventListener("message", this.handleMessage, false);
} else if (typeof window.attachEvent != "undefined") {
window.attachEvent("onmessage", this.handleMessage);
}
},
2.2 创建监听函数
我们可以打印一下返回的数据:
handleMessage(event) {
console.log("event", event);
let origin = event.origin;
console.log("event.origin", event.origin);
if (origin == "https://open.work.weixin.qq.com") {
console.log("event.data", event.data);
const query = event.data.split("?")[1];
const params = qs.parse(query);
const code = params.code;
console.log("code", code);
//获取到code后就可以发给服务端做验证
});
}
},
从结果来看我们确实拿到了event.origin
和event.data
了。不过data
数据我们还需要再处理一下,他给我们做成了一个包含参数的连接,我们可以借助于qs
来转化一下。
下面的代码就相对简单了,我就不再赘述了。
3.问题
3.1 企业微信重定向的问题
上面讲到了钉钉扫码
和企业微信扫码
的区别:
钉钉扫码
需要先获取到临时码,然后根据临时码去获取授权码,通过授权码认证通过后,就会重定向到你所配置的redirecturi
。企业微信扫码
则直接或获取到授权码,扫码成功后就会进行重定向。- 由于
钉钉扫码
分为两步,前端则负责取到临时码
,然后服务端根据临时码
去获取到授权码,然后认证重定向。不需要在前端进行重定向。服务端的处理逻辑是,收到302重定向直接忽视即可,然后将获取到的用户信息
返回给客户端。客户端再进行页面跳转即可完成扫码过程。企业微信扫码
却不一样,因为它拿到的授权码的过程都是在客户端,导致在拿到授权码后,认证成功后会进行重定向,而重定向操作是在客户端,重定向到你所配置的redirecturi
。导致你需要在客户端做重定向的处理。
那么问题来了,如何处理企业微信扫码
的重定向操作。我们知道浏览器输入一个地址或者通过window.location.href
便可以进行重定向。如果你做过客户端开发,或者是使用过electron做开发,那么你肯定会知道,在开发环境下通过npm run dev
命令,会进入到开发环境,nodejs
会帮你启动一个本地服务和对应的端口,比如我的开发环境为localhost:9080
:
而通过开发者模式控制台也可以看到:
知道这个地址,有的人可能会想到:如果我得redirecturi配置为lcoalhost:9080,那么在扫码之后的重定向不就会重定向到我的页面了么?
。
对,确实如此。但是你别忘了这是在开发环境,你有没有测试过生产环境呢?
记得在以前的相关文章中也进行过测试,结果是:跑在本地的服务,是没有所谓的localhost:9080的。我也曾尝试能不能通过nodejs去启动一个服务并且端口为9080,然后去监听9080端口,收到重定向后再做进一步的处理但很遗憾的是我们不可能为了这个却占用本地的一个端口,要知道每一个端口都有其意义,而且你也不能保证用户的这个端口有没有被其他应用占用。所以,这样做肯定是不行的。
如果我们配置的地址在不存你或者是其他地址,那么在进行重定向后,页面将是一片空白(也就是重定向到一个不存在的页面),即便这个地址存在,你也需要在本地或者服务器上托管一套前端代码。
比如现在,我通过企业微信扫码
登陆后,需要跳转到主页面system,处理逻辑为:
this.$electron.ipcRenderer.once("window-menu-message", () => {
this.$router.push('/system')
});
扫码的结果如下图,其实可以看到整个扫码的流程还算顺利,直到看到最后一句话Navigated to chrome-error://chromewebdata/
,知道他是进行了重定向,且重定向失败了,导致出现了这个错误。
Navigated to chrome-error://chromewebdata/
:
由于某种原因,您的代码试图导航到无效(不存在)的URL,然后导致 window.location.href 为 chrome-error:// chromewebdata 。
而在开发者工具network中也可以看到,它确实尝试进行重定向到我配置的redirecturi=https://127.0.0.1:8080/wxqcc
,但事实上这个地址是不存在的,所以才会出现上面的那个问题。
而此时的扫码之后的/system
页面也确实是一片空白。
既然问题出现了那就需要找到解决办法。
从可行性来讲有两种处理方式:
- 解决重定向,或者禁止浏览器进行重定向。
- 当重定向到一个不存在的页面时,将其重定向回来,然后通过路由跳转到
system
页面。
*而在关于禁止重定向
的方法上,没有找到合适的处理方式,这个还待研究,暂时TODO一下。
而关于第二种方式重定向之后再次进行重定向,想起来有点靠谱。动手尝试一下。
~~想法是:扫码成功后,通过window.location.href
重定向到/system
页面。而关于页面之间的跳转其实是通过vue的$router
实现的,这个不受location
的限制,如果直接重定向window.location.href='/system'
this.$electron.ipcRenderer.once("window-menu-message", () => {
window.location.href = '/system'
});
从下图可以看出,它确实往/system
中重定向了,但是/system
路径不存在。
而vue
中关于路由的配置信息如下,这里可以猜出是不是使用的hash路由?
export default new Router({
routes: [
{
path: '/',
name: 'login',
component: require('@/pages/login/Login').default
},
{
path: '/system',
name: 'system',
component: require('@/pages/home/system/System').default
},
{
path: '/user',
name: 'user',
component: require('@/pages/home/user/User').default
},
{
path:'/scan',
name:'scan',
component: require('@/pages/home/scan/Scan').default
},
{
path:'/wechatscan',
name:'wechatScan',
component: require('@/pages/home/scan/WechatScan').default
}
]
})
通过打印一下window.location.href
,可以看出上面配置的路由信息,其实在浏览器中是/#/system
而不是/system
那接着尝试着使用window.lcoation.href='/#/system'
,发现依然无法访问,跟上面使用/system
一样。
然后尝试能不能往/
进行重定向呢?
this.$electron.ipcRenderer.once("window-menu-message", () => {
window.location.href = '/'
});
然后,发现扫码后,能正常重定向到登录页,因为登录页配置的是'/'
。
???
如果,能重定向到登录页,也不是不可以,因为重定向登录页,也就意味着在我的项目中是可控的,可以将其通过路由this.$router
的方式跳转到/system
页面。
那么既然重定向到登录页,也就意味着我需要做一些特殊的处理:
- 重定向到登录页,意味着渲染了两次登录页,那么你需要根据一个标志位
isLogined
来判断需不需要走登陆的流程。即如果你是通过扫码进行重定向的,那么就不需要走登陆流程,而直接跳转到system
页面。如果不是,那么需要做登陆流程。- 由于你是从新走了一遍登陆页面,那么你需要将一些状态值重新修改成正确的。比如我登录后的托盘图标应该是登录后的状态即高亮显示,而未登录(在登录页)状态下,应该是灰色的。那么你重定向到登录页,势必会让图标从高亮变成灰色的,那么你就需要重新将图标修改为高亮。
- 在退出或者注销的时候,应该将1中设置的标志位进行重置,否则下次登录,会直接跳转到
system
而不走登录了,程序就会异常。
具体的代码其实很简单,扫码登陆成功后,通过代码设置isLogined
localStorage.setItem("isLogined", 'true');
然后在登录页的created
周期函数中处理:
created() {
if (localStorage.getItem("isLogined") === "true") {
// 由于是登陆后重定向到登录页,所以需要对一些标志做处理
this.$store.commit("SET_IS_LOGIN", true);
this.$store.commit("SET_PASSWORD_MSG", false);
this.$router.push("/system");
// 用户成功登陆之后,修改托盘菜单及隐藏表单
this.$electron.ipcRenderer.send("window-menu", true);
this.$electron.ipcRenderer.send("system-status", {
isLogin: true,
status: true,
});
}
}
由于,我的应用是,登录后直接隐藏,作为一个认证的工具,不用显示界面,所以我可以在隐藏之后做处理,用户是感知不到账任何变化的。可能对于不同的需求需要做不同的处理吧。
在验证一下,确实能够正常
使用。只是总觉得别扭。等研究解决重定向的问题了,再来从根本上解决问题,而不是使用一些取巧的方式。因为,说不定哪天代码发再度变高了,就会导致其他的问题。~~
3.2 样式覆盖的问题
查看钉钉扫码的文档,可以看出来,钉钉在创建扫码对象的时候,就可以指定其样式(宽高,背景色…)。
var obj = DDLogin({
id:"login_container",//这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
goto: "", //请参考注释里的方式
style: "border:none;background-color:#FFFFFF;",
width : "365",
height: "400"
});
而企业微信扫码
就不一样了,他需要你定义一个css样式文件然后通过href属性去引用这个样式文件,并且只支持https
var wwLogin = new WwLogin({
"id": "wx_reg",
"appid": "",
"agentid": "",
"redirect_uri": "",
"state": "",
"href": "",
"lang": "zh",
});
其中href
使用也有实例,就挺懵逼的。我只想简单的配置一下样式
尝试在扫码的同级目录下创建一个css文件,然后引入后发现
从下图看出,这个地址指向了企业微信开发者地址https://open.work.weixin.qq.com/wwopen/sso/v1/index.css
:
好蒙蔽啊,难道我要上传自己的样式文件到企业微信的服务器上么?这也太扯了吧。
从DOM元素中可以看到,原来他是把./
解析到当前的地址中,即https://open.work.weixin.qq.com/wwopen/sso/
的同级,所以我们不能在项目中去使用这个样式文件,必须要在一个确切的https
协议的地址才可以。
我们知道iframe还会牵扯到一个问题,那就是跨域的问题,不同域名之间犹豫同源策略是不支持js跨域修改的。多以想要通过js去操作iframe中的数据几乎是不可能的。
在网上看到了一个提问,从官方回答来看,是不支持修改样式文件的,由于是2020年的问题了,现在的新版本给出了上面的通过href
去引用连接css文件地址的方式应该是可行的:
3.3 修改源码调整样式
由于提供https来承载一个css文件的开销较大,得不偿失。所以打算放弃上面的方式(当然如果你的面板足够大,那么就不需要关心这些,只要使用企业微信默认的样式就好了,企业微信的默认宽300px,高400px)。
那么如果我得程序面板大小是固定的,或者不足以显示企业微信默认的大小,就会显示出滚动条,类似下面的。
前面我们也知道同源策略导致我们无法去直接修改企业微信的iframe中样式,即便是获取数据也不行。那么我们能想到最直接的办法就是:将企业微信的扫码源码拉取到本地,然后在源码上动手脚,这样一来iframe就是完全可控的了,接下爱我们就是可以在源码上动手脚了
。
什么?你不知道则呢么拉取源码?
直接点击企业微信官方提供的js文件,就会在浏览器中显示出来他的源码,然后将源码copy出来粘贴到你的项目中。
为了方便,我是直接放到主页面中的。在我的Electron-vue
项目中,主页面是index.ejs
模板文件,当然如果你是用的是其他框架,可以放到你的主入口文件中。
试想啊,如果你想让企业微信扫码的iframe适应你的面板大小,那么你得给他设置一个宽高吧,当然在源码中你可以看到代码是可以直接设置宽高的,从扫码的显示来看:宽度够用,而高度不足,这样就可以设置宽度100%(设置100%会让二维码自动居中,虽然我们没做居中处理,猜想是二维码内部做了处理),高度设置为你得面板高度,比如我的是315px,那么当我去设置的时候,就会发现,企业微信的扫码被裁减了,剩余的不显示了
,如下:
会发现,它的扫码提示被裁剪了…。
原因猜想应该是:iframe的大小不足以容纳二维码的大小,且无法去通过样式和js去控制二维码图片的宽高,iframe变小了,二维码没变,导致二维码被裁剪了
。
虽然被裁减了,但是好像大小合适,也能正常扫码用了。
想要调试iframe的样式,首先我们要找到这个iframe,查看了一下iframe的属性发现并没有什么好的方法去定位这个iframe,不得已我们只能通过id去获取它:
既然要使用到id那么就需要给iframe绑定一个id,如下绑定了一个叫做wxiframe
的:
!function(a, b, c) {
function d(c) {
var d = b.createElement("iframe"),
e = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=" + c.appid + "&agentid=" + c.agentid + "&redirect_uri=" + c.redirect_uri + "&state=" + c.state + "&login_type=jssdk";
e += c.style ? "&style=" + c.style: "",
e += c.href ? "&href=" + c.href: "",
d.src = e,
d.frameBorder = "0",
d.allowTransparency = "true",
d.scrolling = "no",
d.width = "100%",
d.height = "315px",
d.id = 'wxiframe';
var f = b.getElementById(c.id);
f.innerHTML = "",
f.appendChild(d),
d.onload = function() {
d.contentWindow.postMessage && a.addEventListener && (a.addEventListener("message",
function(b) {
b.data && b.origin.indexOf("work.weixin.qq.com") > -1 && (a.location.href = b.data)
}), d.contentWindow.postMessage("ask_usePostMessage", "*"))
}
}
a.WwLogin = d
} (window, document);
绑定之后,我们可以使用通过getelementbyid
获取到iframe,如果你懂css的话可能会想到transform
属性,通过它来缩放大小。
!function(a, b, c) {
function d(c) {
var d = b.createElement("iframe"),
e = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=" + c.appid + "&agentid=" + c.agentid + "&redirect_uri=" + c.redirect_uri + "&state=" + c.state + "&login_type=jssdk";
e += c.style ? "&style=" + c.style: "",
e += c.href ? "&href=" + c.href: "",
d.src = e,
d.frameBorder = "0",
d.allowTransparency = "true",
d.scrolling = "no",
d.width = "100%",
d.height = "315px",
d.id = 'wxiframe';
var f = b.getElementById(c.id);
f.innerHTML = "",
f.appendChild(d),
d.onload = function() {
d.contentWindow.postMessage && a.addEventListener && (a.addEventListener("message",
function(b) {
b.data && b.origin.indexOf("work.weixin.qq.com") > -1 && (a.location.href = b.data)
}), d.contentWindow.postMessage("ask_usePostMessage", "*"))
var wxiframe = document.getElementById('wxiframe')
wxiframe.style.transform = 'scale(.9)'
}
}
a.WwLogin = d
} (window, document);
发现,确实变小了,但是扫码的提示信息似乎还不显示。
通过控制台可以发现,其实你对iframe进行缩放只是整体缩放了大小,并没有改变扫码和iframe的相对位置和大小,这就导致还是被裁剪的二维码。所以我们就需要对这个样式进行设定,当然不一定是图中的这个样式,只要扫码的任意父节点进行缩放都可以达到效果。
但是看起来边距什么的都需要调整,可能搞起来很麻烦。故我就在这里做尝试了(可以尝试使用jQuery来处理这种),即便不显示下面的扫码提示信息,也依旧能够扫码登陆。
3.3 改变窗体大小来适应企业微信扫码
这个搞起来不难,就是需要中转几步:
- 点击扫码的时候,可以通过渲染进程给主进程发送一个消息,主进程收到消息去改变窗体的大小以适应二维码的大小。
- 改变之后再去拉取二维码显示。
- 进行后续扫码流程
当然,类似的你还可以创建一个子窗口(可以固定大小和二维码的大小一致)来展示二维码,扫码之后,销毁子窗口,将成功的信息传给父窗口,那么父窗口接到子窗口的数据就可以进行跳转处理了(登陆成功跳转到主页面,登陆不成功不跳转,提示失败)。
这么做估计连上面的重定向问题
也一并解决了。因为子窗口就负责展示二维码,扫码流程,只要获取到服务端的认证结果,至于子窗口重定向与否,都无关紧要了。因为拿到结果就直接销毁子窗口了,父窗口就不需要处理重定向了。我觉得应该是这么回事,虽然没有尝试,如果有感兴趣的可以告诉我结果。缺点就是,你需要额外定义一个单独的页面去支持二维码,因为牵扯到你需要通过window.opner
来进行父子窗口的传参。
4. 更正
在3中提到如何处理重定向的问题,其中提到了两种解决方案:
- 解决重定向的问题
- 重定向之后,再利用
window.location.href='/'
重定向回项目的根路径,再通过路由的方式去跳转到主页面。
在2的基础上,今天打包之后,跑在生成环境,发现依然出现空白,也就是说重定向失败,还是回到了最初。原因是:通过location进行重定向到/根据路也是基于你起的服务的根路径
,这就导致在生成环境没有所谓的本地服务导致无法重定向到正确的地址。
看来又回到怎么去解决重定向的问题
?
想了想,由于重定向到一个不存在的地址导致我的程序无法做后续操作,才会出现空白而无法应对的情况。那么如果我能够控制重定向或者监听到重定向的操作呢?
无独有偶,在github
上看到了关于扫码的issue
详见这里
提到了关于通过will-navigate
来监听重定向的操作,其中在官方文档中也写道:
那么通过这个事件来获取一下重定向的事件,我们知道在整个扫码操作中有两个navigate
,一个是往企业微信跳转的时候,另一个就是扫码成功后企业微信重定向到我们的redirecturi
上。
那我们只需要在监听到重定向操作时,判断返回的url是不是我们配置的redirecturi
就行了,只要能拿到这个uri,无论是不是真实存在的地址,我们的程序都变得可控了。拿到之后主进程可以向渲染进程中发送一个事件,渲染进程监听后,通过路由跳转到/system
页面。
主进程中:
// 创建窗口的时候启动重定向的监听
mainWindow.webContents.on('will-navigate', (e, url, status) => {
// 此处,redirecturi为了达到可配置的目的,是存服务器上的,点击扫码时从服务端获取到。当然为了方便你也可以写死,但不建议这么做。
if (url.startsWith(redirecturi)) {
e.preventDefault()
// do something
mainWindow.webContents.send('redirect-uri')
}
});
渲染进程中收到重定向的请求:
// 监听进行重定向操作,通过路由跳转到wechatscan主页面,因为扫码后他会首先进行重定向,当你重定向到是当前扫码页面的时候,才能接收到扫码的response,否则获取不到返回值。返回到扫码页面后,你需要根据返回值是否扫码成功,来判断是否往system跳转,而不是直接跳转到system页面。
this.$electron.ipcRenderer.once("redirect-uri", (event) => {
this.$router.push('/wechatscan')
});
其中,监听到重定向的地址为:
[2021-08-12 18:14:21.058] [info] 扫码登陆参数: {"type":"login","address":"192.168.0.55","port":"10000","username":"zAnkQLjc2Jdp2UVwsex2FDP7p2O-OaBMp-85lvTt2Lc","auth":"wechat"}
[2021-08-12 18:14:21.067] [info] **************url************* https://127.0.0.1:8080/wxqcc?code=zAnkQLjc2Jdp2UVwsex2FDP7p2O-OaBMp-85lvTt2Lc&state=undefined&appid=ww721d49a381861f97
[2021-08-12 18:14:21.891] [info] 扫码登陆结果: {
msg: '登录成功',
pwdinit: 'false',
time: '2021-08-12 18:14:21',
type: '0',
username: '不醉怎能入睡'
}
而服务端配置的redirecturi
为:https://127.0.0.1:8080/wxqcc
,那么只要url中包含https://127.0.0.1:8080/wxqcc
就可以处理接下来的流程了
最后
以上就是外向小伙为你收集整理的【Electron-Vue】构建桌面应用(42)- 企业微信扫码登录1.前言2.企业微信扫码登陆3.问题4. 更正的全部内容,希望文章能够帮你解决【Electron-Vue】构建桌面应用(42)- 企业微信扫码登录1.前言2.企业微信扫码登陆3.问题4. 更正所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复