Merge branch 'master' into new-beego-path

pull/662/head
roberChen 2021-03-26 10:43:54 +08:00
commit 8b7d56a547
7 changed files with 303 additions and 50 deletions

View File

@ -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}"

View File

@ -34,8 +34,14 @@ func (c *AccountController) referer() string {
func (c *AccountController) Prepare() { func (c *AccountController) Prepare() {
c.BaseController.Prepare() c.BaseController.Prepare()
c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true) c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true)
c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML()) 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 { if !c.EnableXSRF {
return return
} }
@ -166,14 +172,14 @@ func (c *AccountController) DingTalkLogin() {
userid, err := dingtalkAgent.GetUserIDByCode(code) userid, err := dingtalkAgent.GetUserIDByCode(code)
if err != nil { if err != nil {
logs.Warn("钉钉自动登录失败 ->", err) logs.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 {
logs.Warn("钉钉自动登录失败 ->", err) logs.Warn("获取钉钉用户信息失败 ->", err)
c.JsonResult(500, "自动登录失败", nil) c.JsonResult(500, "自动登录失败", nil)
c.StopRun() c.StopRun()
} }
@ -192,6 +198,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, _ := 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{} { func (c *AccountController) LoggedIn(isPost bool) interface{} {

View File

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

View File

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

View File

@ -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);

View File

@ -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
} }

View File

@ -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,8 +96,9 @@
<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"){ if (dd.env.platform !== "notInDingTalk"){
dd.ready(function() { dd.ready(function() {
dd.runtime.permission.requestAuthCode({ dd.runtime.permission.requestAuthCode({
@ -124,9 +134,36 @@
}); });
}); });
} }
})
</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');