1
0
Эх сурвалжийг харах

重写Auth2.0登录逻辑 (#851)

* 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
LawyZheng 2 жил өмнө
parent
commit
08d0e1613d

+ 1 - 0
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{})

+ 0 - 12
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"

+ 3 - 1
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

+ 3 - 1
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 = 确认密码

+ 16 - 16
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
 }

+ 897 - 502
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
-			}
+		var schemaRule = regexp.MustCompile(`^https?\:\/\/`)
+		if !schemaRule.MatchString(u) {
+			u = conf.BaseUrl + u
 		}
-		c.Data["url"] = referer
 	}
-}
+	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)
+	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))
 	}
 
-	appKey, _ := web.AppConfig.String("dingtalk_app_key")
-	appSecret, _ := web.AppConfig.String("dingtalk_app_secret")
-	tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader")
+	if can, _ := c.Data["CanLoginDingTalk"].(bool); can {
+		c.Data["dingtalk_login_url"] = conf.URLFor(auth2Redirect, ":app", dingtalk.AppName, "url", url.PathEscape(u))
 
-	if appKey == "" || appSecret == "" || tmpReader == "" {
-		c.JsonResult(500, i18n.Tr(c.Lang, "message.dingtalk_auto_login_not_enable"), nil)
-		c.StopRun()
 	}
+	return
+}
 
-	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()
-	}
+/*
+Auth2.0 第三方对接思路:
+1. Auth2Redirect: 点击相应第三方接口,路由重定向至第三方提供的Auth2.0地址
+2. Auth2Callback: 第三方回调处理,接收回调的授权码,并获取用户信息
+	已绑定: 则读取用户信息,直接登录
+	未绑定: 则弹窗提示(需要敏感信息)
+		a) Auth2BindAccount: 绑定已有账户(用户名+密码)
+		b) Auth2AutoAccount: 自动创建账户,以第三方用户ID作为用户名,密码123456。
+							 用该方式创建的账户,无法使用账号密码登录,需要修改一次密码后才可以进行账号密码登录。
+*/
 
-	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()
+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")
 	}
 
-	username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
+	var tokenCache auth2.AccessTokenCache
+	err := cache.Get(tokenKey, &tokenCache)
 	if err != nil {
-		logs.Warn("获取钉钉用户信息失败 ->", err)
-		c.JsonResult(500, i18n.Tr(c.Lang, "message.failed_auto_login"), nil)
-		c.StopRun()
+		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())
 	}
 
-	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
+	// 处理过期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
+}
 
-		c.SetMember(*member)
+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")
 	}
-	c.JsonResult(0, "ok", username)
+
+	logs.Debug("code: ", code)
+	logs.Debug("state: ", state)
+	return
 }
 
-// WorkWeixinLogin 用户企业微信登录
-func (c *AccountController) WorkWeixinLogin() {
-	logs.Info("UserAgent: ", c.Ctx.Input.UserAgent()) // debug
+func (c *AccountController) getAuth2Account() (models.Auth2Account, error) {
+	switch c.Ctx.Input.Param(":app") {
+	case wecom.AppName:
+		return models.NewWorkWeixinAccount(), nil
 
-	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)
+	case dingtalk.AppName:
+		return models.NewDingTalkAccount(), nil
 	}
-	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()
-			}
-		}
+
+	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
 	}
 
-	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)
+	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, "/")
 		}
-		logs.Info("redirect_uri: ", redirect_uri) // debug
-		c.Redirect(redirect_uri, 302)
+		callback = conf.URLFor("AccountController.Auth2Callback", ":app", app, "url", url.PathEscape(u))
 	}
-}
 
-/*
-思路:
-1. 浏览器打开
-        用户名+密码 登录 与企业微信没有交集
-        手机企业微信登录->扫码页面->扫码后获取用户信息, 判断是否绑定了企业微信
-            已绑定,则读取用户信息,直接登录
-            未绑定,则弹窗提示[未绑定企业微信,请先在企业微信中打开,完成绑定]
-2. 企业微信打开->自动登录->判断是否绑定了企业微信
-        已绑定,则读取用户信息,直接登录
-        未绑定,则弹窗提示
-            是否已有账户(用户名+密码方式)
-                有: 弹窗输入[用户名+密码+验证码]校验
-                无: 直接以企业UserId作为用户名(小写),创建随机密码
-*/
+	logs.Debug("callback: ", callback) // debug
+	c.Redirect(client.BuildURL(callback, isAppBrowser), http.StatusFound)
+}
 
