From 08d0e1613d808dbc13587cc8e8da097e8bfb4bfd Mon Sep 17 00:00:00 2001 From: LawyZheng Date: Thu, 20 Apr 2023 13:24:28 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99Auth2.0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20(#851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * go mod update * feat: change to new wxwork sso login * fix: can't log in by workwx browser * fix: workwx auto regist * fix: change app.conf.example * fix: workwx account can't be disabled * fix: workwx account delete * fix: workwx bind error * feat: optimize wecom login * feat: rewrite dingtalk login * feat: rewrite dingtalk login * feat: optimize auth2 login --- commands/command.go | 1 + conf/app.conf.example | 12 - conf/lang/en-us.ini | 4 +- conf/lang/zh-cn.ini | 4 +- conf/workweixin.go | 32 +- controllers/AccountController.go | 1461 +++++++++++------ controllers/SettingController.go | 3 + models/Auth2Account.go | 185 +++ models/Member.go | 32 +- models/WorkWeixinAccount.go | 74 - routers/router.go | 13 +- utils/auth2/auth2.go | 89 + utils/auth2/dingtalk/dingtalk.go | 234 +++ utils/auth2/wecom/wecom.go | 285 ++++ utils/workweixin/workweixin.go | 124 +- ...-login-callback.tpl => auth2_callback.tpl} | 8 +- views/account/login.tpl | 166 +- 17 files changed, 1936 insertions(+), 791 deletions(-) create mode 100644 models/Auth2Account.go delete mode 100644 models/WorkWeixinAccount.go create mode 100644 utils/auth2/auth2.go create mode 100644 utils/auth2/dingtalk/dingtalk.go create mode 100644 utils/auth2/wecom/wecom.go rename views/account/{workweixin-login-callback.tpl => auth2_callback.tpl} (96%) diff --git a/commands/command.go b/commands/command.go index 7097b46b..74555463 100644 --- a/commands/command.go +++ b/commands/command.go @@ -138,6 +138,7 @@ func RegisterModel() { new(models.Itemsets), new(models.Comment), new(models.WorkWeixinAccount), + new(models.DingTalkAccount), ) gob.Register(models.Blog{}) gob.Register(models.Document{}) diff --git a/conf/app.conf.example b/conf/app.conf.example index e617faf6..109a7a8b 100644 --- a/conf/app.conf.example +++ b/conf/app.conf.example @@ -232,15 +232,6 @@ dingtalk_app_key="${MINDOC_DINGTALK_APPKEY}" # 钉钉AppSecret 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}" - ########企业微信登录配置############## # 企业ID @@ -252,8 +243,5 @@ workweixin_agentid="${MINDOC_WORKWEIXIN_AGENTID}" # 应用密钥 workweixin_secret="${MINDOC_WORKWEIXIN_SECRET}" -# 通讯录密钥 -workweixin_contact_secret="${MINDOC_WORKWEIXIN_CONTACT_SECRET}" - # i18n config default_lang="zh-cn" diff --git a/conf/lang/en-us.ini b/conf/lang/en-us.ini index a09a7630..e0e84910 100644 --- a/conf/lang/en-us.ini +++ b/conf/lang/en-us.ini @@ -22,7 +22,9 @@ captcha = Captcha keep_login = Stay signed in forgot_password = Forgot password? register = Create New Account -dingtalk_login = DingTalk QrCode login +third_party_login = Third Party Login +dingtalk_login = DingTalk Login +wecom_login = WeCom Login account_recovery = Account recovery new_password = New password confirm_password = Confirm password diff --git a/conf/lang/zh-cn.ini b/conf/lang/zh-cn.ini index eb2522f5..897892c3 100644 --- a/conf/lang/zh-cn.ini +++ b/conf/lang/zh-cn.ini @@ -22,7 +22,9 @@ captcha = 验证码 keep_login = 保持登录 forgot_password = 忘记密码? register = 立即注册 -dingtalk_login = 扫码登录 +third_party_login = 第三方登录 +dingtalk_login = 钉钉登录 +wecom_login = 企业微信登录 account_recovery = 找回密码 new_password = 新密码 confirm_password = 确认密码 diff --git a/conf/workweixin.go b/conf/workweixin.go index bae128f7..36f4da03 100644 --- a/conf/workweixin.go +++ b/conf/workweixin.go @@ -1,27 +1,27 @@ package conf import ( - "github.com/beego/beego/v2/server/web" + "github.com/beego/beego/v2/server/web" ) type WorkWeixinConf struct { - CorpId string // 企业ID - AgentId string // 应用ID - Secret string // 应用密钥 - ContactSecret string // 通讯录密钥 + CorpId string // 企业ID + AgentId string // 应用ID + Secret string // 应用密钥 + // ContactSecret string // 通讯录密钥 } func GetWorkWeixinConfig() *WorkWeixinConf { - corpid, _ := web.AppConfig.String("workweixin_corpid") - agentid, _ := web.AppConfig.String("workweixin_agentid") - secret, _ := web.AppConfig.String("workweixin_secret") - contact_secret, _ := web.AppConfig.String("workweixin_contact_secret") + corpid, _ := web.AppConfig.String("workweixin_corpid") + agentid, _ := web.AppConfig.String("workweixin_agentid") + secret, _ := web.AppConfig.String("workweixin_secret") + // contact_secret, _ := web.AppConfig.String("workweixin_contact_secret") - c := &WorkWeixinConf{ - CorpId: corpid, - AgentId: agentid, - Secret: secret, - ContactSecret: contact_secret, - } - return c + c := &WorkWeixinConf{ + CorpId: corpid, + AgentId: agentid, + Secret: secret, + // ContactSecret: contact_secret, + } + return c } diff --git a/controllers/AccountController.go b/controllers/AccountController.go index 50852c30..57c9bcbb 100644 --- a/controllers/AccountController.go +++ b/controllers/AccountController.go @@ -1,14 +1,18 @@ package controllers import ( + "context" "encoding/json" - "fmt" + "errors" + "github.com/mindoc-org/mindoc/cache" + "github.com/mindoc-org/mindoc/utils/auth2" + "github.com/mindoc-org/mindoc/utils/auth2/dingtalk" + "github.com/mindoc-org/mindoc/utils/auth2/wecom" "html/template" "math/rand" + "net/http" "net/url" - "reflect" "regexp" - "strconv" "strings" "time" @@ -21,14 +25,11 @@ import ( "github.com/mindoc-org/mindoc/mail" "github.com/mindoc-org/mindoc/models" "github.com/mindoc-org/mindoc/utils" - "github.com/mindoc-org/mindoc/utils/dingtalk" - "github.com/mindoc-org/mindoc/utils/workweixin" ) const ( - WorkWeixin_AuthorizeUrlBase = "https://open.weixin.qq.com/connect/oauth2/authorize" - WorkWeixin_QRConnectUrlBase = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect" - SessionUserInfoKey = "session-user-info-key" + SessionUserInfoKey = "session-user-info-key" + AccessTokenCacheKey = "access-token-cache-key" ) var src = rand.New(rand.NewSource(time.Now().UnixNano())) @@ -46,7 +47,7 @@ func (c *AccountController) referer() string { return u } -func (c *AccountController) IsInWorkWeixin() (is_in_workweixin bool) { +func (c *AccountController) IsInWorkWeixin() bool { ua := c.Ctx.Input.UserAgent() var wechatRule = regexp.MustCompile(`\bMicroMessenger\/\d+(\.\d+)*\b`) var wxworkRule = regexp.MustCompile(`\bwxwork\/\d+(\.\d+)*\b`) @@ -58,15 +59,8 @@ func (c *AccountController) Prepare() { c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true) c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML()) - c.Data["CanLoginWorkWeixin"] = len(web.AppConfig.DefaultString("workweixin_corpid", "")) > 0 - - c.Data["corpID"], _ = web.AppConfig.String("dingtalk_corpid") - c.Data["CanLoginDingTalk"] = len(web.AppConfig.DefaultString("dingtalk_corpid", "")) > 0 - if reflect.ValueOf(c.Data["CanLoginDingTalk"]).Bool() { - c.Data["ENABLE_QR_DINGTALK"] = true - } - c.Data["dingtalk_qr_key"], _ = web.AppConfig.String("dingtalk_qr_key") + c.Data["CanLoginDingTalk"] = len(web.AppConfig.DefaultString("dingtalk_app_key", "")) > 0 if !c.EnableXSRF { return @@ -163,196 +157,175 @@ func (c *AccountController) Login() { logs.Error("用户登录 ->", err) c.JsonResult(500, i18n.Tr(c.Lang, "message.wrong_account_password"), nil) } + return + } + + referer := c.referer() + u := c.GetString("url") + if u == "" { + u = referer + if u == "" { + u = conf.BaseUrl + } } else { - // 默认登录方式 - login_method := "AccountController.Login" - var redirect_uri string - // 企业微信登录检查 - canLoginWorkWeixin := reflect.ValueOf(c.Data["CanLoginWorkWeixin"]).Bool() - referer := c.referer() - if canLoginWorkWeixin { - // 企业微信登录方式 - login_method = "AccountController.WorkWeixinLogin" - u := c.GetString("url") - if u == "" { - u = referer - if u == "" { - u = conf.BaseUrl - } - } else { - var schemaRule = regexp.MustCompile(`^https?\:\/\/`) - if !schemaRule.MatchString(u) { - u = conf.BaseUrl + u - } - } - redirect_uri = conf.URLFor(login_method, "url", url.PathEscape(u)) - // 是否在企业微信内部打开 - isInWorkWeixin := c.IsInWorkWeixin() - c.Data["IsInWorkWeixin"] = isInWorkWeixin - if isInWorkWeixin { - // 客户端拥有微信标识和企业微信标识 - c.Redirect(redirect_uri, 302) - return - } else { - c.Data["workweixin_login_url"] = redirect_uri - } - } - c.Data["url"] = referer - } -} - -// 钉钉登录 -func (c *AccountController) DingTalkLogin() { - code := c.GetString("dingtalk_code") - if code == "" { - c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_obtain_user_info"), nil) - } - - appKey, _ := web.AppConfig.String("dingtalk_app_key") - appSecret, _ := web.AppConfig.String("dingtalk_app_secret") - tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader") - - if appKey == "" || appSecret == "" || tmpReader == "" { - c.JsonResult(500, i18n.Tr(c.Lang, "message.dingtalk_auto_login_not_enable"), nil) - c.StopRun() - } - - dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey) - err := dingtalkAgent.GetAccesstoken() - if err != nil { - logs.Warn("获取钉钉临时Token失败 ->", err) - c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) - c.StopRun() - } - - userid, err := dingtalkAgent.GetUserIDByCode(code) - if err != nil { - logs.Warn("获取钉钉用户ID失败 ->", err) - c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) - c.StopRun() - } - - username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) - if err != nil { - logs.Warn("获取钉钉用户信息失败 ->", err) - c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) - 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.JsonResult(0, "ok", username) -} - -// WorkWeixinLogin 用户企业微信登录 -func (c *AccountController) WorkWeixinLogin() { - logs.Info("UserAgent: ", c.Ctx.Input.UserAgent()) // debug - - if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { - u := c.GetString("url") - if u == "" { - u = c.Ctx.Request.Header.Get("Referer") - if u == "" { - u = conf.URLFor("HomeController.Index") - } - } - // session自动登录时刷新session内容 - member, err := models.NewMember().Find(member.MemberId) - if err != nil { - c.DelSession(conf.LoginSessionName) - c.SetMember(models.Member{}) - c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) - } else { - c.SetMember(*member) - } - c.Redirect(u, 302) - } - var remember CookieRemember - // 如果 Cookie 中存在登录信息 - if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { - if err := utils.Decode(cookie, &remember); err == nil { - if member, err := models.NewMember().Find(remember.MemberId); err == nil { - c.SetMember(*member) - c.LoggedIn(false) - c.StopRun() - } + var schemaRule = regexp.MustCompile(`^https?\:\/\/`) + if !schemaRule.MatchString(u) { + u = conf.BaseUrl + u } } + c.Data["url"] = referer - if c.Ctx.Input.IsPost() { - // account := c.GetString("account") - // password := c.GetString("password") - // captcha := c.GetString("code") - // isRemember := c.GetString("is_remember") - c.JsonResult(400, "request method not allowed", nil) - } else { - var callback_u string - u := c.GetString("url") - if u == "" { - u = c.referer() - } - if u != "" { - var schemaRule = regexp.MustCompile(`^https?\:\/\/`) - if !schemaRule.MatchString(u) { - u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/") - } - } - if u == "" { - callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback") - } else { - callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback", "url", url.PathEscape(u)) - } - logs.Info("callback_u: ", callback_u) // debug - - state := "mindoc" - workweixinConf := conf.GetWorkWeixinConfig() - appid := workweixinConf.CorpId - agentid := workweixinConf.AgentId - var redirect_uri string - - isInWorkWeixin := c.IsInWorkWeixin() - c.Data["IsInWorkWeixin"] = isInWorkWeixin - if isInWorkWeixin { - // 企业微信内-网页授权登录 - urlFmt := "%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect" - redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_AuthorizeUrlBase, appid, url.PathEscape(callback_u), state) - } else { - // 浏览器内-扫码授权登录 - urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&state=%s" - redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_QRConnectUrlBase, appid, agentid, url.PathEscape(callback_u), state) - } - logs.Info("redirect_uri: ", redirect_uri) // debug - c.Redirect(redirect_uri, 302) + auth2Redirect := "AccountController.Auth2Redirect" + if can, _ := c.Data["CanLoginWorkWeixin"].(bool); can { + c.Data["workweixin_login_url"] = conf.URLFor(auth2Redirect, ":app", wecom.AppName, "url", url.PathEscape(u)) } + + if can, _ := c.Data["CanLoginDingTalk"].(bool); can { + c.Data["dingtalk_login_url"] = conf.URLFor(auth2Redirect, ":app", dingtalk.AppName, "url", url.PathEscape(u)) + + } + return } /* -思路: -1. 浏览器打开 - 用户名+密码 登录 与企业微信没有交集 - 手机企业微信登录->扫码页面->扫码后获取用户信息, 判断是否绑定了企业微信 - 已绑定,则读取用户信息,直接登录 - 未绑定,则弹窗提示[未绑定企业微信,请先在企业微信中打开,完成绑定] -2. 企业微信打开->自动登录->判断是否绑定了企业微信 - 已绑定,则读取用户信息,直接登录 - 未绑定,则弹窗提示 - 是否已有账户(用户名+密码方式) - 有: 弹窗输入[用户名+密码+验证码]校验 - 无: 直接以企业UserId作为用户名(小写),创建随机密码 +Auth2.0 第三方对接思路: +1. Auth2Redirect: 点击相应第三方接口,路由重定向至第三方提供的Auth2.0地址 +2. Auth2Callback: 第三方回调处理,接收回调的授权码,并获取用户信息 + 已绑定: 则读取用户信息,直接登录 + 未绑定: 则弹窗提示(需要敏感信息) + a) Auth2BindAccount: 绑定已有账户(用户名+密码) + b) Auth2AutoAccount: 自动创建账户,以第三方用户ID作为用户名,密码123456。 + 用该方式创建的账户,无法使用账号密码登录,需要修改一次密码后才可以进行账号密码登录。 */ -// WorkWeixinLoginCallback 用户企业微信登录-回调 -func (c *AccountController) WorkWeixinLoginCallback() { - c.TplName = "account/workweixin-login-callback.tpl" +func (c *AccountController) getAuth2Client() (auth2.Client, error) { + app := c.Ctx.Input.Param(":app") + var client auth2.Client + tokenKey := AccessTokenCacheKey + "-" + app + + switch app { + case wecom.AppName: + if can, _ := c.Data["CanLoginWorkWeixin"].(bool); !can { + return nil, errors.New("auth2.client.wecom.disabled") + } + corpId, _ := web.AppConfig.String("workweixin_corpid") + agentId, _ := web.AppConfig.String("workweixin_agentid") + secret, _ := web.AppConfig.String("workweixin_secret") + client = wecom.NewClient(corpId, agentId, secret) + + case dingtalk.AppName: + if can, _ := c.Data["CanLoginDingTalk"].(bool); !can { + return nil, errors.New("auth2.client.dingtalk.disabled") + } + + appKey, _ := web.AppConfig.String("dingtalk_app_key") + appSecret, _ := web.AppConfig.String("dingtalk_app_secret") + client = dingtalk.NewClient(appSecret, appKey) + + default: + return nil, errors.New("auth2.client.notsupported") + } + + var tokenCache auth2.AccessTokenCache + err := cache.Get(tokenKey, &tokenCache) + if err != nil { + logs.Info("AccessToken从缓存读取失败") + token, err := client.GetAccessToken(context.Background()) + if err != nil { + return client, nil + } + tokenCache = auth2.NewAccessToken(token) + cache.Put(tokenKey, tokenCache, tokenCache.GetExpireIn()) + } + + // 处理过期Token + if tokenCache.IsExpired() { + token, err := client.GetAccessToken(context.Background()) + if err != nil { + return client, nil + } + tokenCache = auth2.NewAccessToken(token) + cache.Put(tokenKey, tokenCache, tokenCache.GetExpireIn()) + } + + client.SetAccessToken(tokenCache) + return client, nil +} + +func (c *AccountController) parseAuth2CallbackParam() (code, state string) { + switch c.Ctx.Input.Param(":app") { + case wecom.AppName: + code = c.GetString("code") + state = c.GetString("state") + case dingtalk.AppName: + code = c.GetString("authCode") + state = c.GetString("state") + } + + logs.Debug("code: ", code) + logs.Debug("state: ", state) + return +} + +func (c *AccountController) getAuth2Account() (models.Auth2Account, error) { + switch c.Ctx.Input.Param(":app") { + case wecom.AppName: + return models.NewWorkWeixinAccount(), nil + + case dingtalk.AppName: + return models.NewDingTalkAccount(), nil + } + + return nil, errors.New("auth2.account.notsupported") +} + +// Auth2Redirect 第三方auth2.0登录: 钉钉、企业微信 +func (c *AccountController) Auth2Redirect() { + client, err := c.getAuth2Client() + if err != nil { + c.DelSession(conf.LoginSessionName) + c.SetMember(models.Member{}) + c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) + c.StopRun() + return + } + + app := c.Ctx.Input.Param(":app") + var isAppBrowser bool + switch app { + case wecom.AppName: + isAppBrowser = c.IsInWorkWeixin() + } + + var callback string + u := c.GetString("url") + if u == "" { + u = c.referer() + callback = conf.URLFor("AccountController.Auth2Callback", ":app", app) + } + if u != "" { + var schemaRule = regexp.MustCompile(`^https?\:\/\/`) + if !schemaRule.MatchString(u) { + u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/") + } + callback = conf.URLFor("AccountController.Auth2Callback", ":app", app, "url", url.PathEscape(u)) + } + + logs.Debug("callback: ", callback) // debug + c.Redirect(client.BuildURL(callback, isAppBrowser), http.StatusFound) +} + +// Auth2Callback 第三方auth2.0回调 +func (c *AccountController) Auth2Callback() { + client, err := c.getAuth2Client() + if err != nil { + c.DelSession(conf.LoginSessionName) + c.SetMember(models.Member{}) + c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) + c.StopRun() + logs.Error(err) + return + } if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { u := c.GetString("url") @@ -385,359 +358,781 @@ func (c *AccountController) WorkWeixinLoginCallback() { } } + c.TplName = "account/auth2_callback.tpl" + bindExisted := "false" + errMsg := "" + userInfoJson := "{}" + defer func() { + c.Data["bind_existed"] = template.JS(bindExisted) + logs.Debug("bind_existed: ", bindExisted) + c.Data["error_msg"] = template.JS(errMsg) + c.Data["user_info_json"] = template.JS(userInfoJson) + c.Data["app"] = template.JS(c.Ctx.Input.Param(":app")) + }() + // 请求参数获取 - req_code := c.GetString("code") - logs.Warning("req_code: ", req_code) - req_state := c.GetString("state") - logs.Warning("req_state: ", req_state) - var user_info_json string - var error_msg string - var bind_existed string - if len(req_code) > 0 && req_state == "mindoc" { - // 获取当前应用的access_token - access_token, ok := workweixin.GetAccessToken(false) - if ok { - logs.Warning("access_token: ", access_token) - // 获取当前请求的userid - user_id, ok := workweixin.RequestUserId(access_token, req_code) - if ok { - logs.Warning("user_id: ", user_id) - // 获取通讯录应用的access_token - contact_access_token, ok := workweixin.GetAccessToken(true) - if ok { - logs.Warning("contact_access_token: ", contact_access_token) - // 获取用户信息 - //user_info, err_msg, ok := workweixin.RequestUserInfo(contact_access_token, user_id) - // 获取用户id 列表 - user_info, err_msg, ok := workweixin.GetUserListId(contact_access_token, user_id) - if ok { - // [-------所有字段-Debug---------- - // user_info.UserId - // user_info.Name - // user_info.HideMobile - // user_info.Mobile - // user_info.Department - // user_info.Email - // user_info.IsLeaderInDept - // user_info.IsLeader - // user_info.Avatar - // user_info.Alias - // user_info.Status - // user_info.MainDepartment - // -----------------------------] - // logs.Debug("user_info.UserId: ", user_info.UserId) - // logs.Debug("user_info.Name: ", user_info.Name) - json_info, _ := json.Marshal(user_info) - user_info_json = string(json_info) - // 查询系统现有数据,是否绑定了当前请求用户的企业微信 - member, err := models.NewWorkWeixinAccount().ExistedMember(user_info.UserId) - if err == nil { - member.LastLoginTime = time.Now() - _ = member.Update("last_login_time") + code, state := c.parseAuth2CallbackParam() + if err := client.ValidateCallback(state); err != nil { + c.DelSession(conf.LoginSessionName) + c.SetMember(models.Member{}) + c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) + errMsg = err.Error() + logs.Error(err) + return + } - c.SetMember(*member) + userInfo, err := client.GetUserInfo(context.Background(), code) + if err != nil { + c.DelSession(conf.LoginSessionName) + c.SetMember(models.Member{}) + c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) + errMsg = err.Error() + logs.Error(err) + return + } - var remember CookieRemember - remember.MemberId = member.MemberId - remember.Account = member.Account - remember.Time = time.Now() - v, err := utils.Encode(remember) - if err == nil { - c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) - } - bind_existed = "true" - error_msg = "" - u := c.GetString("url") - if u == "" { - u = conf.URLFor("HomeController.Index") - } - c.Redirect(u, 302) - } else { - if err == orm.ErrNoRows { - c.SetSession(SessionUserInfoKey, user_info) - bind_existed = "false" - error_msg = "" - } else { - logs.Error("Error: ", err) - error_msg = "数据库错误: " + err.Error() - } - } - // - } else { - error_msg = "获取用户信息失败: " + err_msg - } - } else { - error_msg = "通讯录访问凭据获取失败: " + contact_access_token - } + account, err := c.getAuth2Account() + if err != nil { + logs.Error("获取Auth2用户失败 ->", err) + c.JsonResult(500, "不支持的第三方用户", nil) + return + } + + member, err := account.ExistedMember(userInfo.UserId) + if err != nil { + if err == orm.ErrNoRows { + if userInfo.Mobile == "" { + errMsg = "请到应用浏览器中登录,并授权获取敏感信息。" } else { - error_msg = "获取用户Id失败: " + user_id + jsonInfo, _ := json.Marshal(userInfo) + userInfoJson = string(jsonInfo) + errMsg = "" + c.SetSession(SessionUserInfoKey, userInfo) } } else { - error_msg = "应用凭据获取失败: " + access_token + logs.Error("Error: ", err) + errMsg = "登录错误: " + err.Error() } - } else { - error_msg = "参数错误" + return } - if user_info_json == "" { - user_info_json = "{}" + + bindExisted = "true" + errMsg = "" + + member.LastLoginTime = time.Now() + _ = member.Update("last_login_time") + + c.SetMember(*member) + remember.MemberId = member.MemberId + remember.Account = member.Account + remember.Time = time.Now() + v, err := utils.Encode(remember) + if err == nil { + c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) } - if bind_existed == "" { - bind_existed = "null" + u := c.GetString("url") + if u == "" { + u = conf.URLFor("HomeController.Index") } - // refer & doc: - // - https://golang.org/pkg/html/template/#HTML - // - https://stackoverflow.com/questions/24411880/go-html-templates-can-i-stop-the-templates-package-inserting-quotes-around-stri - // - https://stackoverflow.com/questions/38035176/insert-javascript-snippet-inside-template-with-beego-golang - c.Data["bind_existed"] = template.JS(bind_existed) - logs.Debug("bind_existed: ", bind_existed) - c.Data["error_msg"] = template.JS(error_msg) - c.Data["user_info_json"] = template.JS(user_info_json) - /* - // 调试: 显示源码 - result, err := c.RenderString() - if err != nil { - logs.Error(err) - } else { - logs.Warning(result) - } - */ + c.Redirect(u, 302) + } +// Auth2BindAccount 第三方auth2.0绑定已有账号 +func (c *AccountController) Auth2BindAccount() { + userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo) + if !ok || len(userInfo.UserId) <= 0 { + c.DelSession(SessionUserInfoKey) + c.JsonResult(400, "请求错误, 请从首页重新登录") + return + } + + account := c.GetString("account") + password := c.GetString("password") + if account == "" || password == "" { + c.JsonResult(400, "账号或密码不能为空") + return + } + + member, err := models.NewMember().Login(account, password) + if err != nil { + logs.Error("用户登录 ->", err) + c.JsonResult(500, "账号或密码错误", nil) + return + } + + bindAccount, err := c.getAuth2Account() + if err != nil { + logs.Error("获取Auth2用户失败 ->", err) + c.JsonResult(500, "不支持的第三方用户", nil) + return + } + + member.CreateAt = 0 + ormer := orm.NewOrm() + o, err := ormer.Begin() + if err != nil { + logs.Error("开启事务时出错 -> ", err) + c.JsonResult(500, "开启事务时出错: ", err.Error()) + return + } + if err := bindAccount.AddBind(ormer, userInfo, member); err != nil { + logs.Error(err) + o.Rollback() + c.JsonResult(500, "绑定失败,数据库错误: "+err.Error()) + return + } + + // 绑定成功之后修改用户信息 + member.LastLoginTime = time.Now() + //member.RealName = user_info.Name + //member.Avatar = user_info.Avatar + if len(member.Avatar) < 1 { + member.Avatar = conf.GetDefaultAvatar() + } + //member.Email = user_info.Email + //member.Phone = user_info.Mobile + if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil { + o.Rollback() + logs.Error("保存用户信息失败=>", err) + c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error()) + return + + } + + if err := o.Commit(); err != nil { + logs.Error("开启事务时出错 -> ", err) + c.JsonResult(500, "开启事务时出错: ", err.Error()) + return + } + + c.DelSession(SessionUserInfoKey) + c.SetMember(*member) + + var remember CookieRemember + remember.MemberId = member.MemberId + remember.Account = member.Account + remember.Time = time.Now() + v, err := utils.Encode(remember) + if err != nil { + c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) + return + } + + c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) + c.JsonResult(0, "绑定成功", nil) +} + +// Auth2AutoAccount auth2.0自动创建账号 +func (c *AccountController) Auth2AutoAccount() { + app := c.Ctx.Input.Param(":app") + logs.Debug("app: ", app) + + userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo) + if !ok || len(userInfo.UserId) <= 0 { + c.DelSession(SessionUserInfoKey) + c.JsonResult(400, "请求错误, 请从首页重新登录") + return + } + + c.DelSession(SessionUserInfoKey) + member := models.NewMember() + + if _, err := member.FindByAccount(userInfo.UserId); err == nil && member.MemberId > 0 { + c.JsonResult(400, "账号已存在") + return + } + + ormer := orm.NewOrm() + o, err := ormer.Begin() + if err != nil { + logs.Error("开启事务时出错 -> ", err) + c.JsonResult(500, "开启事务时出错: ", err.Error()) + return + } + + member.Account = userInfo.UserId + member.RealName = userInfo.Name + member.Password = "123456" // 强制设置默认密码,需修改一次密码后,才可以进行账号密码登录 + hash, err := utils.PasswordHash(member.Password) + + if err != nil { + logs.Error("加密用户密码失败 =>", err) + c.JsonResult(500, "加密用户密码失败"+err.Error()) + return + } + + logs.Debug("member.Password: ", member.Password) + logs.Debug("hash: ", hash) + member.Password = hash + + member.Role = conf.MemberGeneralRole + member.Avatar = userInfo.Avatar + if len(member.Avatar) < 1 { + member.Avatar = conf.GetDefaultAvatar() + } + member.CreateAt = 0 + member.Email = userInfo.Mail + member.Phone = userInfo.Mobile + member.Status = 0 + if _, err = ormer.Insert(member); err != nil { + o.Rollback() + c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) + return + } + + account, err := c.getAuth2Account() + if err != nil { + logs.Error("获取Auth2用户失败 ->", err) + c.JsonResult(500, "不支持的第三方用户", nil) + return + } + + member.CreateAt = 0 + if err := account.AddBind(ormer, userInfo, member); err != nil { + logs.Error(err) + o.Rollback() + c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) + return + } + + if err := o.Commit(); err != nil { + logs.Error("提交事务时出错 -> ", err) + c.JsonResult(500, "提交事务时出错: ", err.Error()) + return + } + + member.LastLoginTime = time.Now() + _ = member.Update("last_login_time") + + c.SetMember(*member) + + var remember CookieRemember + remember.MemberId = member.MemberId + remember.Account = member.Account + remember.Time = time.Now() + v, err := utils.Encode(remember) + if err != nil { + c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) + return + } + + c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) + c.JsonResult(0, "绑定成功", nil) +} + +// 钉钉登录 +//func (c *AccountController) DingTalkLogin() { +// code := c.GetString("dingtalk_code") +// if code == "" { +// c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_obtain_user_info"), nil) +// } +// +// appKey, _ := web.AppConfig.String("dingtalk_app_key") +// appSecret, _ := web.AppConfig.String("dingtalk_app_secret") +// tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader") +// +// if appKey == "" || appSecret == "" || tmpReader == "" { +// c.JsonResult(500, i18n.Tr(c.Lang, "message.dingtalk_auto_login_not_enable"), nil) +// c.StopRun() +// } +// +// dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey) +// err := dingtalkAgent.GetAccesstoken() +// if err != nil { +// logs.Warn("获取钉钉临时Token失败 ->", err) +// c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) +// c.StopRun() +// } +// +// userid, err := dingtalkAgent.GetUserIDByCode(code) +// if err != nil { +// logs.Warn("获取钉钉用户ID失败 ->", err) +// c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) +// c.StopRun() +// } +// +// username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid) +// if err != nil { +// logs.Warn("获取钉钉用户信息失败 ->", err) +// c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil) +// 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.JsonResult(0, "ok", username) +//} + +// WorkWeixinLogin 用户企业微信登录 +//func (c *AccountController) WorkWeixinLogin() { +// logs.Info("UserAgent: ", c.Ctx.Input.UserAgent()) // debug +// +// if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { +// u := c.GetString("url") +// if u == "" { +// u = c.Ctx.Request.Header.Get("Referer") +// if u == "" { +// u = conf.URLFor("HomeController.Index") +// } +// } +// // session自动登录时刷新session内容 +// member, err := models.NewMember().Find(member.MemberId) +// if err != nil { +// c.DelSession(conf.LoginSessionName) +// c.SetMember(models.Member{}) +// c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) +// } else { +// c.SetMember(*member) +// } +// c.Redirect(u, 302) +// } +// var remember CookieRemember +// // 如果 Cookie 中存在登录信息 +// if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { +// if err := utils.Decode(cookie, &remember); err == nil { +// if member, err := models.NewMember().Find(remember.MemberId); err == nil { +// c.SetMember(*member) +// c.LoggedIn(false) +// c.StopRun() +// } +// } +// } +// +// if c.Ctx.Input.IsPost() { +// // account := c.GetString("account") +// // password := c.GetString("password") +// // captcha := c.GetString("code") +// // isRemember := c.GetString("is_remember") +// c.JsonResult(400, "request method not allowed", nil) +// } else { +// var callback_u string +// u := c.GetString("url") +// if u == "" { +// u = c.referer() +// } +// if u != "" { +// var schemaRule = regexp.MustCompile(`^https?\:\/\/`) +// if !schemaRule.MatchString(u) { +// u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/") +// } +// } +// if u == "" { +// callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback") +// } else { +// callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback", "url", url.PathEscape(u)) +// } +// logs.Info("callback_u: ", callback_u) // debug +// +// state := "mindoc" +// workweixinConf := conf.GetWorkWeixinConfig() +// appid := workweixinConf.CorpId +// agentid := workweixinConf.AgentId +// var redirect_uri string +// +// isInWorkWeixin := c.IsInWorkWeixin() +// c.Data["IsInWorkWeixin"] = isInWorkWeixin +// if isInWorkWeixin { +// // 企业微信内-网页授权登录 +// urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect" +// redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_AuthorizeUrlBase, appid, agentid, url.PathEscape(callback_u), state) +// } else { +// // 浏览器内-扫码授权登录 +// urlFmt := "%s?login_type=CorpApp&appid=%s&agentid=%s&redirect_uri=%s&state=%s" +// redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_QRConnectUrlBase, appid, agentid, url.PathEscape(callback_u), state) +// } +// logs.Info("redirect_uri: ", redirect_uri) // debug +// c.Redirect(redirect_uri, 302) +// } +//} + +/* +思路: +1. 浏览器打开 + 用户名+密码 登录 与企业微信没有交集 + 手机企业微信登录->扫码页面->扫码后获取用户信息, 判断是否绑定了企业微信 + 已绑定,则读取用户信息,直接登录 + 未绑定,则弹窗提示[未绑定企业微信,请先在企业微信中打开,完成绑定] +2. 企业微信打开->自动登录->判断是否绑定了企业微信 + 已绑定,则读取用户信息,直接登录 + 未绑定,则弹窗提示 + 是否已有账户(用户名+密码方式) + 有: 弹窗输入[用户名+密码+验证码]校验 + 无: 直接以企业UserId作为用户名(小写),创建随机密码 +*/ + +// WorkWeixinLoginCallback 用户企业微信登录-回调 +//func (c *AccountController) WorkWeixinLoginCallback() { +// c.TplName = "account/auth2_callback.tpl" +// +// if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 { +// u := c.GetString("url") +// if u == "" { +// u = c.Ctx.Request.Header.Get("Referer") +// } +// if u == "" { +// u = conf.URLFor("HomeController.Index") +// } +// member, err := models.NewMember().Find(member.MemberId) +// if err != nil { +// c.DelSession(conf.LoginSessionName) +// c.SetMember(models.Member{}) +// c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600) +// } else { +// c.SetMember(*member) +// } +// c.Redirect(u, 302) +// } +// +// var remember CookieRemember +// // 如果 Cookie 中存在登录信息 +// if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok { +// if err := utils.Decode(cookie, &remember); err == nil { +// if member, err := models.NewMember().Find(remember.MemberId); err == nil { +// c.SetMember(*member) +// c.LoggedIn(false) +// c.StopRun() +// } +// } +// } +// +// // 请求参数获取 +// req_code := c.GetString("code") +// logs.Warning("req_code: ", req_code) +// req_state := c.GetString("state") +// logs.Warning("req_state: ", req_state) +// var user_info_json string +// var error_msg string +// var bind_existed string +// if len(req_code) > 0 && req_state == "mindoc" { +// // 获取当前应用的access_token +// access_token, ok := workweixin.GetAccessToken() +// if ok { +// logs.Warning("access_token: ", access_token) +// // 获取当前请求的userid +// user_id, ticket, ok := workweixin.RequestUserId(access_token, req_code) +// if ok { +// logs.Warning("user_id: ", user_id) +// // 查询系统现有数据,是否绑定了当前请求用户的企业微信 +// member, err := models.NewWorkWeixinAccount().ExistedMember(user_id) +// if err == nil { +// member.LastLoginTime = time.Now() +// _ = member.Update("last_login_time") +// +// c.SetMember(*member) +// +// var remember CookieRemember +// remember.MemberId = member.MemberId +// remember.Account = member.Account +// remember.Time = time.Now() +// v, err := utils.Encode(remember) +// if err == nil { +// c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) +// } +// bind_existed = "true" +// error_msg = "" +// u := c.GetString("url") +// if u == "" { +// u = conf.URLFor("HomeController.Index") +// } +// c.Redirect(u, 302) +// } else if err == orm.ErrNoRows { +// bind_existed = "false" +// if ticket == "" { +// error_msg = "请到企业微信中登录,并授权获取敏感信息。" +// } else { +// user_info, err := workweixin.RequestUserPrivateInfo(access_token, user_id, ticket) +// if err != nil { +// error_msg = "获取敏感信息错误: " + err.Error() +// } else { +// json_info, _ := json.Marshal(user_info) +// user_info_json = string(json_info) +// error_msg = "" +// c.SetSession(SessionUserInfoKey, user_info) +// } +// } +// } else { +// logs.Error("Error: ", err) +// error_msg = "登录错误: " + err.Error() +// } +// } else { +// error_msg = "获取用户Id失败: " + user_id +// } +// } else { +// error_msg = "应用凭据获取失败: " + access_token +// } +// } else { +// error_msg = "参数错误" +// } +// if user_info_json == "" { +// user_info_json = "{}" +// } +// if bind_existed == "" { +// bind_existed = "null" +// } +// // refer & doc: +// // - https://golang.org/pkg/html/template/#HTML +// // - https://stackoverflow.com/questions/24411880/go-html-templates-can-i-stop-the-templates-package-inserting-quotes-around-stri +// // - https://stackoverflow.com/questions/38035176/insert-javascript-snippet-inside-template-with-beego-golang +// c.Data["bind_existed"] = template.JS(bind_existed) +// logs.Debug("bind_existed: ", bind_existed) +// c.Data["error_msg"] = template.JS(error_msg) +// c.Data["user_info_json"] = template.JS(user_info_json) +// /* +// // 调试: 显示源码 +// result, err := c.RenderString() +// if err != nil { +// logs.Error(err) +// } else { +// logs.Warning(result) +// } +// */ +//} + // WorkWeixinLoginBind 用户企业微信登录-绑定 -func (c *AccountController) WorkWeixinLoginBind() { - if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinDeptUserInfo); ok && len(user_info.UserId) > 0 { - req_account := c.GetString("account") - req_password := c.GetString("password") - if req_account == "" || req_password == "" { - c.JsonResult(400, "账号或密码不能为空") - } else { - member, err := models.NewMember().Login(req_account, req_password) - if err == nil { - account := models.NewWorkWeixinAccount() - account.MemberId = member.MemberId - account.WorkWeixin_UserId = user_info.UserId - member.CreateAt = 0 - ormer := orm.NewOrm() - o, err := ormer.Begin() - if err != nil { - logs.Error("开启事务时出错 -> ", err) - c.JsonResult(500, "开启事务时出错: ", err.Error()) - } - if err := account.AddBind(ormer); err != nil { - o.Rollback() - c.JsonResult(500, "绑定失败,数据库错误: "+err.Error()) - } else { - // 绑定成功之后修改用户信息 - member.LastLoginTime = time.Now() - //member.RealName = user_info.Name - //member.Avatar = user_info.Avatar - if len(member.Avatar) < 1 { - member.Avatar = conf.GetDefaultAvatar() - } - //member.Email = user_info.Email - //member.Phone = user_info.Mobile - if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil { - o.Rollback() - logs.Error("保存用户信息失败=>", err) - c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error()) - } else { - if err := o.Commit(); err != nil { - logs.Error("开启事务时出错 -> ", err) - c.JsonResult(500, "开启事务时出错: ", err.Error()) - } else { - c.DelSession(SessionUserInfoKey) - c.SetMember(*member) - - var remember CookieRemember - remember.MemberId = member.MemberId - remember.Account = member.Account - remember.Time = time.Now() - v, err := utils.Encode(remember) - if err == nil { - c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) - c.JsonResult(0, "绑定成功", nil) - } else { - c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) - } - } - } - - } - - } else { - logs.Error("用户登录 ->", err) - c.JsonResult(500, "账号或密码错误", nil) - } - c.JsonResult(500, "TODO: 绑定以后账号功能开发中") - } - } else { - if ok { - c.DelSession(SessionUserInfoKey) - } - c.JsonResult(400, "请求错误, 请从首页重新登录") - } - -} +//func (c *AccountController) WorkWeixinLoginBind() { +// if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserPrivateInfo); ok && len(user_info.UserId) > 0 { +// req_account := c.GetString("account") +// req_password := c.GetString("password") +// if req_account == "" || req_password == "" { +// c.JsonResult(400, "账号或密码不能为空") +// } else { +// member, err := models.NewMember().Login(req_account, req_password) +// if err == nil { +// account := models.NewWorkWeixinAccount() +// account.MemberId = member.MemberId +// account.WorkWeixin_UserId = user_info.UserId +// member.CreateAt = 0 +// ormer := orm.NewOrm() +// o, err := ormer.Begin() +// if err != nil { +// logs.Error("开启事务时出错 -> ", err) +// c.JsonResult(500, "开启事务时出错: ", err.Error()) +// } +// if err := account.AddBind(ormer); err != nil { +// o.Rollback() +// c.JsonResult(500, "绑定失败,数据库错误: "+err.Error()) +// } else { +// // 绑定成功之后修改用户信息 +// member.LastLoginTime = time.Now() +// //member.RealName = user_info.Name +// //member.Avatar = user_info.Avatar +// if len(member.Avatar) < 1 { +// member.Avatar = conf.GetDefaultAvatar() +// } +// //member.Email = user_info.Email +// //member.Phone = user_info.Mobile +// if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil { +// o.Rollback() +// logs.Error("保存用户信息失败=>", err) +// c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error()) +// } else { +// if err := o.Commit(); err != nil { +// logs.Error("开启事务时出错 -> ", err) +// c.JsonResult(500, "开启事务时出错: ", err.Error()) +// } else { +// c.DelSession(SessionUserInfoKey) +// c.SetMember(*member) +// +// var remember CookieRemember +// remember.MemberId = member.MemberId +// remember.Account = member.Account +// remember.Time = time.Now() +// v, err := utils.Encode(remember) +// if err == nil { +// c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) +// c.JsonResult(0, "绑定成功", nil) +// } else { +// c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) +// } +// } +// } +// +// } +// +// } else { +// logs.Error("用户登录 ->", err) +// c.JsonResult(500, "账号或密码错误", nil) +// } +// c.JsonResult(500, "TODO: 绑定以后账号功能开发中") +// } +// } else { +// if ok { +// c.DelSession(SessionUserInfoKey) +// } +// c.JsonResult(400, "请求错误, 请从首页重新登录") +// } +// +//} // WorkWeixinLoginIgnore 用户企业微信登录-忽略 -func (c *AccountController) WorkWeixinLoginIgnore() { - if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserInfo); ok && len(user_info.UserId) > 0 { - c.DelSession(SessionUserInfoKey) - member := models.NewMember() - - if _, err := member.FindByAccount(user_info.UserId); err == nil && member.MemberId > 0 { - c.JsonResult(400, "账号已存在") - } - - ormer := orm.NewOrm() - o, err := ormer.Begin() - if err != nil { - logs.Error("开启事务时出错 -> ", err) - c.JsonResult(500, "开启事务时出错: ", err.Error()) - } - - member.Account = user_info.UserId - member.RealName = user_info.Name - var rnd = rand.New(src) - // fmt.Sprintf("%x", rnd.Uint64()) - // strconv.FormatUint(rnd.Uint64(), 16) - member.Password = user_info.UserId + strconv.FormatUint(rnd.Uint64(), 16) - member.Password = "pathea.2020" // 强制设置默认密码,不然无法修改密码(因为目前修改密码需要知道当前密码) - hash, err := utils.PasswordHash(member.Password) - if err != nil { - logs.Error("加密用户密码失败 =>", err) - c.JsonResult(500, "加密用户密码失败"+err.Error()) - } else { - logs.Error("member.Password: ", member.Password) - logs.Error("hash: ", hash) - member.Password = hash - } - member.Role = conf.MemberGeneralRole - member.Avatar = user_info.Avatar - if len(member.Avatar) < 1 { - member.Avatar = conf.GetDefaultAvatar() - } - member.CreateAt = 0 - member.Email = user_info.Email - member.Phone = user_info.Mobile - member.Status = 0 - if _, err = ormer.Insert(member); err != nil { - o.Rollback() - c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) - } else { - account := models.NewWorkWeixinAccount() - account.MemberId = member.MemberId - account.WorkWeixin_UserId = user_info.UserId - member.CreateAt = 0 - if err := account.AddBind(ormer); err != nil { - o.Rollback() - c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) - } else { - if err := o.Commit(); err != nil { - logs.Error("提交事务时出错 -> ", err) - c.JsonResult(500, "提交事务时出错: ", err.Error()) - } else { - member.LastLoginTime = time.Now() - _ = member.Update("last_login_time") - - c.SetMember(*member) - - var remember CookieRemember - remember.MemberId = member.MemberId - remember.Account = member.Account - remember.Time = time.Now() - v, err := utils.Encode(remember) - if err == nil { - c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) - c.JsonResult(0, "绑定成功", nil) - } else { - c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) - } - } - } - } - } else { - if ok { - c.DelSession(SessionUserInfoKey) - } - c.JsonResult(400, "请求错误, 请从首页重新登录") - } -} +//func (c *AccountController) WorkWeixinLoginIgnore() { +// if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserPrivateInfo); ok && len(user_info.UserId) > 0 { +// c.DelSession(SessionUserInfoKey) +// member := models.NewMember() +// +// if _, err := member.FindByAccount(user_info.UserId); err == nil && member.MemberId > 0 { +// c.JsonResult(400, "账号已存在") +// } +// +// ormer := orm.NewOrm() +// o, err := ormer.Begin() +// if err != nil { +// logs.Error("开启事务时出错 -> ", err) +// c.JsonResult(500, "开启事务时出错: ", err.Error()) +// } +// +// member.Account = user_info.UserId +// member.RealName = user_info.Name +// var rnd = rand.New(src) +// // fmt.Sprintf("%x", rnd.Uint64()) +// // strconv.FormatUint(rnd.Uint64(), 16) +// member.Password = user_info.UserId + strconv.FormatUint(rnd.Uint64(), 16) +// member.Password = "123456" // 强制设置默认密码,需修改一次密码后,才可以进行账号密码登录 +// hash, err := utils.PasswordHash(member.Password) +// if err != nil { +// logs.Error("加密用户密码失败 =>", err) +// c.JsonResult(500, "加密用户密码失败"+err.Error()) +// } else { +// logs.Error("member.Password: ", member.Password) +// logs.Error("hash: ", hash) +// member.Password = hash +// } +// member.Role = conf.MemberGeneralRole +// member.Avatar = user_info.Avatar +// if len(member.Avatar) < 1 { +// member.Avatar = conf.GetDefaultAvatar() +// } +// member.CreateAt = 0 +// member.Email = user_info.BizMail +// member.Phone = user_info.Mobile +// member.Status = 0 +// if _, err = ormer.Insert(member); err != nil { +// o.Rollback() +// c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) +// } else { +// account := models.NewWorkWeixinAccount() +// account.MemberId = member.MemberId +// account.WorkWeixin_UserId = user_info.UserId +// member.CreateAt = 0 +// if err := account.AddBind(ormer); err != nil { +// o.Rollback() +// c.JsonResult(500, "注册失败,数据库错误: "+err.Error()) +// } else { +// if err := o.Commit(); err != nil { +// logs.Error("提交事务时出错 -> ", err) +// c.JsonResult(500, "提交事务时出错: ", err.Error()) +// } else { +// member.LastLoginTime = time.Now() +// _ = member.Update("last_login_time") +// +// c.SetMember(*member) +// +// var remember CookieRemember +// remember.MemberId = member.MemberId +// remember.Account = member.Account +// remember.Time = time.Now() +// v, err := utils.Encode(remember) +// if err == nil { +// c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix()) +// c.JsonResult(0, "绑定成功", nil) +// } else { +// c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil) +// } +// } +// } +// } +// } else { +// if ok { +// c.DelSession(SessionUserInfoKey) +// } +// c.JsonResult(400, "请求错误, 请从首页重新登录") +// } +//} // QR二维码登录 -func (c *AccountController) QRLogin() { - 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) - - // 企业微信扫码登录 - case "workweixin": - // - - default: - c.Redirect(conf.URLFor("AccountController.Login"), 302) - c.StopRun() - } -} +//func (c *AccountController) QRLogin() { +// 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) +// +// // 企业微信扫码登录 +// case "workweixin": +// // +// +// default: +// c.Redirect(conf.URLFor("AccountController.Login"), 302) +// c.StopRun() +// } +//} // 登录成功后的操作,如重定向到原始请求页面 func (c *AccountController) LoggedIn(isPost bool) interface{} { diff --git a/controllers/SettingController.go b/controllers/SettingController.go index 762af330..b7920404 100644 --- a/controllers/SettingController.go +++ b/controllers/SettingController.go @@ -79,6 +79,9 @@ func (c *SettingController) Password() { c.JsonResult(6007, i18n.Tr(c.Lang, "message.pwd_encrypt_failed")) } c.Member.Password = pwd + if c.Member.AuthMethod == "" { + c.Member.AuthMethod = "local" + } if err := c.Member.Update(); err != nil { c.JsonResult(6008, err.Error()) } diff --git a/models/Auth2Account.go b/models/Auth2Account.go new file mode 100644 index 00000000..a5ff751e --- /dev/null +++ b/models/Auth2Account.go @@ -0,0 +1,185 @@ +// Package models . +package models + +import ( + "errors" + "github.com/mindoc-org/mindoc/utils/auth2" + "time" + + "github.com/beego/beego/v2/client/orm" + "github.com/beego/beego/v2/core/logs" + "github.com/mindoc-org/mindoc/conf" +) + +var ( + _ Auth2Account = (*WorkWeixinAccount)(nil) + _ Auth2Account = (*DingTalkAccount)(nil) +) + +type Auth2Account interface { + ExistedMember(id string) (*Member, error) + AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error +} + +func NewWorkWeixinAccount() *WorkWeixinAccount { + return &WorkWeixinAccount{} +} + +type WorkWeixinAccount struct { + MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"` + UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"` + WorkWeixin_UserId string `orm:"size(100);unique;column(workweixin_user_id)" json:"workweixin_user_id"` + // WorkWeixin_Name string `orm:"size(255);column(workweixin_name)" json:"workweixin_name"` + // WorkWeixin_Phone string `orm:"size(25);column(workweixin_phone)" json:"workweixin_phone"` + // WorkWeixin_Email string `orm:"size(255);column(workweixin_email)" json:"workweixin_email"` + // WorkWeixin_Status int `orm:"type(int);column(status)" json:"status"` + // WorkWeixin_Avatar string `orm:"size(1024);column(avatar)" json:"avatar"` + CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` + CreateAt int `orm:"type(int);column(create_at)" json:"create_at"` + LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"` +} + +// TableName 获取对应数据库表名. +func (m *WorkWeixinAccount) TableName() string { + return "workweixin_accounts" +} + +// TableEngine 获取数据使用的引擎. +func (m *WorkWeixinAccount) TableEngine() string { + return "INNODB" +} + +func (m *WorkWeixinAccount) TableNameWithPrefix() string { + return conf.GetDatabasePrefix() + m.TableName() +} + +func (m *WorkWeixinAccount) ExistedMember(workweixin_user_id string) (*Member, error) { + o := orm.NewOrm() + account := NewWorkWeixinAccount() + member := NewMember() + err := o.QueryTable(m.TableNameWithPrefix()).Filter("workweixin_user_id", workweixin_user_id).One(account) + if err != nil { + return member, err + } + + member, err = member.Find(account.MemberId) + if err != nil { + return member, err + } + + if member.Status != 0 { + return member, errors.New("receive_account_disabled") + } + + return member, nil + +} + +// AddBind 添加一个用户. +func (m *WorkWeixinAccount) AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error { + tmpM := NewWorkWeixinAccount() + err := o.QueryTable(m.TableNameWithPrefix()).Filter("workweixin_user_id", userInfo.UserId).One(tmpM) + if err == nil { + tmpM.MemberId = member.MemberId + _, err = o.Update(tmpM) + if err != nil { + logs.Error("保存用户数据到数据时失败 =>", err) + return errors.New("用户信息绑定失败, 数据库错误") + } + return nil + } + + m.MemberId = member.MemberId + m.WorkWeixin_UserId = userInfo.UserId + + if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", m.MemberId).Count(); err == nil && c > 0 { + return errors.New("已绑定,不可重复绑定") + } + + _, err = o.Insert(m) + if err != nil { + logs.Error("保存用户数据到数据时失败 =>", err) + return errors.New("用户信息绑定失败, 数据库错误") + } + + return nil +} + +func NewDingTalkAccount() *DingTalkAccount { + return &DingTalkAccount{} +} + +type DingTalkAccount struct { + MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"` + UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"` + Dingtalk_UserId string `orm:"size(100);unique;column(dingtalk_user_id)" json:"dingtalk_user_id"` + CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` + CreateAt int `orm:"type(int);column(create_at)" json:"create_at"` + LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"` +} + +// TableName 获取对应数据库表名. +func (m *DingTalkAccount) TableName() string { + return "dingtalk_accounts" +} + +// TableEngine 获取数据使用的引擎. +func (m *DingTalkAccount) TableEngine() string { + return "INNODB" +} + +func (m *DingTalkAccount) TableNameWithPrefix() string { + return conf.GetDatabasePrefix() + m.TableName() +} + +func (m *DingTalkAccount) ExistedMember(userid string) (*Member, error) { + o := orm.NewOrm() + account := NewDingTalkAccount() + member := NewMember() + err := o.QueryTable(m.TableNameWithPrefix()).Filter("dingtalk_user_id", userid).One(account) + if err != nil { + return member, err + } + + member, err = member.Find(account.MemberId) + if err != nil { + return member, err + } + + if member.Status != 0 { + return member, errors.New("receive_account_disabled") + } + + return member, nil + +} + +// AddBind 添加一个用户. +func (m *DingTalkAccount) AddBind(o orm.Ormer, userInfo auth2.UserInfo, member *Member) error { + tmpM := NewDingTalkAccount() + err := o.QueryTable(m.TableNameWithPrefix()).Filter("dingtalk_user_id", userInfo.UserId).One(tmpM) + if err == nil { + tmpM.MemberId = member.MemberId + _, err = o.Update(tmpM) + if err != nil { + logs.Error("保存用户数据到数据时失败 =>", err) + return errors.New("用户信息绑定失败, 数据库错误") + } + return nil + } + + m.Dingtalk_UserId = userInfo.UserId + m.MemberId = member.MemberId + + if c, err := o.QueryTable(m.TableNameWithPrefix()).Filter("member_id", m.MemberId).Count(); err == nil && c > 0 { + return errors.New("已绑定,不可重复绑定") + } + + _, err = o.Insert(m) + if err != nil { + logs.Error("保存用户数据到数据时失败 =>", err) + return errors.New("用户信息绑定失败, 数据库错误") + } + + return nil +} diff --git a/models/Member.go b/models/Member.go index 1d836553..a570578e 100644 --- a/models/Member.go +++ b/models/Member.go @@ -90,7 +90,6 @@ func (m *Member) Login(account string, password string) (*Member, error) { } switch member.AuthMethod { - case "": case "local": ok, err := utils.PasswordVerify(member.Password, password) if ok && err == nil { @@ -109,15 +108,15 @@ func (m *Member) Login(account string, password string) (*Member, error) { } // TmpLogin 用于钉钉临时登录 -func (m *Member) TmpLogin(account string) (*Member, error) { - o := orm.NewOrm() - member := &Member{} - err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member) - if err != nil { - return member, ErrorMemberPasswordError - } - return member, nil -} +//func (m *Member) TmpLogin(account string) (*Member, error) { +// o := orm.NewOrm() +// member := &Member{} +// err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member) +// if err != nil { +// return member, ErrorMemberPasswordError +// } +// return member, nil +//} // ldapLogin 通过LDAP登陆 func (m *Member) ldapLogin(account string, password string) (*Member, error) { @@ -510,17 +509,26 @@ func (m *Member) Delete(oldId int, newId int) error { ormer := orm.NewOrm() o, err := ormer.Begin() - if err != nil { return err } + _, err = o.Raw("DELETE FROM md_dingtalk_accounts WHERE member_id = ?", oldId).Exec() + if err != nil { + o.Rollback() + return err + } + _, err = o.Raw("DELETE FROM md_workweixin_accounts WHERE member_id = ?", oldId).Exec() + if err != nil { + o.Rollback() + return err + } _, err = o.Raw("DELETE FROM md_members WHERE member_id = ?", oldId).Exec() if err != nil { o.Rollback() return err } - _, err = o.Raw("UPDATE md_attachment SET `create_at` = ? WHERE `create_at` = ?", newId, oldId).Exec() + _, err = o.Raw("UPDATE md_attachment SET create_at = ? WHERE create_at = ?", newId, oldId).Exec() if err != nil { o.Rollback() diff --git a/models/WorkWeixinAccount.go b/models/WorkWeixinAccount.go deleted file mode 100644 index 8e3b04ce..00000000 --- a/models/WorkWeixinAccount.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package models . -package models - -import ( - "errors" - "time" - - "github.com/beego/beego/v2/client/orm" - "github.com/beego/beego/v2/core/logs" - "github.com/mindoc-org/mindoc/conf" -) - -type WorkWeixinAccount struct { - MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"` - UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"` - WorkWeixin_UserId string `orm:"size(100);unique;column(workweixin_user_id)" json:"workweixin_user_id"` - // WorkWeixin_Name string `orm:"size(255);column(workweixin_name)" json:"workweixin_name"` - // WorkWeixin_Phone string `orm:"size(25);column(workweixin_phone)" json:"workweixin_phone"` - // WorkWeixin_Email string `orm:"size(255);column(workweixin_email)" json:"workweixin_email"` - // WorkWeixin_Status int `orm:"type(int);column(status)" json:"status"` - // WorkWeixin_Avatar string `orm:"size(1024);column(avatar)" json:"avatar"` - CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"` - CreateAt int `orm:"type(int);column(create_at)" json:"create_at"` - LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"` -} - -// TableName 获取对应数据库表名. -func (m *WorkWeixinAccount) TableName() string { - return "workweixin_accounts" -} - -// TableEngine 获取数据使用的引擎. -func (m *WorkWeixinAccount) TableEngine() string { - return "INNODB" -} - -func (m *WorkWeixinAccount) TableNameWithPrefix() string { - return conf.GetDatabasePrefix() + m.TableName() -} - -func NewWorkWeixinAccount() *WorkWeixinAccount { - return &WorkWeixinAccount{} -} - -func (a *WorkWeixinAccount) ExistedMember(workweixin_user_id string) (*Member, error) { - o := orm.NewOrm() - account := NewWorkWeixinAccount() - member := NewMember() - err := o.QueryTable(a.TableNameWithPrefix()).Filter("workweixin_user_id", workweixin_user_id).One(account) - if err == nil { - if member, err = member.Find(account.MemberId); err == nil { - return member, nil - } else { - return member, err - } - } else { - return member, err - } -} - -// Add 添加一个用户. -func (a *WorkWeixinAccount) AddBind(o orm.Ormer) error { - if c, err := o.QueryTable(a.TableNameWithPrefix()).Filter("member_id", a.MemberId).Count(); err == nil && c > 0 { - return errors.New("已绑定,不可重复绑定") - } - - _, err := o.Insert(a) - if err != nil { - logs.Error("保存用户数据到数据时失败 =>", err) - return errors.New("用户信息绑定失败, 数据库错误") - } - - return nil -} diff --git a/routers/router.go b/routers/router.go index f30c8593..a6996180 100644 --- a/routers/router.go +++ b/routers/router.go @@ -123,12 +123,13 @@ func init() { web.Router("/", &controllers.HomeController{}, "*:Index") web.Router("/login", &controllers.AccountController{}, "*:Login") - web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin") - web.Router("/workweixin-login", &controllers.AccountController{}, "*:WorkWeixinLogin") - web.Router("/workweixin-callback", &controllers.AccountController{}, "*:WorkWeixinLoginCallback") - web.Router("/workweixin-bind", &controllers.AccountController{}, "*:WorkWeixinLoginBind") - web.Router("/workweixin-ignore", &controllers.AccountController{}, "*:WorkWeixinLoginIgnore") - web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin") + web.Router("/auth2/redirect/:app", &controllers.AccountController{}, "*:Auth2Redirect") + web.Router("/auth2/callback/:app", &controllers.AccountController{}, "*:Auth2Callback") + web.Router("/auth2/account/bind/:app", &controllers.AccountController{}, "*:Auth2BindAccount") + web.Router("/auth2/account/auto/:app", &controllers.AccountController{}, "*:Auth2AutoAccount") + + //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/utils/auth2/auth2.go b/utils/auth2/auth2.go new file mode 100644 index 00000000..881858f3 --- /dev/null +++ b/utils/auth2/auth2.go @@ -0,0 +1,89 @@ +package auth2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type UserInfo struct { + UserId string `json:"userid"` // 企业成员userid + Name string `json:"name"` // 姓名 + Avatar string `json:"avatar"` // 头像 + Mobile string `json:"mobile"` // 手机号 + Mail string `json:"mail"` // 邮箱 +} + +func NewAccessToken(token IAccessToken) AccessTokenCache { + return AccessTokenCache{ + AccessToken: token.GetToken(), + ExpireIn: token.GetExpireIn(), + ExpireTime: token.GetExpireTime(), + } +} + +type AccessTokenCache struct { + ExpireIn time.Duration + ExpireTime time.Time + AccessToken string +} + +func (a AccessTokenCache) GetToken() string { + return a.AccessToken +} + +func (a AccessTokenCache) GetExpireIn() time.Duration { + return a.ExpireIn +} + +func (a AccessTokenCache) GetExpireTime() time.Time { + return a.ExpireTime +} + +func (a AccessTokenCache) IsExpired() bool { + return time.Now().After(a.ExpireTime) +} + +type IAccessToken interface { + GetToken() string + GetExpireIn() time.Duration + GetExpireTime() time.Time +} + +type Client interface { + GetAccessToken(ctx context.Context) (IAccessToken, error) + SetAccessToken(token IAccessToken) + BuildURL(callback string, isAppBrowser bool) string + ValidateCallback(state string) error + GetUserInfo(ctx context.Context, code string) (UserInfo, error) +} + +type IResponse interface { + AsError() error +} + +func Request(req *http.Request, v IResponse) error { + response, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer response.Body.Close() + + b, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + if response.StatusCode != http.StatusOK { + return fmt.Errorf("status = %d, msg = %s", response.StatusCode, string(b)) + } + + if err := json.Unmarshal(b, v); err != nil { + return err + } + + return v.AsError() +} diff --git a/utils/auth2/dingtalk/dingtalk.go b/utils/auth2/dingtalk/dingtalk.go new file mode 100644 index 00000000..e8645f8e --- /dev/null +++ b/utils/auth2/dingtalk/dingtalk.go @@ -0,0 +1,234 @@ +package dingtalk + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/mindoc-org/mindoc/utils/auth2" + "net/http" + "net/url" + "time" +) + +const ( + AppName = "dingtalk" + + callbackState = "mindoc" +) + +type BasicResponse struct { + Message string `json:"errmsg"` + Code int `json:"errcode"` +} + +func (r *BasicResponse) Error() string { + return fmt.Sprintf("errcode=%d, errmsg=%s", r.Code, r.Message) +} + +func (r *BasicResponse) AsError() error { + if r == nil { + return nil + } + + if r.Code != 0 || r.Message != "ok" { + return r + } + return nil +} + +type AccessToken struct { + // 文档: https://open.dingtalk.com/document/orgapp/obtain-orgapp-token + *BasicResponse + + AccessToken string `json:"access_token"` + ExpireIn int `json:"expires_in"` + + createTime time.Time +} + +func (a AccessToken) GetToken() string { + return a.AccessToken +} + +func (a AccessToken) GetExpireIn() time.Duration { + return time.Duration(a.ExpireIn) * time.Second +} + +func (a AccessToken) GetExpireTime() time.Time { + return a.createTime.Add(a.GetExpireIn()) +} + +type UserAccessToken struct { + // 文档: https://open.dingtalk.com/document/orgapp/obtain-user-token + *BasicResponse // 此接口未返回错误代码信息,仅仅能检查HTTP状态码 + + ExpireIn int `json:"expireIn"` + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + CorpId string `json:"corpId"` +} + +type UserInfo struct { + // 文档: https://open.dingtalk.com/document/orgapp/dingtalk-retrieve-user-information + *BasicResponse + + NickName string `json:"nick"` + Avatar string `json:"avatarUrl"` + Mobile string `json:"mobile"` + OpenId string `json:"openId"` + UnionId string `json:"unionId"` + Email string `json:"email"` + StateCode string `json:"stateCode"` +} + +type UserIdByUnion struct { + // 文档: https://open.dingtalk.com/document/isvapp/query-a-user-by-the-union-id + *BasicResponse + + RequestId string `json:"request_id"` + Result struct { + ContactType int `json:"contact_type"` + UserId string `json:"userid"` + } `json:"result"` +} + +func NewClient(appSecret string, appKey string) auth2.Client { + return NewDingtalkClient(appSecret, appKey) +} + +func NewDingtalkClient(appSecret string, appKey string) *DingtalkClient { + return &DingtalkClient{AppSecret: appSecret, AppKey: appKey} +} + +type DingtalkClient struct { + AppSecret string + AppKey string + + token auth2.IAccessToken +} + +func (d *DingtalkClient) GetAccessToken(ctx context.Context) (auth2.IAccessToken, error) { + if d.token != nil { + return d.token, nil + } + + endpoint := fmt.Sprintf("https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s", d.AppKey, d.AppSecret) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + + var token AccessToken + if err := auth2.Request(req, &token); err != nil { + return nil, err + } + + token.createTime = time.Now() + return token, nil +} + +func (d *DingtalkClient) SetAccessToken(token auth2.IAccessToken) { + d.token = token +} + +func (d *DingtalkClient) BuildURL(callback string, _ bool) string { + v := url.Values{} + v.Set("redirect_uri", callback) + v.Set("response_type", "code") + v.Set("client_id", d.AppKey) + v.Set("scope", "openid") + v.Set("state", callbackState) + v.Set("prompt", "consent") + return "https://login.dingtalk.com/oauth2/auth?" + v.Encode() +} + +func (d *DingtalkClient) ValidateCallback(state string) error { + if state != callbackState { + return errors.New("auth2.state.wrong") + } + return nil +} + +func (d *DingtalkClient) getUserAccessToken(ctx context.Context, code string) (UserAccessToken, error) { + val := map[string]string{ + "clientId": d.AppKey, + "clientSecret": d.AppSecret, + "code": code, + "grantType": "authorization_code", + } + + jv, _ := json.Marshal(val) + + endpoint := "https://api.dingtalk.com/v1.0/oauth2/userAccessToken" + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jv)) + req.Header.Set("Content-Type", "application/json") + + var token UserAccessToken + if err := auth2.Request(req, &token); err != nil { + return token, err + } + + return token, nil +} + +func (d *DingtalkClient) getUserInfo(ctx context.Context, userToken UserAccessToken, unionId string) (UserInfo, error) { + var user UserInfo + + endpoint := fmt.Sprintf("https://api.dingtalk.com/v1.0/contact/users/%s", unionId) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + req.Header.Set("x-acs-dingtalk-access-token", userToken.AccessToken) + req.Header.Set("Content-Type", "application/json") + + if err := auth2.Request(req, &user); err != nil { + return user, err + } + return user, nil +} + +func (d *DingtalkClient) getUserIdByUnion(ctx context.Context, union string) (UserIdByUnion, error) { + var userId UserIdByUnion + token, err := d.GetAccessToken(ctx) + if err != nil { + return userId, err + } + endpoint := fmt.Sprintf("https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=%s", token.GetToken()) + b, _ := json.Marshal(map[string]string{ + "unionid": union, + }) + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + + if err := auth2.Request(req, &userId); err != nil { + return userId, err + } + + return userId, nil +} + +func (d *DingtalkClient) GetUserInfo(ctx context.Context, code string) (auth2.UserInfo, error) { + var info auth2.UserInfo + userToken, err := d.getUserAccessToken(ctx, code) + if err != nil { + return info, err + } + + userInfo, err := d.getUserInfo(ctx, userToken, "me") + if err != nil { + return info, err + } + + userId, err := d.getUserIdByUnion(ctx, userInfo.UnionId) + if err != nil { + return info, err + } + + if userId.Result.ContactType > 0 { + return info, errors.New("auth2.user.outer") + } + + info.UserId = userId.Result.UserId + info.Mail = userInfo.Email + info.Mobile = userInfo.Mobile + info.Name = userInfo.NickName + info.Avatar = userInfo.Avatar + return info, nil +} diff --git a/utils/auth2/wecom/wecom.go b/utils/auth2/wecom/wecom.go new file mode 100644 index 00000000..a85bbddd --- /dev/null +++ b/utils/auth2/wecom/wecom.go @@ -0,0 +1,285 @@ +package wecom + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/mindoc-org/mindoc/utils/auth2" + "net/http" + "net/url" + "time" +) + +// doc +// - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313 + +const ( + AppName = "workwx" + + auth2Url = "https://open.weixin.qq.com/connect/oauth2/authorize" + ssoUrl = "https://login.work.weixin.qq.com/wwlogin/sso/login" + callbackState = "mindoc" +) + +type BasicResponse struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} + +func (r *BasicResponse) Error() string { + return fmt.Sprintf("errcode=%d,errmsg=%s", r.ErrCode, r.ErrMsg) +} + +func (r *BasicResponse) AsError() error { + if r == nil { + return nil + } + + if r.ErrCode != 0 { + return r + } + return nil +} + +// 获取用户Id-请求响应结构 +type UserIdResponse struct { + // 接口文档: https://developer.work.weixin.qq.com/document/path/91023 + *BasicResponse + + UserId string `json:"userid"` // 企业成员UserID + UserTicket string `json:"user_ticket"` // 用于获取敏感信息 + OpenId string `json:"openid"` // 非企业成员的标识,对当前企业唯一 + ExternalUserId string `json:"external_userid"` // 外部联系人ID +} + +// 获取用户信息-请求响应结构 +type UserInfoResponse struct { + // 接口文档: https://developer.work.weixin.qq.com/document/path/90196 + *BasicResponse + + UserId string `json:"userid"` // 企业成员UserID + Name string `json:"name"` // 成员名称 + Department []int `json:"department"` // 成员所属部门id列表 + IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级 + IsLeader int `json:"isleader"` // 是否是部门上级(领导) + Alias string `json:"alias"` // 别名 + Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业 + MainDepartment int `json:"main_department"` // 主部门 +} + +type UserPrivateInfoResponse struct { + // 文档地址: https://developer.work.weixin.qq.com/document/path/95833 + *BasicResponse + + UserId string `json:"userid"` // 企业成员userid + Gender string `json:"gender"` // 成员性别 + Avatar string `json:"avatar"` // 头像 + QrCode string `json:"qr_code"` // 二维码 + Mobile string `json:"mobile"` // 手机号 + Mail string `json:"mail"` // 邮箱 + BizMail string `json:"biz_mail"` // 企业邮箱 + Address string `json:"address"` // 地址 +} + +// 访问凭据缓存-结构 +type AccessToken struct { + *BasicResponse + + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + + createTime time.Time `json:"create_time"` +} + +func (a AccessToken) GetToken() string { + return a.AccessToken +} + +func (a AccessToken) GetExpireIn() time.Duration { + return time.Duration(a.ExpiresIn) * time.Second +} + +func (a AccessToken) GetExpireTime() time.Time { + return a.createTime.Add(a.GetExpireIn()) +} + +// 企业微信用户敏感信息-结构 +type WorkWeixinUserPrivateInfo struct { + UserId string `json:"userid"` // 企业成员userid + Name string `json:"name"` // 姓名 + Gender string `json:"gender"` // 成员性别 + Avatar string `json:"avatar"` // 头像 + QrCode string `json:"qr_code"` // 二维码 + Mobile string `json:"mobile"` // 手机号 + Mail string `json:"mail"` // 邮箱 + BizMail string `json:"biz_mail"` // 企业邮箱 + Address string `json:"address"` // 地址 +} + +// 企业微信用户信息-结构 +type WorkWeixinUserInfo struct { + UserId string `json:"UserId"` // 企业成员UserID + Name string `json:"name"` // 成员名称 + HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码 + Mobile string `json:"mobile"` // 手机号码 + Department []int `json:"department"` // 成员所属部门id列表 + Email string `json:"email"` // 邮箱 + IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级 + IsLeader int `json:"isleader"` // 是否是部门上级(领导) + Avatar string `json:"avatar"` // 头像url + Alias string `json:"alias"` // 别名 + Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业 + MainDepartment int `json:"main_department"` // 主部门 +} + +func NewClient(corpId, appId, appSecrete string) auth2.Client { + return NewWorkWechatClient(corpId, appId, appSecrete) +} +func NewWorkWechatClient(corpId, appId, appSecrete string) *WorkWechatClient { + return &WorkWechatClient{ + CorpId: corpId, + AppId: appId, + AppSecret: appSecrete, + } +} + +type WorkWechatClient struct { + CorpId string + AppId string + AppSecret string + + token auth2.IAccessToken +} + +func (c *WorkWechatClient) GetAccessToken(ctx context.Context) (auth2.IAccessToken, error) { + if c.token != nil { + return c.token, nil + } + + endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", c.CorpId, c.AppSecret) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + + var token AccessToken + if err := auth2.Request(req, &token); err != nil { + return token, err + } + token.createTime = time.Now() + return token, nil +} + +func (c *WorkWechatClient) SetAccessToken(token auth2.IAccessToken) { + c.token = token + return +} + +func (c *WorkWechatClient) BuildURL(callback string, isAppBrowser bool) string { + var endpoint string + if isAppBrowser { + // 企业微信内-网页授权登录 + urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect" + endpoint = fmt.Sprintf(urlFmt, auth2Url, c.CorpId, c.AppId, url.PathEscape(callback), callbackState) + } else { + // 浏览器内-扫码授权登录 + urlFmt := "%s?login_type=CorpApp&appid=%s&agentid=%s&redirect_uri=%s&state=%s" + endpoint = fmt.Sprintf(urlFmt, ssoUrl, c.CorpId, c.AppId, url.PathEscape(callback), callbackState) + } + return endpoint +} + +func (c *WorkWechatClient) ValidateCallback(state string) error { + if state != callbackState { + return errors.New("auth2.state.wrong") + } + return nil +} + +func (c *WorkWechatClient) getUserId(ctx context.Context, code string) (UserIdResponse, error) { + var userId UserIdResponse + + token, err := c.GetAccessToken(ctx) + if err != nil { + return userId, err + } + endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s", token.GetToken(), code) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + + if err := auth2.Request(req, &userId); err != nil { + return userId, err + } + + if userId.UserId == "" { + return userId, errors.New("auth2.userid.empty") + } + + return userId, nil +} + +func (c *WorkWechatClient) getUserInfo(ctx context.Context, userid string) (UserInfoResponse, error) { + var userInfo UserInfoResponse + token, err := c.GetAccessToken(ctx) + if err != nil { + return userInfo, err + } + + endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", token.GetToken(), userid) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + + if err := auth2.Request(req, &userInfo); err != nil { + return userInfo, err + } + return userInfo, nil +} + +func (c *WorkWechatClient) getUserPrivateInfo(ctx context.Context, ticket string) (UserPrivateInfoResponse, error) { + var userInfo UserPrivateInfoResponse + + token, err := c.GetAccessToken(ctx) + if err != nil { + return userInfo, err + } + endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s", token.GetToken()) + + b, _ := json.Marshal(map[string]string{ + "user_ticket": ticket, + }) + + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(b)) + + if err := auth2.Request(req, &userInfo); err != nil { + return userInfo, err + } + return userInfo, nil +} + +func (c *WorkWechatClient) GetUserInfo(ctx context.Context, code string) (auth2.UserInfo, error) { + var info auth2.UserInfo + + userid, err := c.getUserId(ctx, code) + if err != nil { + return info, err + } + + userInfo, err := c.getUserInfo(ctx, userid.UserId) + if err != nil { + return info, err + } + + info.UserId = userInfo.UserId + info.Name = userInfo.Name + + if userid.UserTicket == "" { + return info, nil + } + + private, err := c.getUserPrivateInfo(ctx, userid.UserTicket) + if err != nil { + return info, err + } + + info.Mail = private.BizMail + info.Avatar = private.Avatar + info.Mobile = private.Mobile + return info, nil +} diff --git a/utils/workweixin/workweixin.go b/utils/workweixin/workweixin.go index 9041430c..a02ff515 100644 --- a/utils/workweixin/workweixin.go +++ b/utils/workweixin/workweixin.go @@ -3,7 +3,9 @@ package workweixin import ( "context" "crypto/tls" - // "encoding/json" + "encoding/json" + "errors" + "net/http" "time" @@ -17,8 +19,8 @@ import ( // - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313 const ( - AccessTokenCacheKey = "access-token-cache-key" - ContactAccessTokenCacheKey = "contact-access-token-cache-key" + AccessTokenCacheKey = "access-token-cache-key" + // ContactAccessTokenCacheKey = "contact-access-token-cache-key" ) // 获取访问凭据-请求响应结构 @@ -31,11 +33,13 @@ type AccessTokenResponse struct { // 获取用户Id-请求响应结构 type UserIdResponse struct { - ErrCode int `json:"errcode"` - ErrMsg string `json:"errmsg"` - UserId string `json:"UserId"` // 企业成员UserID - OpenId string `json:"OpenId"` // 非企业成员的标识,对当前企业唯一 - DeviceId string `json:"DeviceId"` // 设备号 + // 接口文档: https://developer.work.weixin.qq.com/document/path/91023 + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + UserId string `json:"userid"` // 企业成员UserID + UserTicket string `json:"user_ticket"` // 用于获取敏感信息 + OpenId string `json:"openid"` // 非企业成员的标识,对当前企业唯一 + ExternalUserId string `json:"external_userid"` // 外部联系人ID } // 获取成员ID列表-请求响应结构 @@ -65,6 +69,20 @@ type UserInfoResponse struct { MainDepartment int `json:"main_department"` // 主部门 } +type UserPrivateInfoResponse struct { + // 文档地址: https://developer.work.weixin.qq.com/document/path/95833 + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + UserId string `json:"userid"` // 企业成员userid + Gender string `json:"gender"` // 成员性别 + Avatar string `json:"avatar"` // 头像 + QrCode string `json:"qr_code"` // 二维码 + Mobile string `json:"mobile"` // 手机号 + Mail string `json:"mail"` // 邮箱 + BizMail string `json:"biz_mail"` // 企业邮箱 + Address string `json:"address"` // 地址 +} + // 访问凭据缓存-结构 type AccessTokenCache struct { AccessToken string `json:"access_token"` @@ -72,6 +90,19 @@ type AccessTokenCache struct { UpdateTime time.Time `json:"update_time"` } +// 企业微信用户敏感信息-结构 +type WorkWeixinUserPrivateInfo struct { + UserId string `json:"userid"` // 企业成员userid + Name string `json:"name"` // 姓名 + Gender string `json:"gender"` // 成员性别 + Avatar string `json:"avatar"` // 头像 + QrCode string `json:"qr_code"` // 二维码 + Mobile string `json:"mobile"` // 手机号 + Mail string `json:"mail"` // 邮箱 + BizMail string `json:"biz_mail"` // 企业邮箱 + Address string `json:"address"` // 地址 +} + // 企业微信用户信息-结构 type WorkWeixinDeptUserInfo struct { UserId string `json:"UserId"` // 企业成员UserID @@ -133,12 +164,9 @@ func RequestAccessToken(corpid string, secret string) (cache_token AccessTokenCa } // 获取访问凭据 -func GetAccessToken(is_contact bool) (access_token string, ok bool) { +func GetAccessToken() (access_token string, ok bool) { var cache_token AccessTokenCache cache_key := AccessTokenCacheKey - if is_contact { - cache_key = ContactAccessTokenCacheKey - } err := cache.Get(cache_key, &cache_token) if err == nil { logs.Info("AccessToken从缓存读取成功") @@ -150,11 +178,7 @@ func GetAccessToken(is_contact bool) (access_token string, ok bool) { logs.Debug("corp_id: ", workweixinConfig.CorpId) logs.Debug("agent_id: ", workweixinConfig.AgentId) logs.Debug("secret: ", workweixinConfig.Secret) - logs.Debug("contact_secret: ", workweixinConfig.ContactSecret) secret := workweixinConfig.Secret - if is_contact { - secret = workweixinConfig.ContactSecret - } new_token, ok := RequestAccessToken(workweixinConfig.CorpId, secret) if ok { logs.Debug(new_token) @@ -171,8 +195,8 @@ func GetAccessToken(is_contact bool) (access_token string, ok bool) { } // 获取用户id-请求 -func RequestUserId(access_token string, code string) (user_id string, ok bool) { - url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo" +func RequestUserId(access_token string, code string) (user_id string, ticket string, ok bool) { + url := "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo" req := httplib.Get(url) req.Param("access_token", access_token) // 应用调用接口凭证 req.Param("code", code) // 通过成员授权获取到的code @@ -182,15 +206,63 @@ func RequestUserId(access_token string, code string) (user_id string, ok bool) { _ = resp if err != nil { logs.Error(err) - return "", false + return "", "", false } var uir UserIdResponse err = req.ToJSON(&uir) if err != nil { logs.Error(err) - return "", false + return "", "", false } - return uir.UserId, true + return uir.UserId, uir.UserTicket, uir.UserId != "" +} + +func RequestUserPrivateInfo(access_token, userid, ticket string) (WorkWeixinUserPrivateInfo, error) { + url := "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=" + access_token + req := httplib.Post(url) + body := map[string]string{ + "user_ticket": ticket, + } + b, _ := json.Marshal(body) + req.Body(b) + + req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false}) + req.AddFilters(httpFilter) + resp, err := req.Response() + _ = resp + var uir UserPrivateInfoResponse + var info WorkWeixinUserPrivateInfo + if err != nil { + logs.Error(err) + return info, err + } + err = req.ToJSON(&uir) + if err != nil { + logs.Error(err) + return info, err + } + + if uir.ErrCode != 0 { + return info, errors.New(uir.ErrMsg) + } + + user_info, err, _ := RequestUserInfo(access_token, userid) + if err != nil { + return info, err + } + + info = WorkWeixinUserPrivateInfo{ + UserId: userid, + Name: user_info.Name, + Gender: uir.Gender, + Avatar: uir.Avatar, + QrCode: uir.QrCode, + Mobile: uir.Mobile, + Mail: uir.Mail, + BizMail: uir.BizMail, + Address: uir.Address, + } + return info, nil } /* @@ -198,7 +270,7 @@ func RequestUserId(access_token string, code string) (user_id string, ok bool) { 从2022年8月15日10点开始,“企业管理后台 - 管理工具 - 通讯录同步”的新增IP将不能再调用此接口 url:https://developer.work.weixin.qq.com/document/path/96079 */ -func RequestUserInfo(contact_access_token string, userid string) (user_info WorkWeixinUserInfo, error_msg string, ok bool) { +func RequestUserInfo(contact_access_token string, userid string) (user_info WorkWeixinUserInfo, error_msg error, ok bool) { url := "https://qyapi.weixin.qq.com/cgi-bin/user/get" req := httplib.Get(url) req.Param("access_token", contact_access_token) // 通讯录应用调用接口凭证 @@ -210,7 +282,7 @@ func RequestUserInfo(contact_access_token string, userid string) (user_info Work var info WorkWeixinUserInfo if err != nil { logs.Error(err) - return info, "请求失败", false + return info, err, false } else { logs.Debug(resp_str) } @@ -218,10 +290,10 @@ func RequestUserInfo(contact_access_token string, userid string) (user_info Work err = req.ToJSON(&uir) if err != nil { logs.Error(err) - return info, "请求数据结果错误", false + return info, err, false } if uir.ErrCode != 0 { - return info, uir.ErrMsg, false + return info, errors.New(uir.ErrMsg), false } info = WorkWeixinUserInfo{ UserId: uir.UserId, @@ -237,7 +309,7 @@ func RequestUserInfo(contact_access_token string, userid string) (user_info Work Status: uir.Status, MainDepartment: uir.MainDepartment, } - return info, "", true + return info, nil, true } /* diff --git a/views/account/workweixin-login-callback.tpl b/views/account/auth2_callback.tpl similarity index 96% rename from views/account/workweixin-login-callback.tpl rename to views/account/auth2_callback.tpl index 63531c09..3d10c094 100644 --- a/views/account/workweixin-login-callback.tpl +++ b/views/account/auth2_callback.tpl @@ -39,8 +39,8 @@ window.user_info_json = {{ .user_info_json }}; window.server_error_msg = "{{ .error_msg }}"; window.home_url = "{{ .BaseUrl }}"; - window.workweixin_login_bind = "{{urlfor "AccountController.WorkWeixinLoginBind"}}"; - window.workweixin_login_ignore = "{{urlfor "AccountController.WorkWeixinLoginIgnore"}}"; + window.account_bind = "{{urlfor "AccountController.Auth2BindAccount" ":app" .app}}"; + window.account_auto_create = "{{urlfor "AccountController.Auth2AutoAccount" ":app" .app}}"; @@ -114,7 +114,7 @@ btn: ['绑定','取消'], yes: function(index, layero){ $.ajax({ - url: window.workweixin_login_bind, + url: window.account_bind, type: 'POST', beforeSend: function(request) { request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val()); @@ -165,7 +165,7 @@ }); */ $.ajax({ - url: window.workweixin_login_ignore, + url: window.account_auto_create, type: 'GET', beforeSend: function(request) { request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val()); diff --git a/views/account/login.tpl b/views/account/login.tpl index 9c6d52d4..e2e52350 100644 --- a/views/account/login.tpl +++ b/views/account/login.tpl @@ -14,23 +14,46 @@ - {{if .CanLoginWorkWeixin}} - - {{end}} @@ -87,30 +110,27 @@
- {{if .ENABLE_QR_DINGTALK}} -
- {{i18n .Lang "common.dingtalk_login"}} -
- {{end}} {{if .ENABLED_REGISTER}} - {{if ne .ENABLED_REGISTER "false"}} -
- {{i18n .Lang "message.no_account_yet"}} {{i18n .Lang "common.register"}} -
+ {{if ne .ENABLED_REGISTER "false"}} +
+ {{i18n .Lang "message.no_account_yet"}} {{i18n .Lang "common.register"}} +
+ {{end}} {{end}} - {{end}} - {{if .CanLoginWorkWeixin}} -
-
- 手机企业微信-扫码登录 +
+
+ {{i18n .Lang "common.third_party_login"}} +
+
+
+ {{i18n .Lang +
+
+ {{i18n .Lang +
- {{end}} -
@@ -119,79 +139,6 @@ - - - -{{if .ENABLE_QR_DINGTALK}} - -{{end}}