由于公司的工作模式都是使用的钉钉软件,所以当客户端开发完之后,就需要在内部使用,边用边测试。所以为了支持钉钉扫码登录,需要下一番功夫研究一下。
钉钉参考文档
考虑到需要支持钉钉,以后还可能要支持企业微信…等,所以界面设计就需要考虑一下,打算通过点击的方式进行页面跳转。见下图,在最下方添加钉钉的链接,以后支持其他第三方登陆,可能就是排成一排的形式了。
钉钉扫码页面,考虑到用户可以取消钉钉登陆方式,提供返回操作,返回到主登陆页面,使用账户/密码登陆。
整体的界面就是这样的,那么下面我们来看一下代码。
对接钉钉前,需要注册团队开发账户,然后创建扫码登陆应用授权
。
https://open-dev.dingtalk.com/#/loginMan
登陆账户后在应用开发->登陆
模块中创建扫码登陆应用授权码
。
官方介绍的都比较详细,需要注意的就是回调域名
。这个会贯穿你的整个扫码的开发过程。
我们知道一般的web端开发都是在网页应用,基本上都是在http/https
下开发的。它指向了你的页面访问路径比如http://localhost:8080/login
等。
配置完成之后,会自动生成appid和appsecret
。
上面的配置完成之后,我们要开始真正的代码coding了。
官方的文档很写的很清楚,我就不详细讲解代码了。下面是我的扫码登陆的代码。
其中,http://localhost:9080/
就是我的回调域名,为什么设置为这个呢?是因为在开发环境下,通过npm start
或者其他命令启动服务,会给我们启动一个服务和监听的端口号,我的服务和端口号如下:
i 「wds」: Project is running at http://localhost:9080/
i 「wds」: webpack output is served from undefined
i 「wds」: Content not from webpack is served from D:\my-project\
所以这里的回调域名配置为我本地起的服务,那么在钉钉重定向的时候就能够找到这个域名,重定向到我的页面。如果你的回调域名不是真实存在的,那么钉钉没法进行重定向,导致你无法获取到钉钉给你返回的code
值。没有code
值,你就没法进行下去。
<template>
<div id="dd_container"></div>
</template>
<script lang="ts">
export default {
name: "scan",
mounted() {
var url = "http://localhost:9080/";
let goto = encodeURIComponent(
"https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoasnoyum0xfxbamoom&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=" +
url
);
// 创建钉钉对象
var obj = DDLogin({
id: "dd_container", //这里需要你在自己的页面定义一个HTML标签并设置id,例如或
goto: goto, //请参考注释里的方式
style: "border:none;background-color:#ffffff;",
width: "250",
height: "290",
});
var handleMessage = function (event) {
var origin = event.origin;
console.log("origin", event.origin);
if (origin == "https://login.dingtalk.com") {
//判断是否来自ddLogin扫码事件。
var loginTmpCode = event.data;
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
console.log("loginTmpCode", loginTmpCode);
window.location.href =
"https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoasnoyum0xfxbamoom&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=http://localhost:9080/&loginTmpCode=" +
loginTmpCode;
}
};
if (typeof window.addEventListener != "undefined") {
window.addEventListener("message", handleMessage, false);
} else if (typeof window.attachEvent != "undefined") {
window.attachEvent("onmessage", handleMessage);
}
},
};
</script>
<style scoped>
#dd_container{
text-align: center;
}
</style>
上面的代码scan
扫码的代码,是在登录页点击钉钉扫码
才显示出来的。你可以在登录页定义一个标志位,默认false
即不显示扫码,点击钉钉扫码
后,置为true
显示扫码,隐藏登录页。做过前端的都能理解,我就不多说了。
配置之后,你启动应用,因为在electron-vue
中使用的npm run dev
启动服务。当你点击时,就会出现二维码。
在开发的时候可能会出现innerhtml
为空或者undefined
的情况,那是因为你的二维码容器dd_container
还没被渲染出来,你的js代码就执行了,导致找不到二维码要渲染的容器,会报这个错误。
当你扫码,手机端确认钉钉授权登陆后,钉钉通过授权就会重定向到你配置的域名回调中,也就是前面说的http://localhost:9080
,然后将code
和STATE
拼接到你的回调域名中,你通过window.location.href
就能获取到这个路径,然后经过一系列的处理,就能拿到code
值。
window.location.href =
"https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoasnoyum0xfxbamoom&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=http://localhost:9080/&loginTmpCode=" +
loginTmpCode;
下面是在开发环境中启动程序时,服务和端口
扫码后,我们看一下,下面这张图是我js端发起的链接跳转
我们可以看到,在钉钉那边授权通过后,会重定向到我配置的域名回调
中,还多了两个参数code
和STATE
。
由于重定向到我的登录页,就相当于登录页main重新渲染了一次,为了方便测试我就直接在vue
的生命周期函数created
中获取了window.location.href
,然后进行一系列的分解操作,获取到code
,将code
发给服务端,服务端拿到code
去钉钉做用户查询,如果能查到用户则成功,否则失败。
看到这里,你可能觉得很顺利。多么的自然…
如果这么简单就实现了,那我写这篇博客就没有什么意思了。
因为我们的应用时客户端,在真实的生产环境和开发环境中千差万别。当我满怀期待的将项目打包安装之后,整个人就呆住了,当我扫码后,一片空白…就跟我当时的脑袋一样。
冷静下来之后,加了打印日志,在一次打包查看一下,恍然大悟。
看到这,就或许就该想到了。客户端使一堆的静态文件,哪来的服务端IP和端口之类的
。而且在代码中也可以看到,下面是main.js
中的代码。
if (process.env.NODE_ENV !== 'development') {
global.__static = path.join(__dirname, '/static').replace(/\\/g, '\\\\')
}
const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080`
: `file://${__dirname}/index.html`
从代码中可以看出来,当你在开发环境时,指向的是http://localhost:9080
,而在生产环境中指向的就是你的本地的安装目录file://${__dirname}/index.html
对应着file:///D:/exe-soft/%E8%94%B7%E8%96%87%E7%81%B5%E5%8A%A8/resources/app.asar/dist/electron/index.html#/
。
WR,这样一来回调域名怎么能匹配的上,匹配不上回调域名,是没法进行重定向的。也就没法拿到对应的code
,那下面的流程全走不下去了。
于是乎我就抱着侥幸的心理去钉钉论坛和electron
论坛,希望能找到些许曙光。但现实是残酷的,很遗憾的一无所获。
于是我就恬不知耻的给钉钉官方提了个工单,不得不说大公司就是好,回复也是超级的快。
大体上的意思是:
我:请问,如果在客户端开发,没有所谓的回调域名,或者说没法配置回调域名我该怎么搞。
钉钉:不好意思,如果没有回调域名,那么后面的一些列的认证流程就走不通,就不支持。
我:那你们有没有支持以函数调用的形式去支持返回code码,因为在整个认证流程中我只关注code码,也只有code码是有用的。比如使用某个函数callback(“xxxxxxxxxx”),这个函数是钉钉定义的,然后又返回值,返回之中带着code值的方式,这种方式能够适应不同的场景。
钉钉:目前不支持,需要做一些…
好吧,看来想让钉钉做出让步去支持不太现实。既然用到被人的东西,就应该遵循别人的设计规则。
从昨天想到现在有这么几种思路:
Electron
中使用iframe
去内嵌一个服务端页面,并且将回调域名指向一个在服务端存在的页面,这样回调域名就能生效了。- 回调域名指向后端服务器
- 既然在开发环境支持,开发环境下是通过ndoejs启动了一个http服务和端口号,那么在生产环境中是不是也可以通过nodejs启动一个服务然后去监听
9080
这个端口,那么只要这个服务存在,重定向的时候就能找到地址,就能获取到code值。
先说说前两种方式,大同小异。难点都在如果你回调域名配置了服务端地址,那么会往服务端请求跳转,服务端需要指向一个页面,这个页面就必须托管在服务器上。否则就会显示一片空白。说白了也就是你需要在服务器上一套前端代码,才能正常显示。但是一旦你托管到服务器上,那么本地的代码实际上就不可控了。所以的操作都将会是操作服务器上的代码。那么electron客户端便不可控。这中间可能还会牵扯到跨域的问题。
第三种方式的实现原理就是:在程序启动时,去创建监听一个9080的端口,这个服务是和我的客户端同时运行的,而且可以在mian.js中执行的,属于代码可控范围。当重定向发生时,我就能监听到这个服务和端口,那么就能拿到所对应的请求连接和参数信息。然后在main.js中通过ipc的方式通信,将数据传递给渲染进程vue,vue监听到主进程发过来的事件,便可以进行处理,将地址,端口号以及获取的code值发给服务端。服务端拿到这些信息区做认证。返回认证结果,根据结果进行跳转到主页面/登录页
。
随之而来的就是另外一个问题:你怎么确定你的服务一定能拿到你所配置的端口呢?要是被占用了,相当于整个程序也就奔溃了。
先来尝试一下,使用nodejs
创建一个服务监听。
在main/index.js
主进程中
import express from 'express'
// 创建一个新的服务监听
appListener.get('/', function (req, res) {
log.info("req.query", req.query)
mainWindow.webContents.send('ddscan-login', req.query)
});
appListener.listen(9080)
ipcMain.on("ddCode-result", (event, args) => {
log.info("钉钉扫码的结果=", args)
event.sender.send("jumptosystem","测试成功了")
})
在渲染进程中:
// 监听扫码结果
this.$electron.ipcRenderer.once("ddscan-login", (event, arg) => {
this.$electron.ipcRenderer.send("ddCode-result", {
address: this.loginVo.address,
port: this.loginVo.port,
code: arg.code,
});
});
打包后的输出结果,你猜我看到了什么…
这是我之前的打印
这样整个流程就通了,然后扫码可以成功后就可以在渲染进程中通过vue的路由功能进行跳转了。
为了防止占用客户端(用户)的端口,所以决定不在前端进行跳转操作。
前端只负责拿到临时授权码
,然后将临时授权码通过请求发给服务端,服务端负责后需要的操作,拿着临时授权码去认证扫码用户,并将扫码结果返回给前端,前端接受到认证结果,做进一步处理。