-// WorkWeixinLoginCallback 用户企业微信登录-回调
-func (c *AccountController) WorkWeixinLoginCallback() {
-	c.TplName = "account/workweixin-login-callback.tpl"
+// 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,360 +358,782 @@ 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")
-
-							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 {
-								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
-				}
+	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
+	}
+
+	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
+	}
+
+	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 = "参数错误"
-	}
-	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)
-		}
-	*/
-}
+		return
+	}
 
-// 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)
-							}
-						}
-					}
+	bindExisted = "true"
+	errMsg = ""
 
-				}
+	member.LastLoginTime = time.Now()
+	_ = member.Update("last_login_time")
 
-			} else {
-				logs.Error("用户登录 ->", err)
-				c.JsonResult(500, "账号或密码错误", nil)
-			}
-			c.JsonResult(500, "TODO: 绑定以后账号功能开发中")
-		}
-	} else {
-		if ok {
-			c.DelSession(SessionUserInfoKey)
-		}
-		c.JsonResult(400, "请求错误, 请从首页重新登录")
+	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())
 	}
+	u := c.GetString("url")
+	if u == "" {
+		u = conf.URLFor("HomeController.Index")
+	}
+	c.Redirect(u, 302)
 
 }
 
-// WorkWeixinLoginIgnore 用户企业微信登录-忽略
-func (c *AccountController) WorkWeixinLoginIgnore() {
-	if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserInfo); ok && len(user_info.UserId) > 0 {
+// Auth2BindAccount 第三方auth2.0绑定已有账号
+func (c *AccountController) Auth2BindAccount() {
+	userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo)
+	if !ok || len(userInfo.UserId) <= 0 {
 		c.DelSession(SessionUserInfoKey)
-		member := models.NewMember()
+		c.JsonResult(400, "请求错误, 请从首页重新登录")
+		return
+	}
 
-		if _, err := member.FindByAccount(user_info.UserId); err == nil && member.MemberId > 0 {
-			c.JsonResult(400, "账号已存在")
-		}
+	account := c.GetString("account")
+	password := c.GetString("password")
+	if account == "" || password == "" {
+		c.JsonResult(400, "账号或密码不能为空")
+		return
+	}
 
-		ormer := orm.NewOrm()
-		o, err := ormer.Begin()
-		if err != nil {
-			logs.Error("开启事务时出错 -> ", err)
-			c.JsonResult(500, "开启事务时出错: ", err.Error())
-		}
+	member, err := models.NewMember().Login(account, password)
+	if err != nil {
+		logs.Error("用户登录 ->", err)
+		c.JsonResult(500, "账号或密码错误", nil)
+		return
+	}
 
-		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, "请求错误, 请从首页重新登录")
+	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)
 }
 
-// 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")
+// Auth2AutoAccount auth2.0自动创建账号
+func (c *AccountController) Auth2AutoAccount() {
+	app := c.Ctx.Input.Param(":app")
+	logs.Debug("app: ", app)
 
-		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()
-		}
+	userInfo, ok := c.GetSession(SessionUserInfoKey).(auth2.UserInfo)
+	if !ok || len(userInfo.UserId) <= 0 {
+		c.DelSession(SessionUserInfoKey)
+		c.JsonResult(400, "请求错误, 请从首页重新登录")
+		return
+	}
 
-		appKey, _ = web.AppConfig.String("dingtalk_app_key")
-		appSecret, _ = web.AppConfig.String("dingtalk_app_secret")
-		tmpReader, _ := web.AppConfig.String("dingtalk_tmp_reader")
+	c.DelSession(SessionUserInfoKey)
+	member := models.NewMember()
 
