From 9b94192d99f3233bef6823a4276961f03dce406e Mon Sep 17 00:00:00 2001 From: LawyZHENG Date: Thu, 25 Mar 2021 15:03:35 +0800 Subject: [PATCH 1/5] backend for dingtalk QR login --- controllers/AccountController.go | 75 ++++++++++++++++++++++ routers/router.go | 1 + utils/dingtalk/dingtalk.go | 104 ++++++++++++++++++++++++++++++- views/account/login.tpl | 4 ++ 4 files changed, 181 insertions(+), 3 deletions(-) diff --git a/controllers/AccountController.go b/controllers/AccountController.go index f5112dd9..5e801c1e 100644 --- a/controllers/AccountController.go +++ b/controllers/AccountController.go @@ -191,6 +191,81 @@ func (c *AccountController) DingTalkLogin() { c.JsonResult(0, "ok", username) } +// QR二维码登录 +func (c *AccountController) QRLogin() { + c.Prepare() + + appName := c.Ctx.Input.Param(":app") + + switch appName { + // 钉钉扫码登录 + case "dingtalk": + code := c.GetString("code") + state := c.GetString("state") + if state != "1" || code == "" { + c.Redirect(conf.URLFor("AccountController.Login"), 302) + c.StopRun() + } + appKey := "dingoa0wp8qg6gyqtlyno1" + appSecret := "YPcijr8Wj47_N2jjEtE3wUsfiUwcTJqfJewgTs9mnHmaHAIsxAe92fmzy-XpdSMs" + + qrDingtalk := dingtalk.NewDingtalkQRLogin(appSecret, appKey) + unionID, err := qrDingtalk.GetUnionIDByCode(code) + if err != nil { + beego.Warn("获取钉钉临时Token失败 ->", err) + c.Redirect(conf.URLFor("AccountController.Login"), 302) + c.StopRun() + } + + appKey = beego.AppConfig.String("dingtalk_app_key") + appSecret = beego.AppConfig.String("dingtalk_app_secret") + tmpReader := beego.AppConfig.String("dingtalk_tmp_reader") + + dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey) + err = dingtalkAgent.GetAccesstoken() + if err != nil { + beego.Warn("获取钉钉临时Token失败 ->", err) + c.Redirect(conf.URLFor("AccountController.Login"), 302) + c.StopRun() + } + + userid, err := dingtalkAgent.GetUserIDByUnionID(unionID) + if err != nil { + beego.Warn("获取钉钉临时Token失败 ->", err) + c.StopRun() + } + + username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) + if err != nil { + beego.Warn("钉钉自动登录失败 ->", err) + c.Redirect(conf.URLFor("AccountController.Login"), 302) + c.StopRun() + } + + member, err := models.NewMember().TmpLogin(tmpReader) + if err == nil { + member.LastLoginTime = time.Now() + _ = member.Update("last_login_time") + member.Account = username + if avatar != "" { + member.Avatar = avatar + } + + c.SetMember(*member) + c.LoggedIn(false) + c.StopRun() + } + c.Redirect(conf.URLFor("AccountController.Login"), 302) + + // fmt.Println(unionID) + // c.JsonResult(0, appName, nil) + + default: + c.Redirect(conf.URLFor("AccountController.Login"), 302) + c.StopRun() + } +} + // 登录成功后的操作,如重定向到原始请求页面 func (c *AccountController) LoggedIn(isPost bool) interface{} { diff --git a/routers/router.go b/routers/router.go index 76ff51b8..d6f4cf36 100644 --- a/routers/router.go +++ b/routers/router.go @@ -10,6 +10,7 @@ func init() { beego.Router("/login", &controllers.AccountController{}, "*:Login") beego.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin") + beego.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin") beego.Router("/logout", &controllers.AccountController{}, "*:Logout") beego.Router("/register", &controllers.AccountController{}, "*:Register") beego.Router("/find_password", &controllers.AccountController{}, "*:FindPassword") diff --git a/utils/dingtalk/dingtalk.go b/utils/dingtalk/dingtalk.go index 53954abe..fa8e6883 100644 --- a/utils/dingtalk/dingtalk.go +++ b/utils/dingtalk/dingtalk.go @@ -10,6 +10,9 @@ import ( "io/ioutil" "net/http" "net/url" + "strconv" + "strings" + "time" ) // DingTalkAgent 用于钉钉交互 @@ -106,6 +109,46 @@ func (d *DingTalkAgent) GetUserNameAndAvatarByUserID(userid string) (string, str return username, avatar, nil } +// GetUserIDByUnionID 根据UnionID获取用户Userid +func (d *DingTalkAgent) GetUserIDByUnionID(unionid string) (string, error) { + urlEndpoint, err := url.Parse("https://oapi.dingtalk.com/topapi/user/getbyunionid") + if err != nil { + return "", err + } + + query := url.Values{} + query.Set("access_token", d.AccessToken) + urlEndpoint.RawQuery = query.Encode() + urlPath := urlEndpoint.String() + + resp, err := http.PostForm(urlPath, url.Values{"unionid": {unionid}}) + if err != nil { + return "", err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + // 解析钉钉返回数据 + var rdata map[string]interface{} + err = json.Unmarshal(body, &rdata) + if err != nil { + return "", err + } + + errcode := rdata["errcode"].(float64) + if errcode != 0 { + return "", errors.New(fmt.Sprintf("登录错误: %.0f, %s", errcode, rdata["errmsg"].(string))) + } + + result := rdata["result"].(map[string]interface{}) + if result["contact_type"].(float64) != 0 { + return "", errors.New("该用户不属于企业内部员工,无法登录。") + } + userid := result["userid"].(string) + return userid, nil +} + // GetAccesstoken 获取钉钉请求Token func (d *DingTalkAgent) GetAccesstoken() (err error) { @@ -132,16 +175,71 @@ func (d *DingTalkAgent) GetAccesstoken() (err error) { return errors.New("accesstoken获取错误:" + i["errmsg"].(string)) } -func (d *DingTalkAgent) encodeSHA256(message string) string { +// DingtalkQRLogin 用于钉钉扫码登录 +type DingtalkQRLogin struct { + AppSecret string + AppKey string +} + +// NewDingtalkQRLogin 构造钉钉扫码登录实例 +func NewDingtalkQRLogin(appSecret, appKey string) DingtalkQRLogin { + return DingtalkQRLogin{ + AppSecret: appSecret, + AppKey: appKey, + } +} + +// GetUnionIDByCode 获取扫码用户UnionID +func (d *DingtalkQRLogin) GetUnionIDByCode(code string) (userid string, err error) { + var resp *http.Response + //服务端通过临时授权码获取授权用户的个人信息 + timestamp := strconv.FormatInt(time.Now().UnixNano()/1000000, 10) // 毫秒时间戳 + signature := d.encodeSHA256(timestamp) // 加密签名 + urlPath := fmt.Sprintf( + "https://oapi.dingtalk.com/sns/getuserinfo_bycode?accessKey=%s×tamp=%s&signature=%s", + d.AppKey, timestamp, signature) + + // 构造请求数据 + param := struct { + Tmp_auth_code string `json:"tmp_auth_code"` + }{code} + paraByte, _ := json.Marshal(param) + paraString := string(paraByte) + + resp, err = http.Post(urlPath, "application/json;charset=UTF-8", strings.NewReader(paraString)) + if err != nil { + return "", err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // 解析钉钉返回数据 + var rdata map[string]interface{} + err = json.Unmarshal(body, &rdata) + if err != nil { + return "", err + } + errcode := rdata["errcode"].(float64) + if errcode != 0 { + return "", errors.New(fmt.Sprintf("登录错误: %.0f, %s", errcode, rdata["errmsg"].(string))) + } + unionid := rdata["user_info"].(map[string]interface{})["unionid"].(string) + return unionid, nil +} + +func (d *DingtalkQRLogin) encodeSHA256(timestamp string) string { // 钉钉签名算法实现 h := hmac.New(sha256.New, []byte(d.AppSecret)) - h.Write([]byte(message)) + h.Write([]byte(timestamp)) sum := h.Sum(nil) // 二进制流 tmpMsg := base64.StdEncoding.EncodeToString(sum) uv := url.Values{} uv.Add("0", tmpMsg) - message = uv.Encode()[2:] + message := uv.Encode()[2:] return message } diff --git a/views/account/login.tpl b/views/account/login.tpl index 04efc2a8..68d3a48a 100644 --- a/views/account/login.tpl +++ b/views/account/login.tpl @@ -70,6 +70,10 @@
+
+ 钉钉扫码登录 + +
{{if .ENABLED_REGISTER}} {{if ne .ENABLED_REGISTER "false"}}
From ae780418b5b0af747bc5653e4ef444548d9b1a30 Mon Sep 17 00:00:00 2001 From: LawyZHENG Date: Thu, 25 Mar 2021 15:17:38 +0800 Subject: [PATCH 2/5] make "dingtalk QR login" configurable --- conf/app.conf.example | 6 ++++++ controllers/AccountController.go | 18 ++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/conf/app.conf.example b/conf/app.conf.example index 3bc5d116..b7ce3b31 100644 --- a/conf/app.conf.example +++ b/conf/app.conf.example @@ -224,6 +224,12 @@ dingtalk_app_secret="${MINDOC_DINGTALK_APPSECRET}" # 钉钉登录默认只读账号 dingtalk_tmp_reader="${MINDOC_DINGTALK_READER}" +# 钉钉扫码登录Key +dingtalk_qr_key="${MINDOC_DINGTALK_QRKEY}" + +# 钉钉扫码登录Secret +dingtalk_qr_secret="${MINDOC_DINGTALK_QRSECRET}" + diff --git a/controllers/AccountController.go b/controllers/AccountController.go index 5e801c1e..16332f1b 100644 --- a/controllers/AccountController.go +++ b/controllers/AccountController.go @@ -165,14 +165,14 @@ func (c *AccountController) DingTalkLogin() { userid, err := dingtalkAgent.GetUserIDByCode(code) if err != nil { - beego.Warn("钉钉自动登录失败 ->", err) + beego.Warn("获取钉钉用户ID失败 ->", err) c.JsonResult(500, "自动登录失败", nil) c.StopRun() } username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) if err != nil { - beego.Warn("钉钉自动登录失败 ->", err) + beego.Warn("获取钉钉用户信息失败 ->", err) c.JsonResult(500, "自动登录失败", nil) c.StopRun() } @@ -206,13 +206,13 @@ func (c *AccountController) QRLogin() { c.Redirect(conf.URLFor("AccountController.Login"), 302) c.StopRun() } - appKey := "dingoa0wp8qg6gyqtlyno1" - appSecret := "YPcijr8Wj47_N2jjEtE3wUsfiUwcTJqfJewgTs9mnHmaHAIsxAe92fmzy-XpdSMs" + appKey := beego.AppConfig.String("dingtalk_qr_key") + appSecret := beego.AppConfig.String("dingtalk_qr_secret") qrDingtalk := dingtalk.NewDingtalkQRLogin(appSecret, appKey) unionID, err := qrDingtalk.GetUnionIDByCode(code) if err != nil { - beego.Warn("获取钉钉临时Token失败 ->", err) + beego.Warn("获取钉钉临时UnionID失败 ->", err) c.Redirect(conf.URLFor("AccountController.Login"), 302) c.StopRun() } @@ -231,13 +231,14 @@ func (c *AccountController) QRLogin() { userid, err := dingtalkAgent.GetUserIDByUnionID(unionID) if err != nil { - beego.Warn("获取钉钉临时Token失败 ->", err) + beego.Warn("获取钉钉用户ID失败 ->", err) + c.Redirect(conf.URLFor("AccountController.Login"), 302) c.StopRun() } username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) if err != nil { - beego.Warn("钉钉自动登录失败 ->", err) + beego.Warn("获取钉钉用户信息失败 ->", err) c.Redirect(conf.URLFor("AccountController.Login"), 302) c.StopRun() } @@ -257,9 +258,6 @@ func (c *AccountController) QRLogin() { } c.Redirect(conf.URLFor("AccountController.Login"), 302) - // fmt.Println(unionID) - // c.JsonResult(0, appName, nil) - default: c.Redirect(conf.URLFor("AccountController.Login"), 302) c.StopRun() From c768845a455e18147f4159a8b67f1f73f6223b75 Mon Sep 17 00:00:00 2001 From: LawyZHENG Date: Thu, 25 Mar 2021 15:31:24 +0800 Subject: [PATCH 3/5] "dingtalk QR login" button auto appears when dingtalk_corpid is set --- controllers/AccountController.go | 3 +++ views/account/login.tpl | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/controllers/AccountController.go b/controllers/AccountController.go index 16332f1b..2d3e34b1 100644 --- a/controllers/AccountController.go +++ b/controllers/AccountController.go @@ -33,8 +33,11 @@ func (c *AccountController) referer() string { func (c *AccountController) Prepare() { c.BaseController.Prepare() c.EnableXSRF = beego.AppConfig.DefaultBool("enablexsrf", true) + c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML()) c.Data["corpID"] = beego.AppConfig.String("dingtalk_corpid") + c.Data["ENABLE_QR_DINGTALK"] = (beego.AppConfig.String("dingtalk_corpid") != "") + if !c.EnableXSRF { return } diff --git a/views/account/login.tpl b/views/account/login.tpl index 68d3a48a..a7123fd1 100644 --- a/views/account/login.tpl +++ b/views/account/login.tpl @@ -70,10 +70,11 @@
+ {{if .ENABLE_QR_DINGTALK}} + {{end}} {{if .ENABLED_REGISTER}} {{if ne .ENABLED_REGISTER "false"}}
From 5404db07eb6096c8b237b9c9b5f6abdfc499e135 Mon Sep 17 00:00:00 2001 From: LawyZHENG Date: Thu, 25 Mar 2021 16:30:12 +0800 Subject: [PATCH 4/5] "dingtalk QR login" frontend --- controllers/AccountController.go | 1 + static/js/dingtalk-ddlogin.js | 18 +++++++++++++ views/account/login.tpl | 46 +++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 static/js/dingtalk-ddlogin.js diff --git a/controllers/AccountController.go b/controllers/AccountController.go index 2d3e34b1..f7e1f08b 100644 --- a/controllers/AccountController.go +++ b/controllers/AccountController.go @@ -37,6 +37,7 @@ func (c *AccountController) Prepare() { c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML()) c.Data["corpID"] = beego.AppConfig.String("dingtalk_corpid") c.Data["ENABLE_QR_DINGTALK"] = (beego.AppConfig.String("dingtalk_corpid") != "") + c.Data["dingtalk_qr_key"] = beego.AppConfig.String("dingtalk_qr_key") if !c.EnableXSRF { return diff --git a/static/js/dingtalk-ddlogin.js b/static/js/dingtalk-ddlogin.js new file mode 100644 index 00000000..f62433be --- /dev/null +++ b/static/js/dingtalk-ddlogin.js @@ -0,0 +1,18 @@ +!function (window, document) { + function d(a) { + var e, c = document.createElement("iframe"), + d = "https://login.dingtalk.com/login/qrcode.htm?goto=" + a.goto ; + d += a.style ? "&style=" + encodeURIComponent(a.style) : "", + d += a.href ? "&href=" + a.href : "", + c.src = d, + c.frameBorder = "0", + c.allowTransparency = "true", + c.scrolling = "no", + c.width = a.width ? a.width + 'px' : "365px", + c.height = a.height ? a.height + 'px' : "400px", + e = document.getElementById(a.id), + e.innerHTML = "", + e.appendChild(c) + } + window.DDLogin = d +}(window, document); \ No newline at end of file diff --git a/views/account/login.tpl b/views/account/login.tpl index a7123fd1..2129f7b5 100644 --- a/views/account/login.tpl +++ b/views/account/login.tpl @@ -72,7 +72,7 @@
{{if .ENABLE_QR_DINGTALK}} {{end}} {{if .ENABLED_REGISTER}} @@ -83,6 +83,10 @@ {{end}} {{end}} +
@@ -92,6 +96,8 @@ + + + + +