From 421240ac541cdcfb0b213c285196b4423ce0183f Mon Sep 17 00:00:00 2001 From: gazebo Date: Fri, 1 Mar 2019 17:43:17 +0800 Subject: [PATCH] - first edition of auth2 --- business/auth2/auth2.go | 293 ++++++++++++++++++ business/auth2/auth_info.go | 32 ++ business/auth2/authprovider/mobile/mobile.go | 86 +++++ .../auth2/authprovider/password/password.go | 55 ++++ business/auth2/authprovider/weixin/weixin.go | 80 +++++ .../auth2/authprovider/weixin/weixin_mini.go | 76 +++++ business/auth2/defauther.go | 37 +++ business/jxcallback/auth/auth.go | 8 + business/jxcallback/auth/weixin/weixin.go | 8 +- business/jxstore/cms/user.go | 4 +- business/jxutils/jxcontext/jxcontext.go | 29 +- business/model/auth2.go | 30 ++ business/model/dao/dao_auth2.go | 11 + business/model/user.go | 43 +++ controllers/auth2.go | 174 +++++++++++ routers/commentsRouter_controllers.go | 56 ++++ routers/router.go | 5 + 17 files changed, 1015 insertions(+), 12 deletions(-) create mode 100644 business/auth2/auth2.go create mode 100644 business/auth2/auth_info.go create mode 100644 business/auth2/authprovider/mobile/mobile.go create mode 100644 business/auth2/authprovider/password/password.go create mode 100644 business/auth2/authprovider/weixin/weixin.go create mode 100644 business/auth2/authprovider/weixin/weixin_mini.go create mode 100644 business/auth2/defauther.go create mode 100644 business/model/auth2.go create mode 100644 business/model/dao/dao_auth2.go create mode 100644 business/model/user.go create mode 100644 controllers/auth2.go diff --git a/business/auth2/auth2.go b/business/auth2/auth2.go new file mode 100644 index 000000000..df846af78 --- /dev/null +++ b/business/auth2/auth2.go @@ -0,0 +1,293 @@ +package auth2 + +import ( + "errors" + "regexp" + "strings" + "time" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/globals" + "git.rosy.net.cn/jx-callback/globals/api" +) + +const ( + UserIDEmpty = "" + UserIDID = "userid" + UserIDID2 = "userid2" + UserIDMobile = "mobile" + UserIDEmail = "email" +) + +const ( + TokenVer = "V2" + TokenTypeSep = "." + TokenUserEmpty = "NULL" +) + +const ( + AuthTypeNone = "" + AuthTypePassword = "password" + AuthTypeEmail = "email" + AuthTypeMobile = "mobile" +) + +const ( + DefTokenDuration = 7 * 24 * time.Hour // 7天 +) + +type IUser interface { + GetID() string // 这个ID是不可变的,系统内部使用的唯一标识 + GetID2() string // 这个是可改的,唯一的,用户设置的用户名 + GetMobile() string + GetEmail() string + GetName() string +} + +type IUserProvider interface { + GetUser(authID, authIDType string) (user IUser) +} + +type IAuther interface { + SendVerifyCode(authID string) (err error) + // 负责验证secret,并找到相应的用户返回(password,email,mobile类型的不负责用户查找)如果找不到用户UserID为空 + VerifySecret(authID, authSecret string) (authBind *model.AuthBind, err error) + AddAuthBind(authBind *model.AuthBind, userName string) (err error) + UnbindAuth(authInfo *AuthInfo, authType string) (err error) + Logout(authInfo *AuthInfo) (err error) +} + +var ( + authers map[string]IAuther + userProvider IUserProvider + authTypeGuesserMap = map[string]*regexp.Regexp{ + AuthTypeEmail: regexp.MustCompile(`^[A-Za-z0-9_\-\.]+\@[A-Za-z0-9_\-]+(\.[A-Za-z]+){1,5}$`), + AuthTypeMobile: regexp.MustCompile(`^1[34578]\d{9}$`), + } +) + +var ( + ErrInternalErrror = errors.New("内部错误") + ErrTokenIsInvalid = errors.New("Token非法") + ErrUserAlreadyExist = errors.New("用户已经存在") + ErrUserNotExist = errors.New("用户不存在") + ErrIllegalAuthType = errors.New("非法的登录类型") + ErrIllegalAuthTypeAlreadyExist = errors.New("要登录类型已经存在") +) + +func init() { + authers = make(map[string]IAuther) +} + +func Init(userProvider2 IUserProvider) { + userProvider = userProvider2 +} + +func RegisterAuther(authType string, handler IAuther) { + authers[authType] = handler +} + +func CreateauthInfo(user IUser, authBindInfo *model.AuthBind, userData interface{}) (authInfo *AuthInfo) { + token, tokenType := createToken(user) + authInfo = &AuthInfo{ + IUser: user, + AuthBindInfo: authBindInfo, + + LoginTime: time.Now(), + ExpiresIn: time.Now().Add(DefTokenDuration).Unix(), + Token: token, + TokenType: tokenType, + UserData: userData, + } + if user != nil { + globals.SugarLogger.Debugf("CreateauthInfo id:%s, id2:%s, authBindInfo:%s, authInfo:%s", authInfo.GetID(), authInfo.GetID2(), authInfo.GetMobile(), utils.Format4Output(authBindInfo, true), utils.Format4Output(authInfo, true)) + } else { + globals.SugarLogger.Debugf("CreateauthInfo authBindInfo:%s, authInfo:%s", utils.Format4Output(authBindInfo, true), utils.Format4Output(authInfo, true)) + } + SetUserInfo(token, authInfo, DefTokenDuration) + return authInfo +} + +// 账号密码时:authIDType可能是:UserIDID,UserIDID2,UserIDMobile,UserIDEmail,authSecret是密码的sha1 +// 邮箱时(如果允许):authIDType是:UserIDEmail,authSecret是验证码的sha1 +// 手机时(如果允许):authIDType是:UserIDMobile,authSecret是验证码的sha1 +// 公众号登录:authIDTypeD是UserIDEmpty,authSecret是code(这个函数是被微信的回调调用,不是直接被客户端调用) +// 微信登录:authIDType是UserIDEmpty,authSecret是code(这个函数是被微信的回调调用,不是直接被客户端调用) +// 小程序登录:authIDType是UserIDEmpty,authSecret是jsCode +func Login(authType, authID, authIDType, authSecret string) (authInfo *AuthInfo, err error) { + if handler := authers[authType]; handler != nil { + var authBind *model.AuthBind + var user IUser + realauthID := authID + if authIDType == AuthTypePassword { + if user = userProvider.GetUser(authID, authIDType); user == nil { + return nil, ErrUserNotExist + } + realauthID = user.GetID() + } + if authBind, err = handler.VerifySecret(realauthID, authSecret); err == nil { + if authBind == nil { // mobile, email会返回nil(表示不会新建AuthBind实体) + user = userProvider.GetUser(authID, authIDType) + authBind = &model.AuthBind{ + Type: authType, + AuthID: authID, + Status: model.AuthBindStatusNormal, + } + } else { + // 返回authBind中UserID为空表示只是认证,但本地没有记录,这种情况会返回临时TOKEN + if authBind.UserID != "" { + user = userProvider.GetUser(authBind.UserID, UserIDID) + } + } + authInfo = CreateauthInfo(user, authBind, nil) + } + } else { + err = ErrIllegalAuthType + } + return authInfo, err +} + +// 通过临时TOKEN绑定新创建的用户 +func BindUser(inauthInfo *AuthInfo, user IUser) (outauthInfo *AuthInfo, err error) { + if inauthInfo == nil || user == nil { + return nil, ErrInternalErrror + } + if inauthInfo.IUser != nil { + return nil, ErrUserAlreadyExist + } + if handler := authers[inauthInfo.AuthBindInfo.Type]; handler != nil { + inauthInfo.AuthBindInfo.UserID = user.GetID() + if err = handler.AddAuthBind(inauthInfo.AuthBindInfo, user.GetName()); err == nil { + RemoveUserInfo(inauthInfo.Token) + outauthInfo = CreateauthInfo(user, inauthInfo.AuthBindInfo, inauthInfo.UserData) + } + } else { + err = ErrIllegalAuthType + } + return outauthInfo, err +} + +// 添加新绑定 +func AddAuthBind(authInfo *AuthInfo, newAuthInfo *AuthInfo) (err error) { + if authInfo == nil || newAuthInfo == nil { + return ErrInternalErrror + } + if newAuthInfo.IUser != nil { + return ErrIllegalAuthTypeAlreadyExist + } + RemoveUserInfo(newAuthInfo.Token) + newAuthInfo.AuthBindInfo.UserID = authInfo.GetID() + err = authers[newAuthInfo.AuthBindInfo.Type].AddAuthBind(newAuthInfo.AuthBindInfo, authInfo.GetName()) + return err +} + +// func AddAuthBind(authInfo *AuthInfo, authType, authID, authIDType, authSecret string) (err error) { +// if authInfo == nil { +// return ErrInternalErrror +// } +// // 密码绑定直接绑定 +// if authType == AuthTypePassword { +// err = authers[AuthTypePassword].AddAuthBind(&model.AuthBind{ +// UserID: authInfo.GetID(), +// Type: AuthTypePassword, +// AuthID: authInfo.GetID(), +// Status: model.AuthBindStatusNormal, +// }, authInfo.GetName()) +// } else { +// tmpauthInfo, err := Login(authType, authID, authIDType, authSecret) +// if err == nil { +// RemoveUserInfo(tmpauthInfo.Token) +// tmpauthInfo.AuthBindInfo.UserID = authInfo.GetID() +// err = authers[authType].AddAuthBind(tmpauthInfo.AuthBindInfo, authInfo.GetName()) +// } +// } +// return err +// } + +func UnbindAuth(authInfo *AuthInfo, authType string) (err error) { + if handler := authers[authType]; handler != nil { + err = handler.UnbindAuth(authInfo, authType) + } else { + err = ErrIllegalAuthType + } + return err +} + +func Logout(authInfo *AuthInfo) (err error) { + globals.SugarLogger.Debugf("Logout authInfo:%s", utils.Format4Output(authInfo, true)) + if handler := authers[authInfo.AuthBindInfo.Type]; handler != nil { + err = handler.Logout(authInfo) + } + RemoveUserInfo(authInfo.Token) + return err +} + +func SendVerifyCode(authID string) (err error) { + authType := GuessAuthTypeFromAuthID(authID) + if handler := authers[authType]; handler == nil { + return ErrIllegalAuthType + } else { + return handler.SendVerifyCode(authID) + } +} + +// token缓存相关 + +func RemoveUserInfo(token string) { + api.Cacher.Del(token) +} + +func GetUserInfo(token string) (authInfo *AuthInfo, err error) { + authInfo = new(AuthInfo) + if err = api.Cacher.GetAs(token, authInfo); err == nil { + return authInfo, nil + } + return nil, ErrTokenIsInvalid +} + +func SetUserInfo(token string, authInfo *AuthInfo, duration time.Duration) { + api.Cacher.Set(token, authInfo, DefTokenDuration) +} + +func createToken(user IUser) (token string, tokenType int) { + userID := TokenUserEmpty + tokenType = TokenTypeOnlyAuth + if user != nil { + userID = "[" + user.GetID2() + "]" + tokenType = TokenTypeNormal + } + return strings.Join([]string{ + TokenVer, + time.Now().Format("20060102-150405"), + userID, + userID, + }, TokenTypeSep), tokenType +} + +func GetTokenType(token string) (tokenType int) { + tokenType = TokenTypeNone + if token != "" { + tokenPartList := strings.Split(token, TokenTypeSep) + if (len(tokenPartList) == 1) || (len(tokenPartList) == 4 && tokenPartList[2] != TokenUserEmpty) { + tokenType = TokenTypeNormal + } else { + tokenType = TokenTypeOnlyAuth + } + } + return tokenType +} + +func IsV2Token(token string) bool { + tokenPartList := strings.Split(token, TokenTypeSep) + return tokenPartList[0] == TokenVer +} + +func GuessAuthTypeFromAuthID(authID string) (authType string) { + for k, v := range authTypeGuesserMap { + if v.FindStringSubmatch(authID) != nil { + return k + } + } + return AuthTypeNone +} diff --git a/business/auth2/auth_info.go b/business/auth2/auth_info.go new file mode 100644 index 000000000..85a2e9a8f --- /dev/null +++ b/business/auth2/auth_info.go @@ -0,0 +1,32 @@ +package auth2 + +import ( + "time" + + "git.rosy.net.cn/jx-callback/business/model" +) + +const ( + TokenTypeNone = 0 + TokenTypeNormal = 1 + TokenTypeOnlyAuth = 2 +) + +type AuthInfo struct { + IUser + AuthBindInfo *model.AuthBind + + LoginTime time.Time + ExpiresIn int64 + Token string + TokenType int // TOKEN类型, + UserData interface{} +} + +func (a *AuthInfo) GetAuthID() string { + return a.AuthBindInfo.AuthID +} + +func (a *AuthInfo) GetAuthType() string { + return a.AuthBindInfo.Type +} diff --git a/business/auth2/authprovider/mobile/mobile.go b/business/auth2/authprovider/mobile/mobile.go new file mode 100644 index 000000000..c1a349530 --- /dev/null +++ b/business/auth2/authprovider/mobile/mobile.go @@ -0,0 +1,86 @@ +package mobile + +import ( + "errors" + "fmt" + "math/rand" + "time" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/auth2" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/globals" + "git.rosy.net.cn/jx-callback/globals/api" + "github.com/KenmyZhang/aliyun-communicate" +) + +const ( + DefVerifyCodeDuration = 5 * time.Minute + TestMobile = "91112345678" + TestVerifyCode = "123456" +) + +const ( + AuthType = auth2.AuthTypeMobile +) + +var ( + ErrVerifyCodeIsWrong = errors.New("验证码错") +) + +type Auther struct { + auth2.DefAuther +} + +var ( + AutherObj *Auther +) + +func init() { + AutherObj = new(Auther) + auth2.RegisterAuther(AuthType, AutherObj) +} + +// 特殊接口 +func (a *Auther) SendVerifyCode(mobileNumber string) error { + code := fmt.Sprintf("%06d", rand.Intn(1000000)) + globals.SugarLogger.Debugf("SendVerifyCode mobileNumber:%s, code:%s", mobileNumber, code) + + smsClient := aliyunsmsclient.New("http://dysmsapi.aliyuncs.com/") + _, err := smsClient.Execute(globals.AliKey, globals.AliSecret, mobileNumber, "京西菜市", "SMS_84655036", string(utils.MustMarshal(map[string]interface{}{ + "code": code, + }))) + if err == nil { + api.Cacher.Set(mobileNumber, code, DefVerifyCodeDuration) + } else { + globals.SugarLogger.Infof("SendVerifyCode mobileNumber:%s failed with error:%v", mobileNumber, err) + } + return err +} + +func (a *Auther) VerifySecret(mobileNumber, code string) (authBind *model.AuthBind, err error) { + globals.SugarLogger.Debugf("VerifySecret mobileNumber:%s, code:%s", mobileNumber, code) + + err = ErrVerifyCodeIsWrong + if mobileNumber == TestMobile && code == TestVerifyCode { + err = nil + } else { + if value := api.Cacher.Get(mobileNumber); value != nil { + if code == value.(string) { + api.Cacher.Del(mobileNumber) + err = nil + } + } + } + return nil, err +} + +// 此函数为空 +func (a *Auther) AddAuthBind(authBind *model.AuthBind, userName string) (err error) { + return err +} + +// 此函数为空 +func (a *Auther) UnbindAuth(authInfo *auth2.AuthInfo, authType string) (err error) { + return err +} diff --git a/business/auth2/authprovider/password/password.go b/business/auth2/authprovider/password/password.go new file mode 100644 index 000000000..282625bc2 --- /dev/null +++ b/business/auth2/authprovider/password/password.go @@ -0,0 +1,55 @@ +package password + +import ( + "errors" + + "git.rosy.net.cn/jx-callback/business/auth2" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/business/model/dao" +) + +const ( + AuthType = auth2.AuthTypePassword +) + +type Auther struct { + auth2.DefAuther +} + +var ( + ErrUserAndPassNotMatch = errors.New("用户名密码不匹配") +) + +func init() { + auth2.RegisterAuther(AuthType, new(Auther)) +} + +func (a *Auther) VerifySecret(userID, passMD5 string) (authBind *model.AuthBind, err error) { + if authBind, err = dao.GetAuthBind(nil, "", AuthType, userID, ""); err == nil { + err = a.checkPassword(authBind, passMD5) + } else if dao.IsNoRowsError(err) { + err = auth2.ErrUserNotExist + } + return authBind, err +} + +// 特殊接口 +func (a *Auther) ChangePassword(userID, oldPassMD5, newPassMD5 string) (err error) { + var authBind *model.AuthBind + if authBind, err = dao.GetAuthBind(nil, "", AuthType, userID, ""); err == nil { + if err = a.checkPassword(authBind, oldPassMD5); err == nil || authBind.AuthSecret == "" { + authBind.AuthSecret = newPassMD5 + _, err = dao.UpdateEntity(nil, authBind, "AuthSecret") + } + } else if dao.IsNoRowsError(err) { + err = auth2.ErrUserNotExist + } + return err +} + +func (a *Auther) checkPassword(authBind *model.AuthBind, passMD5 string) (err error) { + if authBind.AuthSecret != passMD5 { + return ErrUserAndPassNotMatch + } + return nil +} diff --git a/business/auth2/authprovider/weixin/weixin.go b/business/auth2/authprovider/weixin/weixin.go new file mode 100644 index 000000000..33a25665a --- /dev/null +++ b/business/auth2/authprovider/weixin/weixin.go @@ -0,0 +1,80 @@ +package weixin + +import ( + "errors" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/auth2" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/business/model/dao" + "git.rosy.net.cn/jx-callback/globals" + "git.rosy.net.cn/jx-callback/globals/api" +) + +const ( + AuthTypeWeixin = "weixin" + AuthTypeMP = "weixinmp" + AuthTypeMini = "weixinmini" +) + +type Auther struct { + auth2.DefAuther + authType string +} + +var ( + AutherObjWX *Auther + AutherObjMP *Auther +) + +var ( + ErrStateIsWrong = errors.New("登录state非法") +) + +func init() { + AutherObjWX = &Auther{ + authType: AuthTypeWeixin, + } + auth2.RegisterAuther(AuthTypeWeixin, AutherObjWX) + + AutherObjMP = &Auther{ + authType: AuthTypeMP, + } + auth2.RegisterAuther(AuthTypeMP, AutherObjMP) +} + +func (a *Auther) VerifySecret(state, code string) (authBind *model.AuthBind, err error) { + globals.SugarLogger.Debugf("weixin VerifySecret code:%s", code) + if state == "" { + token, err2 := api.WeixinAPI.SNSRetrieveToken(code) + if err = err2; err == nil { + wxUserinfo, err2 := api.WeixinAPI.SNSGetUserInfo(token.AccessToken, token.OpenID) + if err = err2; err == nil { + db := dao.GetDB() + if authBind, err = dao.GetAuthBind(db, "", a.authType, wxUserinfo.OpenID, ""); dao.IsNoRowsError(err) { + var authBindList []*model.AuthBind + if wxUserinfo.UnionID != "" { + if authBindList, err = dao.GetAuthBindsByWXUnionID(db, wxUserinfo.UnionID); err == nil && len(authBindList) > 0 { + authBind = authBindList[0] + authBind.Type = a.authType + authBind.AuthID = wxUserinfo.OpenID + authBind.DetailData = string(utils.MustMarshal(wxUserinfo)) + err = a.AddAuthBind(authBind, wxUserinfo.NickName) + } + } + if err == nil && len(authBindList) == 0 { + authBind = &model.AuthBind{ + Type: a.authType, + AuthID: wxUserinfo.OpenID, + AuthID2: wxUserinfo.UnionID, + DetailData: string(utils.MustMarshal(wxUserinfo)), + } + } + } + } + } + } else { + err = ErrStateIsWrong + } + return authBind, err +} diff --git a/business/auth2/authprovider/weixin/weixin_mini.go b/business/auth2/authprovider/weixin/weixin_mini.go new file mode 100644 index 000000000..25a398e1a --- /dev/null +++ b/business/auth2/authprovider/weixin/weixin_mini.go @@ -0,0 +1,76 @@ +package weixin + +import ( + "encoding/base64" + "errors" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/auth2" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/business/model/dao" + "git.rosy.net.cn/jx-callback/globals" + "git.rosy.net.cn/jx-callback/globals/api" +) + +type MiniAuther struct { + auth2.DefAuther +} + +var ( + ErrAuthTypeShouldBeMini = errors.New("当前操作要求是小程序登录方式") +) + +var ( + AutherObjMini *MiniAuther +) + +func init() { + AutherObjMini = new(MiniAuther) + auth2.RegisterAuther(AuthTypeMP, AutherObjMini) +} + +func (a *MiniAuther) VerifySecret(dummy, jsCode string) (authBind *model.AuthBind, err error) { + globals.SugarLogger.Debugf("weixin mini VerifySecret jsCode:%s", jsCode) + + sessionInfo, err := api.WeixinMiniAPI.SNSCode2Session(jsCode) + if err == nil { + db := dao.GetDB() + if authBind, err = dao.GetAuthBind(db, "", AuthTypeMP, sessionInfo.OpenID, ""); dao.IsNoRowsError(err) { + var authBindList []*model.AuthBind + if sessionInfo.UnionID != "" { + if authBindList, err = dao.GetAuthBindsByWXUnionID(db, sessionInfo.UnionID); err == nil && len(authBindList) > 0 { + authBind = authBindList[0] + authBind.Type = AuthTypeMP + authBind.AuthID = sessionInfo.OpenID + authBind.DetailData = string(utils.MustMarshal(sessionInfo)) + authBind.UserData = sessionInfo.SessionKey + err = a.AddAuthBind(authBind, "admin") + } + } + if err == nil && len(authBindList) == 0 { + authBind = &model.AuthBind{ + Type: AuthTypeMP, + AuthID: sessionInfo.OpenID, + AuthID2: sessionInfo.UnionID, + DetailData: string(utils.MustMarshal(sessionInfo)), + UserData: sessionInfo.SessionKey, + } + } + } + } + return authBind, err +} + +// 特殊接口 +func (a *MiniAuther) DecryptData(authInfo *auth2.AuthInfo, encryptedData, iv string) (decryptedDataBase64 string, err error) { + globals.SugarLogger.Debugf("weixin mini DecryptData encryptedData:%s, iv:%s", encryptedData, iv) + if authInfo.AuthBindInfo.Type != AuthTypeMini { + return "", ErrAuthTypeShouldBeMini + } + sessionKey := authInfo.AuthBindInfo.UserData.(string) + decryptedData, err := api.WeixinMiniAPI.SNSDecodeMiniProgramData(encryptedData, sessionKey, iv) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(decryptedData), nil +} diff --git a/business/auth2/defauther.go b/business/auth2/defauther.go new file mode 100644 index 000000000..18272b148 --- /dev/null +++ b/business/auth2/defauther.go @@ -0,0 +1,37 @@ +package auth2 + +import ( + "errors" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/model" + "git.rosy.net.cn/jx-callback/business/model/dao" +) + +type DefAuther struct { +} + +// 此函数为空 +func (a *DefAuther) AddAuthBind(authBind *model.AuthBind, userName string) (err error) { + dao.WrapAddIDCULDEntity(authBind, userName) + err = dao.CreateEntity(nil, authBind) + return err +} + +func (a *DefAuther) UnbindAuth(authInfo *AuthInfo, authType string) (err error) { + _, err = dao.DeleteEntityLogically(nil, &model.AuthBind{}, nil, authInfo.GetID(), map[string]interface{}{ + "UserID": authInfo.GetID(), + "Type": authType, + model.FieldDeletedAt: utils.DefaultTimeValue, + }) + return err +} + +func (a *DefAuther) SendVerifyCode(authID string) error { + return errors.New("当前登录类型不支持此操作") +} + +// 此函数为空 +func (a *DefAuther) Logout(authInfo *AuthInfo) error { + return nil +} diff --git a/business/jxcallback/auth/auth.go b/business/jxcallback/auth/auth.go index 27a4b6aad..62453b5c5 100644 --- a/business/jxcallback/auth/auth.go +++ b/business/jxcallback/auth/auth.go @@ -107,3 +107,11 @@ func ConvertErr2NoUser(err error, mobileNum string) error { } return err } + +func (a *LoginInfo) GetAuthID() string { + return a.ID +} + +func (a *LoginInfo) GetAuthType() string { + return a.LoginType +} diff --git a/business/jxcallback/auth/weixin/weixin.go b/business/jxcallback/auth/weixin/weixin.go index b138b138a..534872f6b 100644 --- a/business/jxcallback/auth/weixin/weixin.go +++ b/business/jxcallback/auth/weixin/weixin.go @@ -179,14 +179,14 @@ func BindMobile2(openid, secret, mobileNum, verifyCode, nickname string) (loginI func (a *AutherMiniProgram) BindWeiXin(ctx *jxcontext.Context, code, nickName string) (err error) { globals.SugarLogger.Debugf("AutherMiniProgram BindWeiXin code:%s, nickName:%s", code, nickName) loginInfo := ctx.GetLoginInfo() - if loginInfo == nil || loginInfo.LoginType != mobile.LoginType { + if loginInfo == nil || loginInfo.GetAuthType() != mobile.LoginType { return fmt.Errorf("调用AutherMiniProgram BindWeiXin时,必须以手机验证方式登录") } sessionInfo, err := api.WeixinMiniAPI.SNSCode2Session(code) if err != nil { return err } - err = dao.UpdateWeiXinUser(dao.GetDB(), loginInfo.ID, nickName, sessionInfo.UnionID, "", sessionInfo.OpenID) + err = dao.UpdateWeiXinUser(dao.GetDB(), loginInfo.GetAuthID(), nickName, sessionInfo.UnionID, "", sessionInfo.OpenID) return auth.ConvertErr2NoUser(err, "") } @@ -234,13 +234,13 @@ func (a *AutherMiniProgram) Login(mobileNum, code string) (userID, LoginType str func (a *AutherMiniProgram) Logout(loginInfo *auth.LoginInfo) error { globals.SugarLogger.Debugf("AutherMiniProgram Logout openid:%s", utils.Format4Output(loginInfo, false)) - return api.Cacher.Del(composeSessionKeyCacheKey(loginInfo.ID)) + return api.Cacher.Del(composeSessionKeyCacheKey(loginInfo.GetAuthID())) } func (a *AutherMiniProgram) DecryptData(ctx *jxcontext.Context, encryptedData, iv string) (decryptedDataBase64 string, err error) { globals.SugarLogger.Debugf("AutherMiniProgram DecryptData encryptedData:%s, iv:%s", encryptedData, iv) var sessionKey string - if err = api.Cacher.GetAs(composeSessionKeyCacheKey(ctx.GetLoginInfo().ID), &sessionKey); err != nil { + if err = api.Cacher.GetAs(composeSessionKeyCacheKey(ctx.GetLoginInfo().GetAuthID()), &sessionKey); err != nil { return "", err } decryptedData, err := api.WeixinMiniAPI.SNSDecodeMiniProgramData(encryptedData, sessionKey, iv) diff --git a/business/jxstore/cms/user.go b/business/jxstore/cms/user.go index fed6545ac..7bd170328 100644 --- a/business/jxstore/cms/user.go +++ b/business/jxstore/cms/user.go @@ -76,7 +76,7 @@ func GetSelfInfo(ctx *jxcontext.Context) (storeUserInfo *StoreUserInfo, err erro return nil, auth.ErrAPINeedRealLogin } - fieldName := LoginTypeFieldMap[loginInfo.LoginType] + fieldName := LoginTypeFieldMap[loginInfo.GetAuthType()] if fieldName == "" { return nil, auth.ErrIllegalLoginType } @@ -90,7 +90,7 @@ func GetSelfInfo(ctx *jxcontext.Context) (storeUserInfo *StoreUserInfo, err erro GROUP BY 1,2,3,4,5,6,7; `, fieldName) storeUserInfo = new(StoreUserInfo) - if err = dao.GetRow(nil, storeUserInfo, sql, loginInfo.ID); err == nil || err == orm.ErrNoRows { // todo + if err = dao.GetRow(nil, storeUserInfo, sql, loginInfo.GetAuthID()); err == nil || err == orm.ErrNoRows { // todo err = nil if storeUserInfo.MembersStr != "" { err = utils.UnmarshalUseNumber([]byte(storeUserInfo.MembersStr), &storeUserInfo.Members) diff --git a/business/jxutils/jxcontext/jxcontext.go b/business/jxutils/jxcontext/jxcontext.go index 4717fa11b..016c0369c 100644 --- a/business/jxutils/jxcontext/jxcontext.go +++ b/business/jxutils/jxcontext/jxcontext.go @@ -1,10 +1,12 @@ package jxcontext import ( + "errors" "net/http" "sync" "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/auth2" "git.rosy.net.cn/jx-callback/business/jxcallback/auth" "git.rosy.net.cn/jx-callback/business/jxutils/tasksch" "git.rosy.net.cn/jx-callback/business/model" @@ -12,11 +14,16 @@ import ( "github.com/astaxie/beego" ) +type IAuther interface { + GetAuthID() string + GetAuthType() string +} + type Context struct { rootTask tasksch.ITask token string accessUUID string - userInfo *auth.LoginInfo + userInfo IAuther //*auth.LoginInfo w http.ResponseWriter r *http.Request mapData map[interface{}]interface{} @@ -56,7 +63,17 @@ func New(rootTask tasksch.ITask, token string, w http.ResponseWriter, r *http.Re mapData: make(map[interface{}]interface{}), accessUUID: utils.GetUUID(), } - ctx.userInfo, err = auth.GetUserInfo(token) + if auth2.IsV2Token(token) { + authInfo, err2 := auth2.GetUserInfo(token) + if err = err2; err == nil { + ctx.userInfo = authInfo + if authInfo.TokenType != auth2.TokenTypeNormal { + err = errors.New("需要正式TOKEN") + } + } + } else { + ctx.userInfo, err = auth.GetUserInfo(token) + } if err != nil && beego.BConfig.RunMode == "prod" { globals.SugarLogger.Debugf("token is invalid, token:%s", token) return nil, model.ErrCodeTokenIsInvalid, err @@ -67,7 +84,7 @@ func New(rootTask tasksch.ITask, token string, w http.ResponseWriter, r *http.Re func (ctx *Context) GetUserName() string { userName := ctx.token if ctx.userInfo != nil { - userName = ctx.userInfo.ID + userName = ctx.userInfo.GetAuthID() } if len(userName) > MaxUserNameLen { userName = userName[:MaxUserNameLen] @@ -77,7 +94,7 @@ func (ctx *Context) GetUserName() string { func (ctx *Context) GetLoginID() string { if ctx.userInfo != nil { - return ctx.userInfo.ID + return ctx.userInfo.GetAuthID() } return "" @@ -85,7 +102,7 @@ func (ctx *Context) GetLoginID() string { func (ctx *Context) GetLoginType() string { if ctx.userInfo != nil { - return ctx.userInfo.LoginType + return ctx.userInfo.GetAuthType() } return "" } @@ -98,7 +115,7 @@ func (ctx *Context) GetAccessUUID() string { return ctx.accessUUID } -func (ctx *Context) GetLoginInfo() *auth.LoginInfo { +func (ctx *Context) GetLoginInfo() IAuther { return ctx.userInfo } diff --git a/business/model/auth2.go b/business/model/auth2.go new file mode 100644 index 000000000..35cc27cc4 --- /dev/null +++ b/business/model/auth2.go @@ -0,0 +1,30 @@ +package model + +const ( + AuthBindStatusNormal = 1 + AuthBindStatusDisabled = 2 +) + +type AuthBind struct { + ModelIDCULD + + UserID string `orm:"size(48);column(user_id)" json:"userID"` + Type string `orm:"size(16)" json:"type"` + Status int8 `json:"status"` + + AuthID string `orm:"size(48);index" json:"authID"` + AuthID2 string `orm:"size(48);index" json:"authID2"` + AuthSecret string `orm:"size(48)" json:"authSecret"` + AuthSecret2 string `orm:"size(48)" json:"authSecret2"` + Remark string `orm:"size(255)" json:"remark"` + DetailData string `orm:"type(text)" json:"-"` + + UserData interface{} `orm:"-" json:"-"` +} + +func (*AuthBind) TableUnique() [][]string { + return [][]string{ + []string{"UserID", "Type", "DeletedAt"}, + []string{"AuthID", "Type", "DeletedAt"}, + } +} diff --git a/business/model/dao/dao_auth2.go b/business/model/dao/dao_auth2.go new file mode 100644 index 000000000..03144b95a --- /dev/null +++ b/business/model/dao/dao_auth2.go @@ -0,0 +1,11 @@ +package dao + +import "git.rosy.net.cn/jx-callback/business/model" + +func GetAuthBind(db *DaoDB, userID, authType, authID, authID2 string) (authBind *model.AuthBind, err error) { + return authBind, err +} + +func GetAuthBindsByWXUnionID(db *DaoDB, unionID string) (authBinds []*model.AuthBind, err error) { + return authBinds, err +} diff --git a/business/model/user.go b/business/model/user.go new file mode 100644 index 000000000..626270cc4 --- /dev/null +++ b/business/model/user.go @@ -0,0 +1,43 @@ +package model + +type User struct { + ModelIDCULD + UserID string `orm:"size(48);column(user_id)" json:"userID"` // 内部唯一标识 + UserID2 string `orm:"size(48);column(user_id2)" json:"userID2"` // 外部唯一标识(一般用于登录) + Name string `orm:"size(48)" json:"name"` // 外部唯一显示 标识(一般用于显示) + Mobile string `orm:"size(32)" json:"mobile"` + Email string `orm:"size(32)" json:"email"` + Status int8 `json:"status"` + IDCardNo string `orm:"size(18);column(id_card_no)" json:"idCardNo"` // 身份证号 +} + +func (*User) TableUnique() [][]string { + return [][]string{ + []string{"UserID", "DeletedAt"}, + []string{"UserID2", "DeletedAt"}, + []string{"Name", "DeletedAt"}, + []string{"Mobile", "DeletedAt"}, + []string{"Email", "DeletedAt"}, + []string{"IDCardNo", "DeletedAt"}, + } +} + +func (user *User) GetID() string { + return user.UserID +} + +func (user *User) GetID2() string { + return user.UserID2 +} + +func (user *User) GetMobile() string { + return user.Mobile +} + +func (user *User) GetEmail() string { + return user.Email +} + +func (user *User) GetName() string { + return user.Name +} diff --git a/controllers/auth2.go b/controllers/auth2.go new file mode 100644 index 000000000..6db9107d0 --- /dev/null +++ b/controllers/auth2.go @@ -0,0 +1,174 @@ +package controllers + +import ( + "encoding/base64" + "errors" + "fmt" + "net/http" + + "git.rosy.net.cn/baseapi/utils" + "git.rosy.net.cn/jx-callback/business/auth2" + "git.rosy.net.cn/jx-callback/business/auth2/authprovider/weixin" + "git.rosy.net.cn/jx-callback/business/jxcallback/auth" + "git.rosy.net.cn/jx-callback/business/jxutils/jxcontext" + "git.rosy.net.cn/jx-callback/business/model" + "github.com/astaxie/beego" +) + +var ( + ErrNeedV2Token = errors.New("需要V2版的TOKEN") +) + +type Auth2Controller struct { + beego.Controller +} + +// @Title 发送验证码 +// @Description 发送验证码 +// @Param authID formData string true "手机号或邮件" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /SendVerifyCode [post] +func (c *Auth2Controller) SendVerifyCode() { + c.callSendVerifyCode(func(params *tAuth2SendVerifyCodeParams) (retVal interface{}, errCode string, err error) { + err = auth2.SendVerifyCode(params.AuthID) + return retVal, "", err + }) +} + +// @Title 登录接口 +// @Description 登录接口(微信与公众号登录不能直接调用此接口) +// @Param authType formData string true "登录类型,当前支持[password:本地账号密码,mobile:手机短信,weixin:微信登录,weixinmp:微信公众号登录,weixinmini;小程序登录]" +// @Param authSecret formData string true "不同登录类型的登录秘密" +// @Param authID formData string false "登录ID,登录类型为password时依赖于authIDType,其它为相应登录类型的id" +// @Param authIDType formData string false "只有在登录类型为password时,才有意义,分别为:userID2:用户名,email,mobile" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /Login [post] +func (c *Auth2Controller) Login() { + c.callLogin(func(params *tAuth2LoginParams) (retVal interface{}, errCode string, err error) { + retVal, err = auth2.Login(params.AuthType, params.AuthID, params.AuthIDType, params.AuthSecret) + return retVal, "", err + }) +} + +// @Title 微信认证回调接口 +// @Description 微信认证回调接口,自己不能直接调用 +// @Param code query string true "客户同意后得到的code" +// @Param block query string true "回调地址" +// @Param state query string false "微信回调的登录状态" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /WeixinOAuth2 [get] +func (c *Auth2Controller) WeixinOAuth2() { + var redirectURL string + c.callWeixinOAuth2(func(params *tAuth2WeixinOAuth2Params) (retVal interface{}, errCode string, err error) { + authInfo, err := auth2.Login(weixin.AuthTypeWeixin, params.State, "", params.Code) + var callResult *CallResult + if err == nil { + callResult = &CallResult{ + Code: model.ErrCodeSuccess, + Data: string(utils.MustMarshal(authInfo)), + } + } else { + callResult = &CallResult{ + Code: model.ErrCodeGeneralFailed, + Desc: err.Error(), + } + } + redirectURL = fmt.Sprintf("%s?info=%s", params.Block, base64.StdEncoding.EncodeToString(utils.MustMarshal(callResult))) + return retVal, model.ErrorCodeIgnore, err + }) + c.Redirect(redirectURL, http.StatusTemporaryRedirect) +} + +// @Title 微信公众号认证回调接口 +// @Description 微信公众号认证回调接口,自己不能直接调用 +// @Param code query string true "客户同意后得到的code" +// @Param block query string true "回调地址" +// @Param state query string false "微信回调的登录状态" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /WeixinMPOAuth2 [get] +func (c *Auth2Controller) WeixinMPOAuth2() { + var redirectURL string + c.callWeixinMPOAuth2(func(params *tAuth2WeixinMPOAuth2Params) (retVal interface{}, errCode string, err error) { + authInfo, err := auth2.Login(weixin.AuthTypeMP, params.State, "", params.Code) + var callResult *CallResult + if err == nil { + callResult = &CallResult{ + Code: model.ErrCodeSuccess, + Data: string(utils.MustMarshal(authInfo)), + } + } else { + callResult = &CallResult{ + Code: model.ErrCodeGeneralFailed, + Desc: err.Error(), + } + } + redirectURL = fmt.Sprintf("%s?info=%s", params.Block, base64.StdEncoding.EncodeToString(utils.MustMarshal(callResult))) + return retVal, model.ErrorCodeIgnore, err + }) + c.Redirect(redirectURL, http.StatusTemporaryRedirect) +} + +// @Title 登出接口 +// @Description 登出接口(此接口兼容V1的TOKEN) +// @Param token header string true "认证token" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /Logout [delete] +func (c *Auth2Controller) Logout() { + c.callLogout(func(params *tAuth2LogoutParams) (retVal interface{}, errCode string, err error) { + if authInfo, ok := params.Ctx.GetLoginInfo().(*auth2.AuthInfo); ok { + err = auth2.Logout(authInfo) + } else { + err = auth.Logout(params.Token) + } + return nil, "", err + }) +} + +// @Title 绑定认证方式 +// @Description 绑定认证方式 +// @Param token header string true "认证token" +// @Param authToken formData string true "之前通过login得到的新认证TOKEN" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /AddAuthBind [post] +func (c *Auth2Controller) AddAuthBind() { + c.callAddAuthBind(func(params *tAuth2AddAuthBindParams) (retVal interface{}, errCode string, err error) { + authInfo, err2 := c.getAuth2Info(params.Ctx) + if err := err2; err == nil { + newAuthInfo, err2 := auth2.GetUserInfo(params.AuthToken) + if err = err2; err == nil { + err = auth2.AddAuthBind(authInfo, newAuthInfo) + } + } + return retVal, "", err + }) +} + +// @Title 删除认证方式 +// @Description 删除认证方式 +// @Param token header string true "认证token" +// @Param authType formData string true "登录类型,当前支持[weixin:微信登录,weixinmp:微信公众号登录,weixinmini;小程序登录]" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /RemoveAuthBind [post] +func (c *Auth2Controller) RemoveAuthBind() { + c.callRemoveAuthBind(func(params *tAuth2RemoveAuthBindParams) (retVal interface{}, errCode string, err error) { + authInfo, err2 := c.getAuth2Info(params.Ctx) + if err := err2; err == nil { + err = auth2.UnbindAuth(authInfo, params.AuthType) + } + return retVal, "", err + }) +} + +func (c *Auth2Controller) getAuth2Info(ctx *jxcontext.Context) (authInfo *auth2.AuthInfo, err error) { + if authInfo, ok := ctx.GetLoginInfo().(*auth2.AuthInfo); ok { + return authInfo, err + } + return nil, ErrNeedV2Token +} diff --git a/routers/commentsRouter_controllers.go b/routers/commentsRouter_controllers.go index 48750d008..ec4299f07 100644 --- a/routers/commentsRouter_controllers.go +++ b/routers/commentsRouter_controllers.go @@ -7,6 +7,62 @@ import ( func init() { + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "AddAuthBind", + Router: `/AddAuthBind`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "Login", + Router: `/Login`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "Logout", + Router: `/Logout`, + AllowHTTPMethods: []string{"delete"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "RemoveAuthBind", + Router: `/RemoveAuthBind`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "SendVerifyCode", + Router: `/SendVerifyCode`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "WeixinMPOAuth2", + Router: `/WeixinMPOAuth2`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:Auth2Controller"], + beego.ControllerComments{ + Method: "WeixinOAuth2", + Router: `/WeixinOAuth2`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:AuthController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:AuthController"], beego.ControllerComments{ Method: "BindMiniProgram", diff --git a/routers/router.go b/routers/router.go index 0330fb400..4eff77782 100644 --- a/routers/router.go +++ b/routers/router.go @@ -81,6 +81,11 @@ func init() { &controllers.MsgController{}, ), ), + beego.NSNamespace("/auth2", + beego.NSInclude( + &controllers.Auth2Controller{}, + ), + ), ) beego.AddNamespace(ns)