package auth2 import ( "bytes" "encoding/base64" "errors" "regexp" "strings" "time" "git.rosy.net.cn/baseapi/utils" "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" "github.com/dchest/captcha" ) const ( UserIDNone = "" UserIDID = "userid" UserIDID2 = "userid2" UserIDMobile = "mobile" UserIDEmail = "email" ) const ( TokenHeader = "TOKEN" TokenVer = "V2" TokenTypeSep = "." TokenUserEmpty = "NULL" ) const ( AuthTypeNone = "" AuthTypePassword = "localpass" AuthTypeEmail = "email" AuthTypeMobile = "mobile" ) const ( DefTokenDuration = 7 * 24 * time.Hour // 正式TOKEN,7天有效期 TmpTokenDuration = 30 * time.Minute // 临时TOKEN,30分钟有效期 MinCaptchaLen = 4 MaxCaptchaWidth = 400 MaxCaptchaHeight = 400 ) type IUser interface { GetID() string // 这个ID是不可变的,系统内部使用的唯一标识 GetID2() string // 这个是可改的,唯一的,用户设置的用户名 GetMobile() string GetEmail() string GetName() string } const ( UpdateUserTypeAdd = 1 UpdateUserTypeDelete = 2 UpdateUserTypeUpdate = 3 ) type IUserProvider interface { GetUser(authID, authIDType string) (user IUser) UpdateUserMobile(userID string, mobile string) (err error) UpdateUserEmail(userID string, email string) (err error) UpdateUserType(userID string, userTypeMask int8, updateType int) (err error) // CreateUser(userID2, mobile, email, name string) (user IUser, err error) } type CaptchaInfo struct { ID string `json:"id"` ImgBase64 string `json:"imgBase64"` } type IAuther interface { SendVerifyCode(authID string) (err error) // 负责验证secret,并找到相应的用户返回(password,email,mobile类型的不负责用户查找)如果找不到用户UserID为空 VerifySecret(authID, authSecret string) (authBindEx *AuthBindEx, err error) AddAuthBind(authBindEx *AuthBindEx, userName string) (err error) UnbindAuth(userID, authType, userName string) (err error) Logout(authInfo *AuthInfo) (err error) GetUserType() (userType int8) } var ( authers map[string]IAuther userProvider IUserProvider TestMobileMap = map[string]int{ "91112345678": 1, } TestCaptchaMap = map[string]string{ "hGU7pB": "dd1AvY", } 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[345789]\d{9}$`), } // 永久全局的TOKEN,内部使用 fixedTokenMap = map[string]*AuthInfo{ "85e60d6a-f1f8-4837-9c7d-d7072b0ba1c6": &AuthInfo{ UserBasic: UserBasic{ UserID2: "shifengfix", Mobile: "18048531223", Name: "石峰固定", }, AuthBindInfo: &AuthBindEx{}, LoginTime: utils.ZeroTimeValue, ExpiresAt: 0, }, } ) var ( ErrInternalErrror = errors.New("内部错误") ErrUserAlreadyExist = errors.New("用户已经存在") ErrUserMobileAlreadyExist = errors.New("用户手机已经存在") ErrUserID2AlreadyExist = errors.New("用户标识已经存在") ErrUserNotExist = errors.New("用户不存在") ErrUserAuthTypeNotExist = errors.New("用户登录类型不存在") ErrIllegalAuthType = errors.New("非法的登录类型") ErrAuthTypeAlreadyExist = errors.New("要登录类型已经存在") ErrCaptchaIsNotOk = errors.New("图形校验码不正确") ErrNeedV2Token = errors.New("需要V2版的TOKEN") ErrInvalidParams = errors.New("参数不合法") ) func init() { authers = make(map[string]IAuther) InitFixedToken() } func InitFixedToken() { for k, v := range fixedTokenMap { v.UserID = k v.Token = k v.TokenType = TokenTypeNormal } } func Init(userProvider2 IUserProvider) { userProvider = userProvider2 } func RegisterAuther(authType string, handler IAuther) { authers[authType] = handler } func getFixedTokenName(token string) *AuthInfo { return fixedTokenMap[token] } func createAuthInfo(user IUser, authBindInfo *AuthBindEx) (authInfo *AuthInfo) { token, tokenType := createToken(user) expireDuration := DefTokenDuration authInfo = &AuthInfo{ AuthBindInfo: authBindInfo, LoginTime: time.Now(), ExpiresAt: time.Now().Add(DefTokenDuration).Unix(), Token: token, TokenType: tokenType, } if user != nil { authInfo.UpdateByIUser(user) globals.SugarLogger.Debugf("createAuthInfo id:%s, id2:%s, mobile:%s, authInfo:%s", authInfo.GetID(), authInfo.GetID2(), authInfo.GetMobile(), utils.Format4Output(authInfo, true)) } else { expireDuration = TmpTokenDuration authInfo.ExpiresAt = time.Now().Add(expireDuration).Unix() globals.SugarLogger.Debugf("createAuthInfo authInfo:%s", utils.Format4Output(authInfo, true)) } SetUserInfo(token, authInfo, expireDuration) return authInfo } func CreateCaptcha(width, height, captchaLen int) (captchaInfo *CaptchaInfo, err error) { if captchaLen < MinCaptchaLen { captchaLen = MinCaptchaLen } if width > MaxCaptchaWidth { width = MaxCaptchaWidth } if height > MaxCaptchaHeight { height = MaxCaptchaHeight } captchaInfo = &CaptchaInfo{ ID: captcha.NewLen(captchaLen), } bufWriter := bytes.NewBuffer(nil) captcha.WriteImage(bufWriter, captchaInfo.ID, width, height) captchaInfo.ImgBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(bufWriter.Bytes()) return captchaInfo, err } func SendVerifyCode(authToken, captchaID, captchaValue, authID string) (err error) { if authToken != "" { _, err = GetTokenInfo(authToken) } else if captchaID != "" && captchaValue != "" { if !(TestCaptchaMap[captchaID] == captchaValue || captcha.VerifyString(captchaID, captchaValue)) { err = ErrCaptchaIsNotOk } } else { err = errors.New("发送验证必须要有认证或CAPTCHA信息") } if err == nil { authType := GuessAuthTypeFromAuthID(authID) if handler := authers[authType]; handler == nil { err = ErrIllegalAuthType } else { err = handler.SendVerifyCode(authID) } } return err } // 账号密码时:authIDType可能是:UserIDID,UserIDID2,UserIDMobile,UserIDEmail,authSecret是密码的sha1 // mobile或email登录时,authIDType必须相应为UserIDMobile,UserIDEmail // 其它登录时authIDType无用 // 邮箱时(如果允许):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) { authType = strings.ToLower(authType) authIDType = strings.ToLower(authIDType) if handler := authers[authType]; handler != nil { var authBindEx *AuthBindEx var user IUser realAuthID := authID if authType == AuthTypePassword { if authID == "" { return nil, ErrInvalidParams } if user = userProvider.GetUser(authID, authIDType); user == nil { return nil, ErrUserNotExist } realAuthID = user.GetID() } if authBindEx, err = handler.VerifySecret(realAuthID, authSecret); err == nil { // globals.SugarLogger.Debugf("auth2 Login authBindEx:%s", utils.Format4Output(authBindEx, false)) needAutoAddAuthBind := false if authBindEx == nil { // mobile, email会返回nil(表示不会新建AuthBind实体) user = userProvider.GetUser(authID, authIDType) authBindEx = &AuthBindEx{ AuthBind: model.AuthBind{ Type: authType, AuthID: authID, Status: model.AuthBindStatusNormal, }, } } else { // 返回authBind中UserID为空表示只是认证,但本地没有记录,这种情况会返回临时TOKEN if authBindEx.UserHint != nil && authBindEx.UserID == "" { if authBindEx.UserHint.Mobile != "" { user = userProvider.GetUser(authBindEx.UserHint.Mobile, UserIDMobile) } if user == nil && authBindEx.UserHint.Email != "" { user = userProvider.GetUser(authBindEx.UserHint.Email, UserIDEmail) } if user != nil { authBindEx.UserID = user.GetID() needAutoAddAuthBind = true } } else if authBindEx.UserID != "" { user = userProvider.GetUser(authBindEx.UserID, UserIDID) } } authInfo = createAuthInfo(user, authBindEx) if needAutoAddAuthBind { if authers[authInfo.AuthBindInfo.Type].AddAuthBind(authInfo.AuthBindInfo, user.GetName()) == nil { // todo,用户类型应该要与RegisterUser一起统一处理 userProvider.UpdateUserType(user.GetID(), handler.GetUserType(), UpdateUserTypeAdd) } } } } 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.IsUserEmpty() { 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) } } else { err = ErrIllegalAuthType } return outauthInfo, err } // 添加新绑定 func AddAuthBind(user IUser, newAuthInfo *AuthInfo) (err error) { if user == nil || newAuthInfo == nil { return ErrInternalErrror } if !newAuthInfo.IsUserEmpty() { return ErrAuthTypeAlreadyExist } RemoveUserInfo(newAuthInfo.Token) if newAuthInfo.AuthBindInfo.Type == AuthTypeMobile { err = userProvider.UpdateUserMobile(user.GetID(), newAuthInfo.AuthBindInfo.AuthID) } else if newAuthInfo.AuthBindInfo.Type == AuthTypeEmail { err = userProvider.UpdateUserEmail(user.GetID(), newAuthInfo.AuthBindInfo.AuthID) } else { newAuthInfo.AuthBindInfo.UserID = user.GetID() err = authers[newAuthInfo.AuthBindInfo.Type].AddAuthBind(newAuthInfo.AuthBindInfo, user.GetName()) } return err } func UnbindAuth(userID, authType, userName string) (err error) { if handler := authers[authType]; handler != nil { err = handler.UnbindAuth(userID, authType, userName) } 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 } // token缓存相关 ///////////// func RemoveUserInfo(token string) { api.Cacher.Del(token) } func GetTokenInfo(token string) (authInfo *AuthInfo, err error) { if authInfo = getFixedTokenName(token); authInfo != nil { return authInfo, nil } if err = api.Cacher.GetAs(token, &authInfo); err == nil { return authInfo, nil } return nil, model.ErrTokenIsInvalid } func SetUserInfo(token string, authInfo *AuthInfo, duration time.Duration) { api.Cacher.Set(token, authInfo, DefTokenDuration) } func ClearUserToken(userID string) { if keys, err := api.Cacher.Keys(strings.Join([]string{ TokenHeader, TokenVer, userID, "*", }, TokenTypeSep)); err == nil { for _, key := range keys { api.Cacher.Del(key) } } } ///////////// func createToken(user IUser) (token string, tokenType int) { userID := TokenUserEmpty userName := TokenUserEmpty tokenType = TokenTypeOnlyAuth if user != nil { userID = user.GetID() userName = "[" + user.GetID2() + "]" tokenType = TokenTypeNormal } return strings.Join([]string{ TokenHeader, TokenVer, userID, time.Now().Format("20060102-150405"), userName, utils.GetUUID(), }, TokenTypeSep), tokenType } func GetTokenType(token string) (tokenType int) { if getFixedTokenName(token) != nil { return TokenTypeNormal } tokenType = TokenTypeNone if token != "" { tokenPartList := strings.Split(token, TokenTypeSep) if (len(tokenPartList) == 1) || (len(tokenPartList) == 6 && tokenPartList[2] != TokenUserEmpty) { tokenType = TokenTypeNormal } else { tokenType = TokenTypeOnlyAuth } } return tokenType } func IsV2Token(token string) bool { if getFixedTokenName(token) != nil { return true } tokenPartList := strings.Split(token, TokenTypeSep) return len(tokenPartList) > 1 && tokenPartList[1] == TokenVer } func GuessAuthTypeFromAuthID(authID string) (authType string) { if TestMobileMap[authID] == 1 { return AuthTypeMobile } for k, v := range authTypeGuesserMap { if v.FindStringSubmatch(authID) != nil { return k } } return AuthTypeNone } func DisableUser(userID, operatorUserName string) (err error) { if _, err = dao.UpdateEntityLogically(dao.GetDB(), &model.AuthBind{}, map[string]interface{}{ "Status": model.AuthBindStatusDisabled, }, operatorUserName, map[string]interface{}{ "UserID": userID, }); err == nil { ClearUserToken(userID) } return err } func GetUserBindAuthInfo(userID string) (authList []*model.AuthBind, err error) { return dao.GetUserBindAuthInfo(dao.GetDB(), userID) }