Browse Source

Merge branch 'master' into new-beego-path

roberChen 4 năm trước cách đây
mục cha
commit
8b7d56a547

+ 6 - 0
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}"
+
 
 
 

+ 82 - 3
controllers/AccountController.go

@@ -34,8 +34,14 @@ func (c *AccountController) referer() string {
 func (c *AccountController) Prepare() {
 	c.BaseController.Prepare()
 	c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true)
+
 	c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML())
-	c.Data["corpID"],_ = web.AppConfig.String("dingtalk_corpid")
+	c.Data["corpID"], _ = web.AppConfig.String("dingtalk_corpid")
+	if dtcorpid, _ := web.AppConfig.String("dingtalk_corpid"); dtcorpid != "" {
+		c.Data["ENABLE_QR_DINGTALK"] = true
+	}
+	c.Data["dingtalk_qr_key"], _ = web.AppConfig.String("dingtalk_qr_key")
+
 	if !c.EnableXSRF {
 		return
 	}
@@ -166,14 +172,14 @@ func (c *AccountController) DingTalkLogin() {
 
 	userid, err := dingtalkAgent.GetUserIDByCode(code)
 	if err != nil {
-		logs.Warn("钉钉自动登录失败 ->", err)
+		logs.Warn("获取钉钉用户ID失败 ->", err)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.StopRun()
 	}
 
 	username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
 	if err != nil {
-		logs.Warn("钉钉自动登录失败 ->", err)
+		logs.Warn("获取钉钉用户信息失败 ->", err)
 		c.JsonResult(500, "自动登录失败", nil)
 		c.StopRun()
 	}
@@ -192,6 +198,79 @@ 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, _ := web.AppConfig.String("dingtalk_qr_key")
+		appSecret, _ := web.AppConfig.String("dingtalk_qr_secret")
+
+		qrDingtalk := dingtalk.NewDingtalkQRLogin(appSecret, appKey)
+		unionID, err := qrDingtalk.GetUnionIDByCode(code)
+		if err != nil {
+			logs.Warn("获取钉钉临时UnionID失败 ->", err)
+			c.Redirect(conf.URLFor("AccountController.Login"), 302)
+			c.StopRun()
+		}
+
+		appKey, _ = web.AppConfig.String("dingtalk_app_key")
+		appSecret, _ = web.AppConfig.String("dingtalk_app_secret")
+		tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader")
+
+		dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey)
+		err = dingtalkAgent.GetAccesstoken()
+		if err != nil {
+			logs.Warn("获取钉钉临时Token失败 ->", err)
+			c.Redirect(conf.URLFor("AccountController.Login"), 302)
+			c.StopRun()
+		}
+
+		userid, err := dingtalkAgent.GetUserIDByUnionID(unionID)
+		if err != nil {
+			logs.Warn("获取钉钉用户ID失败 ->", err)
+			c.Redirect(conf.URLFor("AccountController.Login"), 302)
+			c.StopRun()
+		}
+
+		username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
+		if err != nil {
+			logs.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{} {
 

+ 14 - 10
models/DocumentModel.go

@@ -304,24 +304,28 @@ func (item *Document) Processor() *Document {
 				//处理文档结尾信息
 				docCreator, err := NewMember().Find(item.MemberId, "real_name", "account")
 				release := "<div class=\"wiki-bottom\">"
+
+				release += "作者:"
+				if err == nil && docCreator != nil {
+					if docCreator.RealName != "" {
+						release += docCreator.RealName
+					} else {
+						release += docCreator.Account
+					}
+				}
+				release += " &nbsp;创建时间:" + item.CreateTime.Local().Format("2006-01-02 15:04") + "<br>"
+
 				if item.ModifyAt > 0 {
 					docModify, err := NewMember().Find(item.ModifyAt, "real_name", "account")
 					if err == nil {
 						if docModify.RealName != "" {
-							release += "最后编辑: " + docModify.RealName + " &nbsp;"
+							release += "最后编辑:" + docModify.RealName
 						} else {
-							release += "最后编辑: " + docModify.Account + " &nbsp;"
+							release += "最后编辑:" + docModify.Account
 						}
 					}
 				}
-				release += "文档更新时间: " + item.ModifyTime.Local().Format("2006-01-02 15:04") + " &nbsp;&nbsp;作者:"
-				if err == nil && docCreator != nil {
-					if docCreator.RealName != "" {
-						release += docCreator.RealName
-					} else {
-						release += docCreator.Account
-					}
-				}
+				release += " &nbsp;更新时间:" + item.ModifyTime.Local().Format("2006-01-02 15:04") + "<br>"
 				release += "</div>"
 
 				if selector := docQuery.Find("div.markdown-article").First(); selector.Size() > 0 {

+ 1 - 0
routers/router.go

@@ -10,6 +10,7 @@ func init() {
 
 	web.Router("/login", &controllers.AccountController{}, "*:Login")
 	web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
+	web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin")
 	web.Router("/logout", &controllers.AccountController{}, "*:Logout")
 	web.Router("/register", &controllers.AccountController{}, "*:Register")
 	web.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"
 	"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&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.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
 }

+ 80 - 33
views/account/login.tpl

@@ -70,6 +70,11 @@
                 <div class="form-group">
                     <button type="button" id="btn-login" class="btn btn-success" style="width: 100%"  data-loading-text="正在登录..." autocomplete="off">立即登录</button>
                 </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 ne .ENABLED_REGISTER "false"}}
                 <div class="form-group">
@@ -78,6 +83,10 @@
                 {{end}}
                 {{end}}
             </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 class="clearfix"></div>
@@ -87,46 +96,74 @@
 <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/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
+<script src="{{cdnjs "/static/js/dingtalk-ddlogin.js"}}" type="text/javascript"></script>
+
 <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 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">
     $(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 () {
             $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
             var $btn = $(this).button('loading');