Files
jx-callback/business/auth2/auth2.go
2019-08-21 09:17:28 +08:00

431 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 // 正式TOKEN7天有效期
TmpTokenDuration = 30 * time.Minute // 临时TOKEN30分钟有效期
MinCaptchaLen = 4
MaxCaptchaWidth = 400
MaxCaptchaHeight = 400
)
type IUser interface {
GetID() string // 这个ID是不可变的系统内部使用的唯一标识
GetID2() string // 这个是可改的,唯一的,用户设置的用户名
GetMobile() string
GetEmail() string
GetName() string
}
type IUserProvider interface {
GetUser(authID, authIDType string) (user IUser)
UpdateUserMobile(userID string, mobile string) (err error)
UpdateUserEmail(userID string, email 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) (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)
}
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,UserIDEmailauthSecret是密码的sha1
// mobile或email登录时authIDType必须相应为UserIDMobile,UserIDEmail
// 其它登录时authIDType无用
// 邮箱时如果允许authIDType是UserIDEmailauthSecret是验证码的sha1
// 手机时如果允许authIDType是UserIDMobileauthSecret是验证码的sha1
// 公众号登录authIDTypeD是UserIDEmptyauthSecret是code这个函数是被微信的回调调用不是直接被客户端调用
// 微信登录authIDType是UserIDEmptyauthSecret是code这个函数是被微信的回调调用不是直接被客户端调用
// 小程序登录authIDType是UserIDEmptyauthSecret是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))
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)
}
} 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)
}