multi terminal kick eachOther

This commit is contained in:
Gordon
2021-11-25 14:12:52 +08:00
parent 912ec51315
commit e7e3b6dd15
45 changed files with 553 additions and 464 deletions
+2 -6
View File
@@ -145,12 +145,8 @@ type config struct {
}
}
Secret string `yaml:"secret"`
MultiLoginPolicy struct {
OnlyOneTerminalAccess bool `yaml:"onlyOneTerminalAccess"`
MobileAndPCTerminalAccessButOtherTerminalKickEachOther bool `yaml:"mobileAndPCTerminalAccessButOtherTerminalKickEachOther"`
AllTerminalAccess bool `yaml:"allTerminalAccess"`
}
TokenPolicy struct {
MultiLoginPolicy int `yaml:"multiloginpolicy"`
TokenPolicy struct {
AccessSecret string `yaml:"accessSecret"`
AccessExpire int64 `yaml:"accessExpire"`
}
+16
View File
@@ -22,6 +22,7 @@ const (
WSSendMsg = 1003
WSPullMsgBySeqList = 1004
WSPushMsg = 2001
WSKickOnlineMsg = 2002
WSDataError = 3001
///ContentType
@@ -70,6 +71,21 @@ const (
//SessionType
SingleChatType = 1
GroupChatType = 2
//token
NormalToken = 0
InValidToken = 1
KickedToken = 2
ExpiredToken = 3
//MultiTerminalLogin
//全端登录,但是同端互斥
AllLoginButSameTermKick = 1
//所有端中只能有一端能够登录
SingleTerminalLogin = 2
//web端可以同时在线,其他端只能有一端登录
WebAndOther = 3
//Pc端互斥,移动端互斥,但是web端可以同时在线
PcMobileAndWeb = 4
)
var ContentType2PushContent = map[int64]string{
@@ -1,4 +1,6 @@
package config
package constant
import "errors"
// key = errCode, string = errMsg
type ErrInfo struct {
@@ -20,8 +22,6 @@ var (
ErrorUserRegister = ErrInfo{600, "User registration failed"}
ErrAccountExists = ErrInfo{601, "The account is already registered and cannot be registered again"}
ErrUserPassword = ErrInfo{602, "User password error"}
ErrTokenIncorrect = ErrInfo{603, "Invalid token"}
ErrTokenExpired = ErrInfo{604, "Expired token"}
ErrRefreshToken = ErrInfo{605, "Failed to refresh token"}
ErrAddFriend = ErrInfo{606, "Failed to add friends"}
ErrAgreeToAddFriend = ErrInfo{607, "Failed to agree application"}
@@ -40,9 +40,26 @@ var (
ErrJoinGroupApplication = ErrInfo{620, "Failed to apply to join the group"}
ErrQuitGroup = ErrInfo{621, "Failed to quit the group"}
ErrSetGroupInfo = ErrInfo{622, "Failed to set group info"}
ErrParam = ErrInfo{ErrCode: 700, ErrMsg: "param failed"}
ErrParam = ErrInfo{700, "param failed"}
ErrTokenExpired = ErrInfo{701, TokenExpired.Error()}
ErrTokenInvalid = ErrInfo{702, TokenInvalid.Error()}
ErrTokenMalformed = ErrInfo{703, TokenMalformed.Error()}
ErrTokenNotValidYet = ErrInfo{704, TokenNotValidYet.Error()}
ErrTokenUnknown = ErrInfo{705, TokenUnknown.Error()}
ErrAccess = ErrInfo{ErrCode: 800, ErrMsg: "no permission"}
ErrDb = ErrInfo{ErrCode: 900, ErrMsg: "db failed"}
)
var (
TokenExpired = errors.New("token is timed out, please log in again")
TokenInvalid = errors.New("token has been invalidated")
TokenNotValidYet = errors.New("token not active yet")
TokenMalformed = errors.New("that's not even a token")
TokenUnknown = errors.New("couldn't handle this token")
)
func (e *ErrInfo) Error() string {
return e.ErrMsg
}
@@ -0,0 +1,66 @@
package constant
// fixme 1<--->IOS 2<--->Android 3<--->Windows
//fixme 4<--->OSX 5<--->Web 6<--->MiniWeb 7<--->Linux
const (
//Platform ID
IOSPlatformID = 1
AndroidPlatformID = 2
WindowsPlatformID = 3
OSXPlatformID = 4
WebPlatformID = 5
MiniWebPlatformID = 6
LinuxPlatformID = 7
//Platform string match to Platform ID
IOSPlatformStr = "IOS"
AndroidPlatformStr = "Android"
WindowsPlatformStr = "Windows"
OSXPlatformStr = "OSX"
WebPlatformStr = "Web"
MiniWebPlatformStr = "MiniWeb"
LinuxPlatformStr = "Linux"
//terminal types
TerminalPC = "PC"
TerminalMobile = "Mobile"
)
var PlatformID2Name = map[int32]string{
IOSPlatformID: IOSPlatformStr,
AndroidPlatformID: AndroidPlatformStr,
WindowsPlatformID: WindowsPlatformStr,
OSXPlatformID: OSXPlatformStr,
WebPlatformID: WebPlatformStr,
MiniWebPlatformID: MiniWebPlatformStr,
LinuxPlatformID: LinuxPlatformStr,
}
var PlatformName2ID = map[string]int32{
IOSPlatformStr: IOSPlatformID,
AndroidPlatformStr: AndroidPlatformID,
WindowsPlatformStr: WindowsPlatformID,
OSXPlatformStr: OSXPlatformID,
WebPlatformStr: WebPlatformID,
MiniWebPlatformStr: MiniWebPlatformID,
LinuxPlatformStr: LinuxPlatformID,
}
var Platform2class = map[string]string{
IOSPlatformStr: TerminalMobile,
AndroidPlatformStr: TerminalMobile,
MiniWebPlatformStr: WebPlatformStr,
WebPlatformStr: WebPlatformStr,
WindowsPlatformStr: TerminalPC,
OSXPlatformStr: TerminalPC,
LinuxPlatformStr: TerminalPC,
}
func PlatformIDToName(num int32) string {
return PlatformID2Name[num]
}
func PlatformNameToID(name string) int32 {
return PlatformName2ID[name]
}
func PlatformNameToClass(name string) string {
return Platform2class[name]
}
+27 -22
View File
@@ -2,6 +2,7 @@ package db
import (
log2 "Open_IM/pkg/common/log"
"Open_IM/pkg/utils"
"github.com/garyburd/redigo/redis"
)
@@ -10,6 +11,7 @@ const (
appleDeviceToken = "DEVICE_TOKEN"
lastGetSeq = "LAST_GET_SEQ"
userMinSeq = "REDIS_USER_MIN_SEQ:"
uidPidToken = "UID_PID_TOKEN:"
)
func (d *DataBases) Exec(cmd string, key interface{}, args ...interface{}) (interface{}, error) {
@@ -71,36 +73,39 @@ func (d *DataBases) DelAppleDeviceToken(accountAddress string) (err error) {
return err
}
//Record the last time the user actively pulled the value of Seq
func (d *DataBases) SetLastGetSeq(uid string) (err error) {
key := lastGetSeq + uid
_, err = d.Exec("SET", key)
return err
}
//Get the value of the user's last active pull Seq
func (d *DataBases) GetLastGetSeq(uid string) (int64, error) {
key := lastGetSeq + uid
return redis.Int64(d.Exec("GET", key))
}
//Store userid and platform class to redis
func (d *DataBases) SetUserIDAndPlatform(userID, platformClass, value string, ttl int64) error {
key := userID + platformClass
_, err := d.Exec("SET", key, value, "EX", ttl)
func (d *DataBases) AddTokenFlag(userID string, platformID int32, token string, flag int) error {
key := uidPidToken + userID + ":" + utils.Int32ToString(platformID)
m, err := redis.IntMap(d.Exec("GET", key))
if err != nil && err != redis.ErrNil {
return err
}
if err == redis.ErrNil {
m = make(map[string]int, 5)
}
m[token] = flag
_, err1 := d.Exec("SET", key, m)
return err1
}
func (d *DataBases) GetTokenMapByUidPid(userID, platformID string) (map[string]int, error) {
key := uidPidToken + userID + ":" + platformID
return redis.IntMap(d.Exec("GET", key))
}
func (d *DataBases) SetTokenMapByUidPid(userID, platformID string, m map[string]int) error {
key := uidPidToken + userID + ":" + platformID
_, err := d.Exec("SET", key, m)
return err
}
//Check exists userid and platform class from redis
func (d *DataBases) ExistsUserIDAndPlatform(userID, platformClass string) (interface{}, error) {
func (d *DataBases) ExistsUserIDAndPlatform(userID, platformClass string) (int64, error) {
key := userID + platformClass
exists, err := d.Exec("EXISTS", key)
return exists, err
return redis.Int64(d.Exec("EXISTS", key))
}
//Get platform class Token
func (d *DataBases) GetPlatformToken(userID, platformClass string) (interface{}, error) {
func (d *DataBases) GetPlatformToken(userID, platformClass string) (string, error) {
key := userID + platformClass
token, err := d.Exec("GET", key)
return token, err
return redis.String(d.Exec("GET", key))
}
@@ -1,73 +1,73 @@
package multi_terminal_login
import (
"Open_IM/internal/push/content_struct"
"Open_IM/internal/push/logic"
"Open_IM/pkg/common/config"
"Open_IM/pkg/common/constant"
"Open_IM/pkg/common/db"
pbChat "Open_IM/pkg/proto/chat"
"Open_IM/pkg/utils"
)
func MultiTerminalLoginChecker(uid, token string, platformID int32) error {
// 1.check userid and platform class 0 not exists and 1 exists
existsInterface, err := db.DB.ExistsUserIDAndPlatform(uid, utils.PlatformNameToClass(utils.PlatformIDToName(platformID)))
if err != nil {
return err
}
exists := existsInterface.(int64)
//get config multi login policy
if config.Config.MultiLoginPolicy.OnlyOneTerminalAccess {
//OnlyOneTerminalAccess policy need to check all terminal
if utils.PlatformNameToClass(utils.PlatformIDToName(platformID)) == "PC" {
existsInterface, err = db.DB.ExistsUserIDAndPlatform(uid, "Mobile")
if err != nil {
return err
}
} else {
existsInterface, err = db.DB.ExistsUserIDAndPlatform(uid, "PC")
if err != nil {
return err
}
}
exists = existsInterface.(int64)
if exists == 1 {
err := db.DB.SetUserIDAndPlatform(uid, utils.PlatformNameToClass(utils.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire)
if err != nil {
return err
}
PushMessageToTheTerminal(uid, platformID)
return nil
}
} else if config.Config.MultiLoginPolicy.MobileAndPCTerminalAccessButOtherTerminalKickEachOther {
// common terminal need to kick eich other
if exists == 1 {
err := db.DB.SetUserIDAndPlatform(uid, utils.PlatformNameToClass(utils.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire)
if err != nil {
return err
}
PushMessageToTheTerminal(uid, platformID)
return nil
}
}
err = db.DB.SetUserIDAndPlatform(uid, utils.PlatformNameToClass(utils.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire)
if err != nil {
return err
}
PushMessageToTheTerminal(uid, platformID)
return nil
}
func PushMessageToTheTerminal(uid string, platform int32) {
logic.SendMsgByWS(&pbChat.WSToMsgSvrChatMsg{
SendID: uid,
RecvID: uid,
Content: content_struct.NewContentStructString(1, "", "Your account is already logged on other terminal,please confirm"),
SendTime: utils.GetCurrentTimestampBySecond(),
MsgFrom: constant.SysMsgType,
ContentType: constant.KickOnlineTip,
PlatformID: platform,
})
}
//
//import (
// "Open_IM/internal/push/content_struct"
// "Open_IM/internal/push/logic"
// "Open_IM/pkg/common/config"
// "Open_IM/pkg/common/constant"
// "Open_IM/pkg/common/db"
// pbChat "Open_IM/pkg/proto/chat"
// "Open_IM/pkg/utils"
//)
//
//const DayOfSecond = 24*60*60
//func MultiTerminalLoginChecker(uid, token string, platformID int32) error {
// // 1.check userid and platform class 0 not exists and 1 exists
// exists, err := db.DB.ExistsUserIDAndPlatform(uid, constant.PlatformNameToClass(constant.PlatformIDToName(platformID)))
// if err != nil {
// return err
// }
// //get config multi login policy
// if config.Config.MultiLoginPolicy {
// //OnlyOneTerminalAccess policy need to check all terminal
// if constant.PlatformNameToClass(constant.PlatformIDToName(platformID)) == "PC" {
// exists, err = db.DB.ExistsUserIDAndPlatform(uid, "Mobile")
// if err != nil {
// return err
// }
// } else {
// exists, err = db.DB.ExistsUserIDAndPlatform(uid, "PC")
// if err != nil {
// return err
// }
// }
// if exists == 1 {
// err := db.DB.SetUserIDAndPlatform(uid, constant.PlatformNameToClass(constant.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire*DayOfSecond)
// if err != nil {
// return err
// }
// PushMessageToTheTerminal(uid, platformID)
// return nil
// }
// } else if config.Config.MultiLoginPolicy.MobileAndPCTerminalAccessButOtherTerminalKickEachOther {
// // common terminal need to kick eich other
// if exists == 1 {
// err := db.DB.SetUserIDAndPlatform(uid, constant.PlatformNameToClass(constant.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire*DayOfSecond)
// if err != nil {
// return err
// }
// PushMessageToTheTerminal(uid, platformID)
// return nil
// }
// }
// err = db.DB.SetUserIDAndPlatform(uid, constant.PlatformNameToClass(constant.PlatformIDToName(platformID)), token, config.Config.TokenPolicy.AccessExpire*DayOfSecond)
// if err != nil {
// return err
// }
// PushMessageToTheTerminal(uid, platformID)
// return nil
//}
////
////func PushMessageToTheTerminal(uid string, platform int32) {
////
//// logic.SendMsgByWS(&pbChat.WSToMsgSvrChatMsg{
//// SendID: uid,
//// RecvID: uid,
//// Content: content_struct.NewContentStructString(1, "", "Your account is already logged on other terminal,please confirm"),
//// SendTime: utils.GetCurrentTimestampBySecond(),
//// MsgFrom: constant.SysMsgType,
//// ContentType: constant.KickOnlineTip,
//// PlatformID: platform,
//// })
////}
+144
View File
@@ -0,0 +1,144 @@
package token_verify
import (
"Open_IM/pkg/common/config"
"Open_IM/pkg/common/constant"
commonDB "Open_IM/pkg/common/db"
"Open_IM/pkg/common/log"
"github.com/golang-jwt/jwt/v4"
"time"
)
//var (
// TokenExpired = errors.New("token is timed out, please log in again")
// TokenInvalid = errors.New("token has been invalidated")
// TokenNotValidYet = errors.New("token not active yet")
// TokenMalformed = errors.New("that's not even a token")
// TokenUnknown = errors.New("couldn't handle this token")
//)
type Claims struct {
UID string
Platform string //login platform
jwt.RegisteredClaims
}
func BuildClaims(uid, platform string, ttl int64) Claims {
now := time.Now()
return Claims{
UID: uid,
Platform: platform,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(ttl*24) * time.Hour)), //Expiration time
IssuedAt: jwt.NewNumericDate(now), //Issuing time
NotBefore: jwt.NewNumericDate(now), //Begin Effective time
}}
}
func CreateToken(userID string, platformID int32) (string, int64, error) {
claims := BuildClaims(userID, constant.PlatformIDToName(platformID), config.Config.TokenPolicy.AccessExpire)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(config.Config.TokenPolicy.AccessSecret))
if err != nil {
return "", 0, err
}
err = commonDB.DB.AddTokenFlag(userID, platformID, tokenString, constant.NormalToken)
if err != nil {
return "", 0, err
}
return tokenString, claims.ExpiresAt.Time.Unix(), err
}
func secret() jwt.Keyfunc {
return func(token *jwt.Token) (interface{}, error) {
return []byte(config.Config.TokenPolicy.AccessSecret), nil
}
}
func getClaimFromToken(tokensString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokensString, &Claims{}, secret())
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, &constant.ErrTokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
return nil, &constant.ErrTokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, &constant.ErrTokenNotValidYet
} else {
return nil, &constant.ErrTokenUnknown
}
} else {
return nil, &constant.ErrTokenNotValidYet
}
} else {
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, &constant.ErrTokenNotValidYet
}
}
func ParseToken(tokensString string) (claims *Claims, err error) {
//根据token的算法本身做出的有效性检验
claims, err = getClaimFromToken(tokensString)
if err != nil {
log.NewError("", "token validate err", err.Error())
return nil, err
}
//根据redis做出的有效性检验
m, err := commonDB.DB.GetTokenMapByUidPid(claims.UID, claims.Platform)
if err != nil {
log.NewError("", "get token from redis err", err.Error())
return nil, &constant.ErrTokenInvalid
}
if m == nil {
log.NewError("", "get token from redis err", "m is nil")
return nil, &constant.ErrTokenInvalid
}
if v, ok := m[tokensString]; ok {
switch v {
case constant.NormalToken:
return claims, nil
case constant.InValidToken:
return nil, &constant.ErrTokenInvalid
case constant.KickedToken:
return nil, &constant.ErrTokenInvalid
case constant.ExpiredToken:
return nil, &constant.ErrTokenExpired
}
}
return nil, err
}
//func MakeTheTokenInvalid(currentClaims *Claims, platformClass string) (bool, error) {
// storedRedisTokenInterface, err := db.DB.GetPlatformToken(currentClaims.UID, platformClass)
// if err != nil {
// return false, err
// }
// storedRedisPlatformClaims, err := ParseRedisInterfaceToken(storedRedisTokenInterface)
// if err != nil {
// return false, err
// }
// //if issue time less than redis token then make this token invalid
// if currentClaims.IssuedAt.Time.Unix() < storedRedisPlatformClaims.IssuedAt.Time.Unix() {
// return true, constant.TokenInvalid
// }
// return false, nil
//}
func ParseRedisInterfaceToken(redisToken interface{}) (*Claims, error) {
return getClaimFromToken(string(redisToken.([]uint8)))
}
//Validation token, false means failure, true means successful verification
func VerifyToken(token, uid string) (bool, error) {
claims, err := ParseToken(token)
if err != nil {
return false, err
}
if claims.UID != uid {
return false, &constant.ErrTokenUnknown
}
return true, nil
}