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 a40b7487..ff126c81 100644 --- a/controllers/AccountController.go +++ b/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{} { diff --git a/models/DocumentModel.go b/models/DocumentModel.go index 4de63889..4ed46c7a 100644 --- a/models/DocumentModel.go +++ b/models/DocumentModel.go @@ -304,17 +304,8 @@ func (item *Document) Processor() *Document { //处理文档结尾信息 docCreator, err := NewMember().Find(item.MemberId, "real_name", "account") release := "
" - if item.ModifyAt > 0 { - docModify, err := NewMember().Find(item.ModifyAt, "real_name", "account") - if err == nil { - if docModify.RealName != "" { - release += "最后编辑: " + docModify.RealName + "  " - } else { - release += "最后编辑: " + docModify.Account + "  " - } - } - } - release += "文档更新时间: " + item.ModifyTime.Local().Format("2006-01-02 15:04") + "   作者:" + + release += "作者:" if err == nil && docCreator != nil { if docCreator.RealName != "" { release += docCreator.RealName @@ -322,6 +313,19 @@ func (item *Document) Processor() *Document { release += docCreator.Account } } + release += "  创建时间:" + item.CreateTime.Local().Format("2006-01-02 15:04") + "
" + + if item.ModifyAt > 0 { + docModify, err := NewMember().Find(item.ModifyAt, "real_name", "account") + if err == nil { + if docModify.RealName != "" { + release += "最后编辑:" + docModify.RealName + } else { + release += "最后编辑:" + docModify.Account + } + } + } + release += "  更新时间:" + item.ModifyTime.Local().Format("2006-01-02 15:04") + "
" release += "
" if selector := docQuery.Find("div.markdown-article").First(); selector.Size() > 0 { diff --git a/routers/router.go b/routers/router.go index fc18d297..0335e62e 100644 --- a/routers/router.go +++ b/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") 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/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..0a34c379 100644 --- a/views/account/login.tpl +++ b/views/account/login.tpl @@ -70,6 +70,11 @@
+ {{if .ENABLE_QR_DINGTALK}} +
+ 钉钉扫码登录 +
+ {{end}} {{if .ENABLED_REGISTER}} {{if ne .ENABLED_REGISTER "false"}}
@@ -78,6 +83,10 @@ {{end}} {{end}} +
@@ -87,46 +96,74 @@ + + + + +