重写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
pull/866/head
LawyZheng 2023-04-20 13:24:28 +08:00 committed by GitHub
parent 725b6ac24e
commit 08d0e1613d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1936 additions and 791 deletions

View File

@ -138,6 +138,7 @@ func RegisterModel() {
new(models.Itemsets), new(models.Itemsets),
new(models.Comment), new(models.Comment),
new(models.WorkWeixinAccount), new(models.WorkWeixinAccount),
new(models.DingTalkAccount),
) )
gob.Register(models.Blog{}) gob.Register(models.Blog{})
gob.Register(models.Document{}) gob.Register(models.Document{})

View File

@ -232,15 +232,6 @@ dingtalk_app_key="${MINDOC_DINGTALK_APPKEY}"
# 钉钉AppSecret # 钉钉AppSecret
dingtalk_app_secret="${MINDOC_DINGTALK_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 # 企业ID
@ -252,8 +243,5 @@ workweixin_agentid="${MINDOC_WORKWEIXIN_AGENTID}"
# 应用密钥 # 应用密钥
workweixin_secret="${MINDOC_WORKWEIXIN_SECRET}" workweixin_secret="${MINDOC_WORKWEIXIN_SECRET}"
# 通讯录密钥
workweixin_contact_secret="${MINDOC_WORKWEIXIN_CONTACT_SECRET}"
# i18n config # i18n config
default_lang="zh-cn" default_lang="zh-cn"

View File

@ -22,7 +22,9 @@ captcha = Captcha
keep_login = Stay signed in keep_login = Stay signed in
forgot_password = Forgot password? forgot_password = Forgot password?
register = Create New Account 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 account_recovery = Account recovery
new_password = New password new_password = New password
confirm_password = Confirm password confirm_password = Confirm password

View File

@ -22,7 +22,9 @@ captcha = 验证码
keep_login = 保持登录 keep_login = 保持登录
forgot_password = 忘记密码? forgot_password = 忘记密码?
register = 立即注册 register = 立即注册
dingtalk_login = 扫码登录 third_party_login = 第三方登录
dingtalk_login = 钉钉登录
wecom_login = 企业微信登录
account_recovery = 找回密码 account_recovery = 找回密码
new_password = 新密码 new_password = 新密码
confirm_password = 确认密码 confirm_password = 确认密码

View File

@ -1,27 +1,27 @@
package conf package conf
import ( import (
"github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web"
) )
type WorkWeixinConf struct { type WorkWeixinConf struct {
CorpId string // 企业ID CorpId string // 企业ID
AgentId string // 应用ID AgentId string // 应用ID
Secret string // 应用密钥 Secret string // 应用密钥
ContactSecret string // 通讯录密钥 // ContactSecret string // 通讯录密钥
} }
func GetWorkWeixinConfig() *WorkWeixinConf { func GetWorkWeixinConfig() *WorkWeixinConf {
corpid, _ := web.AppConfig.String("workweixin_corpid") corpid, _ := web.AppConfig.String("workweixin_corpid")
agentid, _ := web.AppConfig.String("workweixin_agentid") agentid, _ := web.AppConfig.String("workweixin_agentid")
secret, _ := web.AppConfig.String("workweixin_secret") secret, _ := web.AppConfig.String("workweixin_secret")
contact_secret, _ := web.AppConfig.String("workweixin_contact_secret") // contact_secret, _ := web.AppConfig.String("workweixin_contact_secret")
c := &WorkWeixinConf{ c := &WorkWeixinConf{
CorpId: corpid, CorpId: corpid,
AgentId: agentid, AgentId: agentid,
Secret: secret, Secret: secret,
ContactSecret: contact_secret, // ContactSecret: contact_secret,
} }
return c return c
} }

File diff suppressed because it is too large Load Diff

View File

@ -79,6 +79,9 @@ func (c *SettingController) Password() {
c.JsonResult(6007, i18n.Tr(c.Lang, "message.pwd_encrypt_failed")) c.JsonResult(6007, i18n.Tr(c.Lang, "message.pwd_encrypt_failed"))
} }
c.Member.Password = pwd c.Member.Password = pwd
if c.Member.AuthMethod == "" {
c.Member.AuthMethod = "local"
}
if err := c.Member.Update(); err != nil { if err := c.Member.Update(); err != nil {
c.JsonResult(6008, err.Error()) c.JsonResult(6008, err.Error())
} }