-		dingtalkAgent := dingtalk.NewDingTalkAgent(appSecret, appKey)
-		err = dingtalkAgent.GetAccesstoken()
-		if err != nil {
-			logs.Warn("获取钉钉临时Token失败 ->", err)
-			c.Redirect(conf.URLFor("AccountController.Login"), 302)
-			c.StopRun()
-		}
+	if _, err := member.FindByAccount(userInfo.UserId); err == nil && member.MemberId > 0 {
+		c.JsonResult(400, "账号已存在")
+		return
+	}
 
-		userid, err := dingtalkAgent.GetUserIDByUnionID(unionID)
-		if err != nil {
-			logs.Warn("获取钉钉用户ID失败 ->", err)
-			c.Redirect(conf.URLFor("AccountController.Login"), 302)
-			c.StopRun()
-		}
+	ormer := orm.NewOrm()
+	o, err := ormer.Begin()
+	if err != nil {
+		logs.Error("开启事务时出错 -> ", err)
+		c.JsonResult(500, "开启事务时出错: ", err.Error())
+		return
+	}
 
-		username, avatar, err := dingtalkAgent.GetUserNameAndAvatarByUserID(userid)
-		if err != nil {
-			logs.Warn("获取钉钉用户信息失败 ->", err)
-			c.Redirect(conf.URLFor("AccountController.Login"), 302)
-			c.StopRun()
-		}
+	member.Account = userInfo.UserId
+	member.RealName = userInfo.Name
+	member.Password = "123456" // 强制设置默认密码,需修改一次密码后,才可以进行账号密码登录
+	hash, err := utils.PasswordHash(member.Password)
 
-		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
-			}
+	if err != nil {
+		logs.Error("加密用户密码失败 =>", err)
+		c.JsonResult(500, "加密用户密码失败"+err.Error())
+		return
+	}
 
-			c.SetMember(*member)
-			c.LoggedIn(false)
-			c.StopRun()
-		}
-		c.Redirect(conf.URLFor("AccountController.Login"), 302)
+	logs.Debug("member.Password: ", member.Password)
+	logs.Debug("hash: ", hash)
+	member.Password = hash
 
-	// 企业微信扫码登录
-	case "workweixin":
-		//
+	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
+	}
 
-	default:
-		c.Redirect(conf.URLFor("AccountController.Login"), 302)
-		c.StopRun()
+	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.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.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) LoggedIn(isPost bool) interface{} {
 

+ 3 - 0
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())
 		}

+ 185 - 0
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
+}

+ 20 - 12
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()

+ 0 - 74
models/WorkWeixinAccount.go

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

+ 7 - 6
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")

+ 89 - 0
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()
+}

+ 234 - 0
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
+}

+ 285 - 0
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
+}

