Forráskód Böngészése

Merge pull request #669 from LawyZheng/master

Able to Login via Dingtalk QR Code
玖亖伍 4 éve
szülő
commit
bae6826a97

+ 6 - 0
conf/app.conf.example

@@ -224,6 +224,12 @@ dingtalk_app_secret="${MINDOC_DINGTALK_APPSECRET}"
 # 钉钉登录默认只读账号
 # 钉钉登录默认只读账号
 dingtalk_tmp_reader="${MINDOC_DINGTALK_READER}"
 dingtalk_tmp_reader="${MINDOC_DINGTALK_READER}"
 
 
+# 钉钉扫码登录Key
+dingtalk_qr_key="${MINDOC_DINGTALK_QRKEY}"
+
+# 钉钉扫码登录Secret
+dingtalk_qr_secret="${MINDOC_DINGTALK_QRSECRET}"
+
 
 
 
 
 
 

+ 79 - 2
controllers/AccountController.go

@@ -33,8 +33,12 @@ func (c *AccountController) referer() string {
 func (c *AccountController) Prepare() {
 func (c *AccountController) Prepare() {
 	c.BaseController.Prepare()
 	c.BaseController.Prepare()
 	c.EnableXSRF = beego.AppConfig.DefaultBool("enablexsrf", true)
 	c.EnableXSRF = beego.AppConfig.DefaultBool("enablexsrf", true)
+
 	c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML())
 	c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML())
 	c.Data["corpID"] = beego.AppConfig.String("dingtalk_corpid")
 	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 {
 	if !c.EnableXSRF {
 		return
 		return
 	}
 	}
@@ -165,14 +169,14 @@ func (c *AccountController) DingTalkLogin() {
 
 
 	userid, err := dingtalkAgent.GetUserIDByCode(code)
 	userid, err := dingtalkAgent.GetUserIDByCode(code)
 	if err != nil {
 	if err != nil {
-		beego.Warn("钉钉自动登录失败 ->", err)
+		beego.Warn("获取钉钉用户ID失败 ->", err)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.StopRun()
 		c.StopRun()
 	}
 	}
 
 
 	username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
 	username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
 	if err != nil {
 	if err != nil {
-		beego.Warn("钉钉自动登录失败 ->", err)
+		beego.Warn("获取钉钉用户信息失败 ->", err)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.StopRun()
 		c.StopRun()
 	}
 	}
@@ -191,6 +195,79 @@ func (c *AccountController) DingTalkLogin() {
 	c.JsonResult(0, "ok", username)
 	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 := 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("获取钉钉临时UnionID失败 ->", 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("获取钉钉用户ID失败 ->", err)
+			c.Redirect(conf.URLFor("AccountController.Login"), 302)
+			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)
+
+	default:
+		c.Redirect(conf.URLFor("AccountController.Login"), 302)
+		c.StopRun()
+	}
+}
+
 // 登录成功后的操作,如重定向到原始请求页面
 // 登录成功后的操作,如重定向到原始请求页面
 func (c *AccountController) LoggedIn(isPost bool) interface{} {
 func (c *AccountController) LoggedIn(isPost bool) interface{} {
 
 

+ 1 - 0
routers/router.go

@@ -10,6 +10,7 @@ func init() {
 
 
 	beego.Router("/login", &controllers.AccountController{}, "*:Login")
 	beego.Router("/login", &controllers.AccountController{}, "*:Login")
 	beego.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
 	beego.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
+	beego.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin")
 	beego.Router("/logout", &controllers.AccountController{}, "*:Logout")
 	beego.Router("/logout", &controllers.AccountController{}, "*:Logout")
 	beego.Router("/register", &controllers.AccountController{}, "*:Register")
 	beego.Router("/register", &controllers.AccountController{}, "*:Register")
 	beego.Router("/find_password", &controllers.AccountController{}, "*:FindPassword")
 	beego.Router("/find_password", &controllers.AccountController{}, "*:FindPassword")

+ 18 - 0
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);

+ 101 - 3
utils/dingtalk/dingtalk.go

@@ -10,6 +10,9 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"strconv"
+	"strings"
+	"time"
 )
 )
 
 
 // DingTalkAgent 用于钉钉交互
 // DingTalkAgent 用于钉钉交互
@@ -106,6 +109,46 @@ func (d *DingTalkAgent) GetUserNameAndAvatarByUserID(userid string) (string, str
 	return username, avatar, nil
 	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
 // GetAccesstoken 获取钉钉请求Token
 func (d *DingTalkAgent) GetAccesstoken() (err error) {
 func (d *DingTalkAgent) GetAccesstoken() (err error) {
 
 
@@ -132,16 +175,71 @@ func (d *DingTalkAgent) GetAccesstoken() (err error) {
 	return errors.New("accesstoken获取错误:" + i["errmsg"].(string))
 	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&timestamp=%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 := hmac.New(sha256.New, []byte(d.AppSecret))
-	h.Write([]byte(message))
+	h.Write([]byte(timestamp))
 	sum := h.Sum(nil) // 二进制流
 	sum := h.Sum(nil) // 二进制流
 	tmpMsg := base64.StdEncoding.EncodeToString(sum)
 	tmpMsg := base64.StdEncoding.EncodeToString(sum)
 
 
 	uv := url.Values{}
 	uv := url.Values{}
 	uv.Add("0", tmpMsg)
 	uv.Add("0", tmpMsg)
-	message = uv.Encode()[2:]
+	message := uv.Encode()[2:]
 
 
 	return message
 	return message
 }
 }

+ 80 - 33
views/account/login.tpl

@@ -70,6 +70,11 @@
                 <div class="form-group">
                 <div class="form-group">
                     <button type="button" id="btn-login" class="btn btn-success" style="width: 100%"  data-loading-text="正在登录..." autocomplete="off">立即登录</button>
                     <button type="button" id="btn-login" class="btn btn-success" style="width: 100%"  data-loading-text="正在登录..." autocomplete="off">立即登录</button>
                 </div>
                 </div>
+                {{if .ENABLE_QR_DINGTALK}}
+                <div class="form-group">
+                    <a id="btn-dingtalk-qr" class="btn btn-default" style="width: 100%" data-loading-text="" autocomplete="off">钉钉扫码登录</a>
+                </div>
+                {{end}}
                 {{if .ENABLED_REGISTER}}
                 {{if .ENABLED_REGISTER}}
                 {{if ne .ENABLED_REGISTER "false"}}
                 {{if ne .ENABLED_REGISTER "false"}}
                 <div class="form-group">
                 <div class="form-group">
@@ -78,6 +83,10 @@
                 {{end}}
                 {{end}}
                 {{end}}
                 {{end}}
             </form>
             </form>
+            <div class="form-group dingtalk-container" style="display: none;">
+                <div id="dingtalk-qr-container"></div>
+                <a class="btn btn-default btn-dingtalk" style="width: 100%" data-loading-text="" autocomplete="off">返回账号密码登录</a>
+            </div>
         </div>
         </div>
     </div>
     </div>
     <div class="clearfix"></div>
     <div class="clearfix"></div>
@@ -87,46 +96,74 @@
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/js/dingtalk-ddlogin.js"}}" type="text/javascript"></script>
+
 <script type="text/javascript">
 <script type="text/javascript">
-    $(function () {
-        if (dd.env.platform !== "notInDingTalk"){
-            dd.ready(function() {
-                dd.runtime.permission.requestAuthCode({
-                    corpId: {{ .corpID }} , // 企业id
-                    onSuccess: function (info) {
-                        var index = layer.load(1, {
-                            shade: [0.1, '#fff'] // 0.1 透明度的白色背景
-                        })
+    if (dd.env.platform !== "notInDingTalk"){
+        dd.ready(function() {
+            dd.runtime.permission.requestAuthCode({
+                corpId: {{ .corpID }} , // 企业id
+                onSuccess: function (info) {
+                    var index = layer.load(1, {
+                        shade: [0.1, '#fff'] // 0.1 透明度的白色背景
+                    })
 
 
-                        var formData = $("form").serializeArray()
-                        formData.push({"name": "dingtalk_code", "value": info.code})
+                    var formData = $("form").serializeArray()
+                    formData.push({"name": "dingtalk_code", "value": info.code})
 
 
-                        $.ajax({
-                            url: "{{urlfor "AccountController.DingTalkLogin"}} ",
-                            data: formData,
-                            dataType: "json",
-                            type: "POST",
-                            complete: function(){
-                                layer.close(index)
-                            },
-                            success: function (res) {
-                                if (res.errcode !== 0) {
-                                    layer.msg(res.message)
-                                } else {
-                                    window.location = "{{ urlfor "HomeController.Index"  }}"
-                                }
-                            },
-                            error: function (res) {
-                                layer.msg("发生异常")
+                    $.ajax({
+                        url: "{{urlfor "AccountController.DingTalkLogin"}} ",
+                        data: formData,
+                        dataType: "json",
+                        type: "POST",
+                        complete: function(){
+                            layer.close(index)
+                        },
+                        success: function (res) {
+                            if (res.errcode !== 0) {
+                                layer.msg(res.message)
+                            } else {
+                                window.location = "{{ urlfor "HomeController.Index"  }}"
                             }
                             }
-                        })
-                    }
-                });
+                        },
+                        error: function (res) {
+                            layer.msg("发生异常")
+                        }
+                    })
+                }
             });
             });
-        }
-    })
+        });
+    }
 
 
 </script>
 </script>
+
+<script type="text/javascript">
+    var url = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid={{.dingtalk_qr_key}}&response_type=code&scope=snsapi_login&state=1&redirect_uri={{ urlfor "AccountController.QRLogin" ":app" "dingtalk"}}'
+    var obj = DDLogin({
+        id:"dingtalk-qr-container",
+        goto: encodeURIComponent(url), 
+        style: "border:none;background-color:#FFFFFF;",
+        width : "338",
+        height: "300"
+    });
+    var handleMessage = function (event) {
+        var origin = event.origin;
+        if( origin == "https://login.dingtalk.com" ) { //判断是否来自ddLogin扫码事件。
+            layer.load(1, { shade: [0.1, '#fff'] })
+            var loginTmpCode = event.data; 
+            //获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
+            console.log("loginTmpCode", loginTmpCode);
+            url = url + "&loginTmpCode=" + loginTmpCode
+            window.location = url
+        }
+    };
+    if (typeof window.addEventListener != 'undefined') {
+        window.addEventListener('message', handleMessage, false);
+    } else if (typeof window.attachEvent != 'undefined') {
+        window.attachEvent('onmessage', handleMessage);
+    }
+</script>
+
 <script type="text/javascript">
 <script type="text/javascript">
     $(function () {
     $(function () {
         $("#account,#password,#code").on('focus', function () {
         $("#account,#password,#code").on('focus', function () {
@@ -140,6 +177,16 @@
             }
             }
         });
         });
 
 
+        $("#btn-dingtalk-qr").on('click', function(){
+            $('form').hide()
+            $(".dingtalk-container").show()
+        })
+
+        $(".btn-dingtalk").on('click', function(){
+            $('form').show()
+            $(".dingtalk-container").hide()
+        })
+
         $("#btn-login").on('click', function () {
         $("#btn-login").on('click', function () {
             $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
             $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
             var $btn = $(this).button('loading');
             var $btn = $(this).button('loading');