View File

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

View File

@ -90,7 +90,6 @@ func (m *Member) Login(account string, password string) (*Member, error) {
} }
switch member.AuthMethod { switch member.AuthMethod {
case "":
case "local": case "local":
ok, err := utils.PasswordVerify(member.Password, password) ok, err := utils.PasswordVerify(member.Password, password)
if ok && err == nil { if ok && err == nil {
@ -109,15 +108,15 @@ func (m *Member) Login(account string, password string) (*Member, error) {
} }
// TmpLogin 用于钉钉临时登录 // TmpLogin 用于钉钉临时登录
func (m *Member) TmpLogin(account string) (*Member, error) { //func (m *Member) TmpLogin(account string) (*Member, error) {
o := orm.NewOrm() // o := orm.NewOrm()
member := &Member{} // member := &Member{}
err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member) // err := o.Raw("select * from md_members where account = ? and status = 0 limit 1;", account).QueryRow(member)
if err != nil { // if err != nil {
return member, ErrorMemberPasswordError // return member, ErrorMemberPasswordError
} // }
return member, nil // return member, nil
} //}
// ldapLogin 通过LDAP登陆 // ldapLogin 通过LDAP登陆
func (m *Member) ldapLogin(account string, password string) (*Member, error) { 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() ormer := orm.NewOrm()
o, err := ormer.Begin() o, err := ormer.Begin()
if err != nil { if err != nil {
return err 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() _, err = o.Raw("DELETE FROM md_members WHERE member_id = ?", oldId).Exec()
if err != nil { if err != nil {
o.Rollback() o.Rollback()
return err 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 { if err != nil {
o.Rollback() o.Rollback()

View File

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

View File

@ -123,12 +123,13 @@ func init() {
web.Router("/", &controllers.HomeController{}, "*:Index") web.Router("/", &controllers.HomeController{}, "*:Index")
web.Router("/login", &controllers.AccountController{}, "*:Login") web.Router("/login", &controllers.AccountController{}, "*:Login")
web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin") web.Router("/auth2/redirect/:app", &controllers.AccountController{}, "*:Auth2Redirect")
web.Router("/workweixin-login", &controllers.AccountController{}, "*:WorkWeixinLogin") web.Router("/auth2/callback/:app", &controllers.AccountController{}, "*:Auth2Callback")
web.Router("/workweixin-callback", &controllers.AccountController{}, "*:WorkWeixinLoginCallback") web.Router("/auth2/account/bind/:app", &controllers.AccountController{}, "*:Auth2BindAccount")
web.Router("/workweixin-bind", &controllers.AccountController{}, "*:WorkWeixinLoginBind") web.Router("/auth2/account/auto/:app", &controllers.AccountController{}, "*:Auth2AutoAccount")
web.Router("/workweixin-ignore", &controllers.AccountController{}, "*:WorkWeixinLoginIgnore")
web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin") //web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
//web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin")
web.Router("/logout", &controllers.AccountController{}, "*:Logout") web.Router("/logout", &controllers.AccountController{}, "*:Logout")
web.Router("/register", &controllers.AccountController{}, "*:Register") web.Router("/register", &controllers.AccountController{}, "*:Register")
web.Router("/find_password", &controllers.AccountController{}, "*:FindPassword") web.Router("/find_password", &controllers.AccountController{}, "*:FindPassword")

View File

@ -0,0 +1,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()
}

View File

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

View File

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

View File

@ -3,7 +3,9 @@ package workweixin
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
// "encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"time" "time"
@ -17,8 +19,8 @@ import (
// - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313 // - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313
const ( const (
AccessTokenCacheKey = "access-token-cache-key" AccessTokenCacheKey = "access-token-cache-key"
ContactAccessTokenCacheKey = "contact-access-token-cache-key" // ContactAccessTokenCacheKey = "contact-access-token-cache-key"
) )
// 获取访问凭据-请求响应结构 // 获取访问凭据-请求响应结构
@ -31,11 +33,13 @@ type AccessTokenResponse struct {
// 获取用户Id-请求响应结构 // 获取用户Id-请求响应结构
type UserIdResponse struct { type UserIdResponse struct {
ErrCode int `json:"errcode"` // 接口文档: https://developer.work.weixin.qq.com/document/path/91023
ErrMsg string `json:"errmsg"` ErrCode int `json:"errcode"`
UserId string `json:"UserId"` // 企业成员UserID ErrMsg string `json:"errmsg"`
OpenId string `json:"OpenId"` // 非企业成员的标识,对当前企业唯一 UserId string `json:"userid"` // 企业成员UserID
DeviceId string `json:"DeviceId"` // 设备号 UserTicket string `json:"user_ticket"` // 用于获取敏感信息
OpenId string `json:"openid"` // 非企业成员的标识,对当前企业唯一
ExternalUserId string `json:"external_userid"` // 外部联系人ID
} }
// 获取成员ID列表-请求响应结构 // 获取成员ID列表-请求响应结构
@ -65,6 +69,20 @@ type UserInfoResponse struct {
MainDepartment int `json:"main_department"` // 主部门 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 { type AccessTokenCache struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@ -72,6 +90,19 @@ type AccessTokenCache struct {
UpdateTime time.Time `json:"update_time"` 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 { type WorkWeixinDeptUserInfo struct {
UserId string `json:"UserId"` // 企业成员UserID 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 var cache_token AccessTokenCache
cache_key := AccessTokenCacheKey cache_key := AccessTokenCacheKey
if is_contact {
cache_key = ContactAccessTokenCacheKey
}
err := cache.Get(cache_key, &cache_token) err := cache.Get(cache_key, &cache_token)
if err == nil { if err == nil {
logs.Info("AccessToken从缓存读取成功") 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("corp_id: ", workweixinConfig.CorpId)
logs.Debug("agent_id: ", workweixinConfig.AgentId) logs.Debug("agent_id: ", workweixinConfig.AgentId)
logs.Debug("secret: ", workweixinConfig.Secret) logs.Debug("secret: ", workweixinConfig.Secret)
logs.Debug("contact_secret: ", workweixinConfig.ContactSecret)
secret := workweixinConfig.Secret secret := workweixinConfig.Secret
if is_contact {
secret = workweixinConfig.ContactSecret
}
new_token, ok := RequestAccessToken(workweixinConfig.CorpId, secret) new_token, ok := RequestAccessToken(workweixinConfig.CorpId, secret)
if ok { if ok {
logs.Debug(new_token) logs.Debug(new_token)
@ -171,8 +195,8 @@ func GetAccessToken(is_contact bool) (access_token string, ok bool) {
} }
// 获取用户id-请求 // 获取用户id-请求
func RequestUserId(access_token string, code string) (user_id string, ok bool) { func RequestUserId(access_token string, code string) (user_id string, ticket string, ok bool) {
url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo" url := "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
req := httplib.Get(url) req := httplib.Get(url)
req.Param("access_token", access_token) // 应用调用接口凭证 req.Param("access_token", access_token) // 应用调用接口凭证
req.Param("code", code) // 通过成员授权获取到的code req.Param("code", code) // 通过成员授权获取到的code
@ -182,15 +206,63 @@ func RequestUserId(access_token string, code string) (user_id string, ok bool) {
_ = resp _ = resp
if err != nil { if err != nil {
logs.Error(err) logs.Error(err)
return "", false return "", "", false
} }
var uir UserIdResponse var uir UserIdResponse
err = req.ToJSON(&uir) err = req.ToJSON(&uir)
if err != nil { if err != nil {
logs.Error(err) 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) {
202281510 - - IP 202281510 - - IP
url:https://developer.work.weixin.qq.com/document/path/96079 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" url := "https://qyapi.weixin.qq.com/cgi-bin/user/get"
req := httplib.Get(url) req := httplib.Get(url)
req.Param("access_token", contact_access_token) // 通讯录应用调用接口凭证 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 var info WorkWeixinUserInfo
if err != nil { if err != nil {
logs.Error(err) logs.Error(err)
return info, "请求失败", false return info, err, false
} else { } else {
logs.Debug(resp_str) logs.Debug(resp_str)
} }
@ -218,10 +290,10 @@ func RequestUserInfo(contact_access_token string, userid string) (user_info Work
err = req.ToJSON(&uir) err = req.ToJSON(&uir)
if err != nil { if err != nil {
logs.Error(err) logs.Error(err)
return info, "请求数据结果错误", false return info, err, false
} }
if uir.ErrCode != 0 { if uir.ErrCode != 0 {
return info, uir.ErrMsg, false return info, errors.New(uir.ErrMsg), false
} }
info = WorkWeixinUserInfo{ info = WorkWeixinUserInfo{
UserId: uir.UserId, UserId: uir.UserId,
@ -237,7 +309,7 @@ func RequestUserInfo(contact_access_token string, userid string) (user_info Work
Status: uir.Status, Status: uir.Status,
MainDepartment: uir.MainDepartment, MainDepartment: uir.MainDepartment,
} }
return info, "", true return info, nil, true
} }
/* /*

View File

@ -39,8 +39,8 @@
window.user_info_json = {{ .user_info_json }}; window.user_info_json = {{ .user_info_json }};
window.server_error_msg = "{{ .error_msg }}"; window.server_error_msg = "{{ .error_msg }}";
window.home_url = "{{ .BaseUrl }}"; window.home_url = "{{ .BaseUrl }}";
window.workweixin_login_bind = "{{urlfor "AccountController.WorkWeixinLoginBind"}}"; window.account_bind = "{{urlfor "AccountController.Auth2BindAccount" ":app" .app}}";
window.workweixin_login_ignore = "{{urlfor "AccountController.WorkWeixinLoginIgnore"}}"; window.account_auto_create = "{{urlfor "AccountController.Auth2AutoAccount" ":app" .app}}";
</script> </script>
</head> </head>
<body class="manual-container"> <body class="manual-container">
@ -114,7 +114,7 @@
btn: ['绑定','取消'], btn: ['绑定','取消'],
yes: function(index, layero){ yes: function(index, layero){
$.ajax({ $.ajax({
url: window.workweixin_login_bind, url: window.account_bind,
type: 'POST', type: 'POST',
beforeSend: function(request) { beforeSend: function(request) {
request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val()); request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val());
@ -165,7 +165,7 @@
}); });
*/ */
$.ajax({ $.ajax({
url: window.workweixin_login_ignore, url: window.account_auto_create,
type: 'GET', type: 'GET',
beforeSend: function(request) { beforeSend: function(request) {
request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val()); request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val());

View File

@ -14,23 +14,46 @@
<link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet"> <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/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet"> <link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
{{if .CanLoginWorkWeixin}} <style>
<style type="text/css"> .line {
#wxwork-login-line > a { height:0;
display: block; border-top: 1px solid #cccccc;
text-align: center; text-align:center;
border: 1px solid #ccc; margin: 14px 0;
border-radius: 0.3em;
padding-top: 0.8em;
padding-bottom: 0.75em;
} }
#wxwork-login-line > a:hover { .line > .text {
color: #fff; position:relative;
background-color: #5cb85c; top:-12px;
border-color: #4cae4c; 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> </style>
{{end}}
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script> <script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
</head> </head>
@ -87,30 +110,27 @@
<div class="form-group"> <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> <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> </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 .ENABLED_REGISTER}}
{{if ne .ENABLED_REGISTER "false"}} {{if ne .ENABLED_REGISTER "false"}}
<div class="form-group"> <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> {{i18n .Lang "message.no_account_yet"}} <a href="{{urlfor "AccountController.Register" }}" title={{i18n .Lang "common.register"}}>{{i18n .Lang "common.register"}}</a>
</div> </div>
{{end}}
{{end}} {{end}}
{{end}} <div class="third-party">
{{if .CanLoginWorkWeixin}} <div class="line">
<div class="form-group"> <span class="text">{{i18n .Lang "common.third_party_login"}}</span>
<div id="wxwork-login-line"> </div>
<a href="{{ .workweixin_login_url }}" title="手机企业微信-扫码登录">手机企业微信-扫码登录</a> <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>
</div> </div>
{{end}}
</form> </form>
<div class="form-group dingtalk-container" style="display: none;">
<div id="dingtalk-qr-container"></div>
<a class="btn btn-default btn-dingtalk" style="width: 100%" data-loading-text="" autocomplete="off">{{i18n .Lang "message.return_account_login"}}</a>
</div>
</div> </div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
@ -119,79 +139,6 @@
<!-- Include all compiled plugins (below), or include individual files as needed --> <!-- 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/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script> <script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/dingtalk-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"> <script type="text/javascript">
$(document).ready(function () { $(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 () { $("#btn-login").on('click', function () {
$(this).tooltip('destroy').parents('.form-group').removeClass('has-error'); $(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
var $btn = $(this).button('loading'); var $btn = $(this).button('loading');