package auth2 import ( "bytes" "encoding/base64" "errors" "fmt" "regexp" "strings" "time" "git.rosy.net.cn/baseapi/utils" "git.rosy.net.cn/jx-callback/business/dao" "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/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 InternalAuthSecret = "a36ca416-c85e-4dcf-aff9-590be3d2f8a2" ) type IUser interface { GetID() string // 这个ID是不可变的,系统内部使用的唯一标识 GetID2() string // 这个是可改的,唯一的,用户设置的用户名 GetMobile() string GetEmail() string GetName() string GetAvatar() 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) UpdateLastLogin(userID string, lastLoginType, fromIP string) (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) (verifyCode 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, authTypeID, 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[3456789]\d{9}$`), } // 永久全局的TOKEN,内部使用 fixedTokenMap = map[string]*AuthInfo{ "85e60d6a-f1f8-4837-9c7d-d7072b0ba1c6": &AuthInfo{ UserBasic: UserBasic{ UserID2: "shifengfix", Mobile: "18048531223", Name: "石峰固定", }, AuthBindInfo: &AuthBindEx{}, LoginTime: utils.DefaultTimeValue, 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, authBindInfo) 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) (verfifyCode string, authInfo *AuthInfo, err error) { if authToken != "" { authInfo, 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 { verfifyCode, err = handler.SendVerifyCode(authID) } } return verfifyCode, authInfo, 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 LoginInternal(ctx *Context, 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 { 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() // } } else if authBindEx.UserID != "" { user = userProvider.GetUser(authBindEx.UserID, UserIDID) } } authInfo = createAuthInfo(user, authBindEx) if user != nil && user.GetID() != "" { userProvider.UpdateLastLogin(user.GetID(), authType, ctx.GetRealRemoteIP()) } //如果是小程序 if authType == "weixinmini" || authType == "weixinapp" { appID := strings.Split(authSecret, ",")[0] if appID == "wxa4a76d7b4c88604e" || appID == "wx2d6949f724b2541d" || appID == "wx18111a41fd17f24f" { //菜市或者果园 if user != nil { binds, err := dao.GetUserBindAuthInfo(dao.GetDB(), user.GetID(), 0, nil, "", "", "wx2bb99eb5d2c9b82c") if err != nil { return authInfo, err } if len(binds) == 0 { authInfo.IsExistOpenID = false } else { authInfo.IsExistOpenID = true } } } } } } else { err = ErrIllegalAuthType } return authInfo, err } func Login(ctx *Context, authType, authID, authIDType, authSecret string) (authInfo *AuthInfo, err error) { if authSecret == InternalAuthSecret { authSecret = "" } return LoginInternal(ctx, authType, authID, authIDType, authSecret) } // 通过临时TOKEN绑定新创建的用户 func BindUser(inauthInfo *AuthInfo, user IUser) (outauthInfo *AuthInfo, err error) { if err = AddAuthBind(user, inauthInfo); err == nil { inauthInfo.AuthBindInfo.UserID = user.GetID() outauthInfo = createAuthInfo(user, inauthInfo.AuthBindInfo) } return outauthInfo, err } // 添加新绑定 func AddAuthBind(user IUser, newAuthInfo *AuthInfo) (err error) { globals.SugarLogger.Debugf("AddAuthBind user:%s, newAuthInfo:%s", utils.Format4Output(user, true), utils.Format4Output(newAuthInfo, true)) 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 { if handler := authers[newAuthInfo.AuthBindInfo.Type]; handler != nil { newAuthInfo.AuthBindInfo.UserID = user.GetID() handler.UnbindAuth(user.GetID(), newAuthInfo.GetAuthType(), newAuthInfo.GetAuthTypeID(), user.GetName()) err = handler.AddAuthBind(newAuthInfo.AuthBindInfo, user.GetName()) } else { err = ErrIllegalAuthType } } return err } func UnbindAuth(userID, authType, authTypeID, userName string) (err error) { globals.SugarLogger.Debugf("UnbindAuth userID:%s, authType:%s, authTypeID:%s, userName:%s", userID, authType, authTypeID, userName) if handler := authers[authType]; handler != nil { err = handler.UnbindAuth(userID, authType, authTypeID, 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, authBindInfo *AuthBindEx) (token string, tokenType int) { userID := TokenUserEmpty userName := authBindInfo.AuthID 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"), authBindInfo.Type, utils.GetUUID(), userName, }, 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, model.AuthBindTypeAuth, nil, "", "", "") } func DeletedTokenInfoWithoutParam(authInfo *AuthInfo) (err error) { userID := authInfo.UserID tokens, err := api.Cacher.Keys("TOKEN.V2." + userID + "*") for _, v := range tokens { if authInfo.Token != v { RemoveUserInfo(v) } } return err } func CheckWeixinminiAuthBind(userID string) (err error) { var ( db = dao.GetDB() ) authBinds, err := dao.GetUserBindAuthInfo(db, userID, model.AuthBindTypeAuth, []string{"weixinmini", "weixinapp"}, "", "", "") if len(authBinds) == 0 { return fmt.Errorf("请绑定微信认证方式!") } else { return nil } return err }