+ 98 - 26
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
 }
 
 /*

+ 4 - 4
views/account/workweixin-login-callback.tpl → 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}}";
     </script>
 </head>
 <body class="manual-container">
@@ -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());

+ 60 - 106
views/account/login.tpl

@@ -14,23 +14,46 @@
     <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
-    {{if .CanLoginWorkWeixin}}
-    <style type="text/css">
-        #wxwork-login-line > a {
-            display: block;
-            text-align: center;
-            border: 1px solid #ccc;
-            border-radius: 0.3em;
-            padding-top: 0.8em;
-            padding-bottom: 0.75em;
+    <style>
+        .line {
+            height:0;
+            border-top: 1px solid #cccccc;
+            text-align:center;
+            margin: 14px 0;
         }
-        #wxwork-login-line > a:hover {
-            color: #fff;
-            background-color: #5cb85c;
-            border-color: #4cae4c;
+        .line > .text {
+            position:relative;
+            top:-12px;
+            background-color:#fff;
+            padding: 5px;
+        }
+        .icon-box {
+            align-items: center;
+            justify-content: center;
+            display: flex;
+            display: -webkit-flex;
+        }
+
+        .icon {
+            box-sizing: border-box;
+            display: inline-block;
+            padding: 10px;
+            border-radius: 50%;
+            cursor: pointer;
+            margin: 0 5px;
+        }
+        .icon-disable {
+            background-color: #cccccc;
+            cursor: not-allowed;
+        }
+        .icon-disable:hover {
+            background-color: #bbbbbb;
+        }
+
+        .icon > img {
+            height: 24px;
         }
     </style>
-    {{end}}
     <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
     <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
 </head>
@@ -87,30 +110,27 @@
                 <div class="form-group">
                     <button type="button" id="btn-login" class="btn btn-success" style="width: 100%"  data-loading-text="{{i18n .Lang "common.logging_in"}}" autocomplete="off">{{i18n .Lang "common.login"}}</button>
                 </div>
-                {{if .ENABLE_QR_DINGTALK}}
-                <div class="form-group">
-                    <a id="btn-dingtalk-qr" class="btn btn-default" style="width: 100%" data-loading-text="" autocomplete="off">{{i18n .Lang "common.dingtalk_login"}}</a>
-                </div>
-                {{end}}
                 {{if .ENABLED_REGISTER}}
-                {{if ne .ENABLED_REGISTER "false"}}
-                <div class="form-group">
-                    {{i18n .Lang "message.no_account_yet"}} <a href="{{urlfor "AccountController.Register" }}" title={{i18n .Lang "common.register"}}>{{i18n .Lang "common.register"}}</a>
-                </div>
-                {{end}}
+                    {{if ne .ENABLED_REGISTER "false"}}
+                        <div class="form-group">
+                            {{i18n .Lang "message.no_account_yet"}} <a href="{{urlfor "AccountController.Register" }}" title={{i18n .Lang "common.register"}}>{{i18n .Lang "common.register"}}</a>
+                        </div>
+                    {{end}}
                 {{end}}
-                {{if .CanLoginWorkWeixin}}
-                <div class="form-group">
-                    <div id="wxwork-login-line">
-                        <a href="{{ .workweixin_login_url }}" title="手机企业微信-扫码登录">手机企业微信-扫码登录</a>
+                <div class="third-party">
+                    <div class="line">
+                        <span class="text">{{i18n .Lang "common.third_party_login"}}</span>
+                    </div>
+                    <div class="icon-box">
+                        <div class="icon {{ if .CanLoginDingTalk }}btn-success{{else}}icon-disable{{end}}" title="{{i18n .Lang "common.dingtalk_login"}}" data-url="{{ .dingtalk_login_url }}">
+                            <img alt="{{i18n .Lang "common.dingtalk_login"}}" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAA9hJREFUaEPNmVvoZ1MUxz/fPOBR8uJBya0QXmRqhsZtasq4xQMP7h5GyiR5MIkHlOQBDyaEcmvGrTBCYjDuKbmUW3jyQF7kEsrSd9pHx+9/fufss8/5/85v1e7/6/9ba+31OXutfdbeP9EgEfE6cChwN3CXpL+a9Jbhf2oBWJ+++wN42DCSvliGoOsx5ADU9R8zjKRXlwWkL0AV905gO7BD0p9TwswDuAG4NSOwrxPIdkmfZeiPrjIP4Fjg456z7TCMpGd62g1SbwSwx4h4DzihwLvBnV6G+a7AvpdJG8BW4JZe3v6v/HsN5OUBflpN2wCOBj4daeLdLvi0Kj+O5HOPm7kAKY2eAzaNOOHPgH3uGZL+Geq7C8AvM7+VV0O8gxnkeUlvlE7QCpBW4QrgYmBd6SQZdh8ATwFPSvo+Q/8/lU6ASjMizksgZ/SZoKfub4ZIIC/m2GYD1ECOAs5MY03OJIU6HwIPSLqvzb6rBo4A3MB9A3wOvAXslvR+Sq8TU5EbyLqrIR8BWyR5J1shnSsQEd8CBzfYurhdfIbyOBXYmMZhq0CyTtLbs35zAO4HXMg58mgqRgPdNDKMt92zSgAuAB7Pib5B5wfAUG7Dz01Qha7YJenk3gAp118BTi+deSS7OyVdVwrg4A0xpWyU9FIRQFqFmwemwBD4JyRdWLQL1Y0iYiqINdXWXbwClWFEXAVcCxwy5JH2sPWtyJZ5+p3baJNhRBwAHAfsm8Y+M3/3ArwD1Yfb876NoVtvP/25B6MigJm02h/wEdTDUNXnHg95rupWSbe1OSoGSPVwDnDMGJE2+PAlgZ++G7y5MgTAbUNWx1gIeJmkh7psiwHS1upj4vldkxR8/7Qkt++dMghglSB+AU6R5C60UwYDJIgx3w83Ssq+DRkFIEG4oO9oeT/8BPhK0nXj1GuSN4HTJP3d+eiTwmgACWI/YENq/KoXnS/IdlYHkogw5IqmLMWzSdILucFbb1SAnIkjwoegkxp0d82zb2qjK92FAkTEQcCXgN/cOfIrcHzb7xKLBvDW6FuHXLlI0iNtyosGuB24PjN6/yJ0TZfuogFeA1YcCxuCfEfS2q7gF1rEEXE4cGBDUN5W3dVW4rxfK+mTpQJoCiYiLgFm+53LJT2YE/xCV2AOgG/drqx9d68kH5iyZaE1MBtVRLhl9lWlJTvv634mA4iII9N1peNx3q/PbeCWBeBq4J4UzGZJ27LzpqY45Qo8C5wNbJO0uST4SYs4ItydfpVSJ7v7nAWdZAVST+Tr+g2S3i19+pOtQERcCuxdmveTF3FEeL8vzvs6wL8iyC9AZnHsagAAAABJRU5ErkJggg==">
+                        </div>
+                        <div class="icon {{ if .CanLoginWorkWeixin }}btn-success{{else}}icon-disable{{end}}" title="{{i18n .Lang "common.wecom_login"}}" data-url="{{ .workweixin_login_url }}">
+                            <img alt="{{i18n .Lang "common.wecom_login"}}" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABE9JREFUaEPtmVvIpWMUx3//IkkYhgkXTBiFHEPjVCYXJscJMyOHMEUZN0NOuWEuJCPHhBnqk0MuhmYaoswFQoYoTAqNHBqH0IyRK9Jfa/e80/u93/vt5917v/vb31ez6m1fPOv0f9Z6nrWetcUMJ81w/xkIgO3DgTOBY4H9St8fwPfAD+l3o6T/hrFZPQOwfSFwMXBGcryJX/8CLwFvAgFmexOhJjyNASTHbwQuaqK4C8/PwDPAakm/DKgrn0K25wKPAIsGNVaRL4DcJyki1Bd1jYDt84ExYE5f2psJvQ8s6TcakwKwfTuwqpkPA3NtAxZI+qJXTbUAbJ8DvN2rshb4T5H0aS96JgCwfRTwdS9KWuY9UFJcw41oHADbewMbgIjAqGiDpEuaGq8CeBK4qanwEPlWSHqsif6dAGwfCsQh2reJ4JB5vgPmS/otZ6cM4C7g/pzAFK4vl/RUzl4ZQOz+cTmBKVx/VdLlOXsdACl9ovGaTrRd0v45hwoAZwHv5ZhHsD5L0o5udgsAV6ZucQQ+djU5T9KWJgCmwwF+AViTimg0d1GLfpW0qQmA61LTNqoIrJW0pB/jRQqdCnzcj4IWZDZLOj5dJvsApwOHAW9JilddVyoA7AX8CeyWExjC+kpJ9yYAH6SXXmHmaElfZVMoCX8GnDAEB3Mqo41+x/bVQJyDMq2TdGlTALEL9+SsDWG9ABAvvnUV/f8AR0jaOpndciU+GPgEOGQITtapjPy+W9LLKQNmRd4DcR7LdK2k57MAkpKpiMJK4MP4JP2V7M4HFsZZsB1ZsLg08YhrNNZqC1q1nR5mFOIwLpI07rFk+/o0NFgj6Y5ip20HiOWpHoxJWlYXhboX2Q2poLSZSTskRYp0yHZcm2enbynwmqSYNU0g20VWzJH0e5VhsjfxQ8CtLSJYJmnM9pHAw5XZUgy5TuvWMqRmMyYj8eA6Js2V3pW0vttU4nXgghZAbJIUxSl2vu6MNWqbbb8IXFXyp6O3G4DYrY1ADLYGoS2S5iUAMemovrezDxfbJwN104oFucFWvNCi0RuEduZ/zazpR+AkSTEXmpRs3wY8WGHYJml2DkDc0VcM4n2SvUXSoykKNwNPAF8CjwPfRCXOAIih8MISz0fA05KeywGI+zru6DZoD0lRWceRbadOOEBOuOttr0jX7OfA+phwSwoAHcoBiOnxQTXeB7ComnsCB6RvduV3MxDft0C0AlslvVEDoHAwClZEJhzspJTtE9OEcG2q2hMGXjkAsTtlioP0gKRQ2BrZXg3E6D4onI+UCTC7A+cW3WqdwRyA4tYIx6NSxoupdUr/9ESlDocL+htYWhe1sgM5APHAmNvP1LhXlLZfAS6ryD0rKTqDSanxPzS9OtQrv+1rgGrXuUrSnTMCQDq05bMQF8ViST/NGAAJRLQd53U7uI3PQK9pMAr+aXMG+gW/C0C/O9eW3K4ItLWT/eqZ8RH4H4Zge30AMjOdAAAAAElFTkSuQmCC">
+                        </div>
                     </div>
                 </div>
-                {{end}}
             </form>
-            <div class="form-group dingtalk-container" style="display: none;">
-                <div id="dingtalk-qr-container"></div>
-                <a class="btn btn-default btn-dingtalk" style="width: 100%" data-loading-text="" autocomplete="off">{{i18n .Lang "message.return_account_login"}}</a>
-            </div>
         </div>
     </div>
     <div class="clearfix"></div>
@@ -119,79 +139,6 @@
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
 <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
-<script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
-<script src="{{cdnjs "/static/js/dingtalk-ddlogin.js"}}" type="text/javascript"></script>
-
-{{if .ENABLE_QR_DINGTALK}}
-<script type="text/javascript">
-    if (dd.env.platform !== "notInDingTalk"){
-        dd.ready(function() {
-            dd.runtime.permission.requestAuthCode({
-                corpId: {{ .corpID }} , // 企业id
-                onSuccess: function (info) {
-                    var index = layer.load(1, {
-                        shade: [0.1, '#fff'] // 0.1 透明度的白色背景
-                    })
-
-                    var formData = $("form").serializeArray()
-                    formData.push({"name": "dingtalk_code", "value": info.code})
-
-                    $.ajax({
-                        url: "{{urlfor "AccountController.DingTalkLogin"}} ",
-                        data: formData,
-                        dataType: "json",
-                        type: "POST",
-                        complete: function(){
-                            layer.close(index)
-                        },
-                        success: function (res) {
-                            if (res.errcode !== 0) {
-                                layer.msg(res.message)
-                            } else {
-                                window.location = "{{ urlfor "HomeController.Index"  }}"
-                            }
-                        },
-                        error: function (res) {
-                            layer.msg("发生异常")
-                        }
-                    })
-                }
-            });
-        });
-    }
-
-    $(document).ready(function () {
-        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"
-        });
-        $(window).on('message', 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
-            }
-        });
-        $("#btn-dingtalk-qr").on('click', function(){
-            $('form').hide()
-            $(".dingtalk-container").show()
-        })
-
-        $(".btn-dingtalk").on('click', function(){
-            $('form').show()
-            $(".dingtalk-container").hide()
-        })
-    });
-</script>
-{{end}}
 
 <script type="text/javascript">
     $(document).ready(function () {
@@ -206,6 +153,13 @@
             }
         });
 
+        $(".icon").on('click', function (){
+           if ($(this).hasClass("icon-disable")) {
+               return;
+           }
+           window.location.href = $(this).data("url");
+        })
+
         $("#btn-login").on('click', function () {
             $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
             var $btn = $(this).button('loading');