feat: support double token

This commit is contained in:
weloe
2023-11-01 04:08:05 +08:00
parent 8a6c8eba30
commit 3c7a8ef8b6
10 changed files with 332 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
# Token-Go
This library focuses on solving login authentication problems, such as: login, multi-account login, shared token login, QR Code login, logout, kickout, banned, second auth, temp-token, SSO ...
This library focuses on solving login authentication problems, such as: login, multi-account login, shared token login, token refresh, double token refresh, QR Code login, logout, kickout, banned, second auth, temp-token, SSO ...
## Installation

View File

@@ -11,6 +11,12 @@ type TokenConfig struct {
TokenName string
Timeout int64
// If you enable DoubleToken, returns double token - token and refreshToken, when token is timeout, you can use refreshToken to refreshToken to auto login.
DoubleToken bool
RefreshTokenName string
RefreshTokenTimeout int64
// If last operate time < ActivityTimeout, token expired
ActivityTimeout int64
// Data clean period
@@ -52,15 +58,46 @@ type TokenConfig struct {
CookieConfig *CookieConfig
}
func (t *TokenConfig) InitConfig() {
if t.TokenStyle == "" {
t.TokenStyle = "uuid"
}
if t.TokenName == "" {
t.TokenName = constant.TokenName
}
if t.Timeout == 0 {
t.Timeout = 60 * 60 * 24 * 30
}
if t.DeviceMaxLoginCount == 0 {
t.DeviceMaxLoginCount = 12
}
if t.DoubleToken {
if t.RefreshTokenName == "" {
t.RefreshTokenName = constant.RefreshToken
}
if t.RefreshTokenTimeout == 0 {
t.RefreshTokenTimeout = t.Timeout * 2
}
}
if t.MaxLoginCount == 0 {
t.MaxLoginCount = 12
}
if t.CookieConfig == nil {
t.CookieConfig = DefaultCookieConfig()
}
}
func DefaultTokenConfig() *TokenConfig {
return &TokenConfig{
TokenStyle: "uuid",
TokenPrefix: "",
TokenName: constant.TokenName,
Timeout: 60 * 60 * 24 * 30,
DoubleToken: false,
ActivityTimeout: -1,
DataRefreshPeriod: 30,
AutoRenew: true,
AutoRenew: false,
IsConcurrent: true,
IsShare: true,
MaxLoginCount: 12,

View File

@@ -16,7 +16,8 @@ const (
)
const (
TokenName = "Tokengo"
TokenName = "Tokengo"
RefreshToken = "Tokengorefresh"
)
const (

View File

@@ -98,6 +98,7 @@ func InitWithConfig(tokenConfig *config.TokenConfig, adapter persist.Adapter) (*
if tokenConfig == nil || adapter == nil {
return nil, errors.New("InitWithConfig() failed: parameters cannot be nil")
}
tokenConfig.InitConfig()
e := &Enforcer{
loginType: "user",
config: *tokenConfig,
@@ -119,13 +120,13 @@ func (e *Enforcer) startCleanTimer() {
return
}
dataRefreshPeriod := e.config.DataRefreshPeriod
if period := dataRefreshPeriod; period >= 0 {
err := defaultAdapter.StartCleanTimer(dataRefreshPeriod)
if period := dataRefreshPeriod; period > 0 {
err := defaultAdapter.StartCleanTimer(period)
if err != nil {
log2.Printf("enble adapter cleanTimer failed: %v", err)
return
}
e.logger.StartCleanTimer(dataRefreshPeriod)
e.logger.StartCleanTimer(period)
}
}
}
@@ -219,6 +220,17 @@ func (e *Enforcer) LoginByModel(id string, loginModel *model.Login, ctx ctx.Cont
Device: device,
})
if e.config.DoubleToken {
refreshToken, err := e.createRefreshToken(id, tokenValue, loginModel)
if err != nil {
return "", err
}
err = e.responseRefreshToken(refreshToken, loginModel, ctx)
if err != nil {
return "", err
}
}
timeout := loginModel.Timeout
// reset session
err = e.SetSession(id, session, timeout)
@@ -245,7 +257,6 @@ func (e *Enforcer) LoginByModel(id string, loginModel *model.Login, ctx ctx.Cont
Timeout: timeout,
JwtData: loginModel.JwtData,
Token: tokenValue,
IsWriteHeader: loginModel.IsWriteHeader,
}
// called logger
@@ -409,6 +420,10 @@ func (e *Enforcer) LogoutByToken(token string) error {
return err
}
}
err = e.deleteRefreshToken(token)
if err != nil {
return err
}
e.logger.Logout(e.loginType, id, token)
@@ -631,3 +646,53 @@ func (e *Enforcer) GetLoginTokenCounts() (int, error) {
}
return c, nil
}
func (e *Enforcer) GetRefreshToken(tokenValue string) string {
return e.getRefreshTokenValue(tokenValue)
}
func (e *Enforcer) RefreshToken(refreshToken string, refreshModel ...*model.Refresh) (*model.RefreshRes, error) {
var m *model.Refresh
if len(refreshModel) != 0 {
m = refreshModel[0]
} else {
m = model.DefaultRefresh()
}
return e.RefreshTokenByModel(refreshToken, m, nil)
}
func (e *Enforcer) RefreshTokenByModel(refreshToken string, refreshModel *model.Refresh, ctx ctx.Context) (*model.RefreshRes, error) {
if refreshModel == nil {
return nil, errors.New("arg refreshModel can not be nil")
}
if !e.config.DoubleToken {
return nil, fmt.Errorf("double tokens are not enabled")
}
refreshTokenSign := e.getRefreshTokenSign(refreshToken)
if refreshTokenSign == nil {
return nil, fmt.Errorf("the refresh token does not exist: %v", refreshToken)
}
err := e.deleteRefreshToken(refreshTokenSign.Token)
if err != nil {
return nil, err
}
login := &model.Login{
Device: refreshTokenSign.Device,
IsLastingCookie: refreshModel.IsLastingCookie,
Timeout: refreshModel.Timeout,
JwtData: refreshModel.JwtData,
Token: refreshModel.Token,
RefreshToken: refreshModel.RefreshToken,
RefreshTokenTimeout: refreshModel.RefreshTokenTimeout,
}
token, err := e.LoginByModel(refreshTokenSign.Id, login, ctx)
if err != nil {
return nil, err
}
return &model.RefreshRes{
Token: token,
RefreshToken: refreshToken,
}, nil
}

View File

@@ -45,6 +45,11 @@ type IEnforcer interface {
GetIdByToken(token string) string
GetLoginCount(id string, device ...string) int
// refresh api
GetRefreshToken(tokenValue string) string
RefreshToken(refreshToken string, refreshModel ...*model.Refresh) (*model.RefreshRes, error)
RefreshTokenByModel(refreshToken string, refreshModel *model.Refresh, ctx ctx.Context) (*model.RefreshRes, error)
GetLoginCounts() (int, error)
GetLoginTokenCounts() (int, error)

View File

@@ -95,7 +95,7 @@ func (e *Enforcer) ResponseToken(tokenValue string, loginModel *model.Login, ctx
}
// set token to header
if loginModel.IsWriteHeader {
if e.config.IsWriteHeader {
ctx.Response().SetHeader(tokenConfig.TokenName, tokenValue)
ctx.Response().AddHeader(constant.AccessControlExposeHeaders, tokenConfig.TokenName)
}
@@ -122,6 +122,73 @@ func (e *Enforcer) checkId(str string) (bool, error) {
return true, nil
}
func (e *Enforcer) createRefreshToken(id string, tokenValue string, loginModel *model.Login) (string, error) {
// create refreshToken
var err error
if loginModel.RefreshToken == "" {
loginModel.RefreshToken, err = e.generateFunc.Exec(e.config.TokenStyle)
if err != nil {
return "", err
}
}
if loginModel.RefreshTokenTimeout == 0 {
loginModel.RefreshTokenTimeout = e.config.RefreshTokenTimeout
}
err = e.setRefreshToken(&model.RefreshTokenSign{
Id: id,
Token: tokenValue,
RefreshValue: loginModel.RefreshToken,
Device: loginModel.Device,
}, loginModel.RefreshTokenTimeout)
if err != nil {
return "", err
}
return loginModel.RefreshToken, nil
}
// responseRefreshToken set token to cookie or header
func (e *Enforcer) responseRefreshToken(refreshTokenValue string, loginModel *model.Login, ctx ctx.Context) error {
if ctx == nil {
return nil
}
tokenConfig := e.config
// set token to cookie
if tokenConfig.IsReadCookie {
var cookieTimeout int64
if !loginModel.IsLastingCookie {
cookieTimeout = -1
} else {
if loginModel.RefreshTokenTimeout != 0 {
cookieTimeout = loginModel.RefreshTokenTimeout
} else {
cookieTimeout = tokenConfig.Timeout * 2
}
if cookieTimeout == constant.NeverExpire {
cookieTimeout = math.MaxInt64
}
}
if tokenConfig.CookieConfig.Path == "" {
tokenConfig.CookieConfig.Path = "/"
}
// add cookie use tokenConfig.CookieConfig
ctx.Response().AddCookie(tokenConfig.RefreshTokenName,
refreshTokenValue,
tokenConfig.CookieConfig.Path,
tokenConfig.CookieConfig.Domain,
cookieTimeout)
}
// set token to header
if tokenConfig.IsWriteHeader {
ctx.Response().SetHeader(tokenConfig.RefreshTokenName, refreshTokenValue)
ctx.Response().AddHeader(constant.AccessControlExposeHeaders, tokenConfig.RefreshTokenName)
}
return nil
}
func (e *Enforcer) SetIdByToken(id string, tokenValue string, timeout int64) error {
err := e.notifySetStr(e.spliceTokenKey(tokenValue), id, timeout)
return err
@@ -146,6 +213,37 @@ func (e *Enforcer) updateTokenTimeout(token string, timeout int64) error {
return err
}
func (e *Enforcer) deleteRefreshToken(tokenValue string) error {
refreshToken := e.getRefreshTokenValue(tokenValue)
err := e.notifyDelete(e.spliceRefreshTokenKey(tokenValue))
if err != nil {
return err
}
err = e.notifyDelete(e.spliceRefreshTokenSignKey(refreshToken))
return err
}
func (e *Enforcer) setRefreshToken(refreshTokenSign *model.RefreshTokenSign, timeout int64) error {
err := e.notifySet(e.spliceRefreshTokenSignKey(refreshTokenSign.RefreshValue), refreshTokenSign, timeout)
if err != nil {
return err
}
err = e.notifySetStr(e.spliceRefreshTokenKey(refreshTokenSign.Token), refreshTokenSign.RefreshValue, timeout)
return err
}
func (e *Enforcer) getRefreshTokenValue(tokenValue string) string {
return e.adapter.GetStr(e.spliceRefreshTokenKey(tokenValue))
}
func (e *Enforcer) getRefreshTokenSign(refreshToken string) *model.RefreshTokenSign {
get := e.adapter.Get(e.spliceRefreshTokenSignKey(refreshToken), util.GetType(&model.RefreshTokenSign{}))
if get != nil {
return get.(*model.RefreshTokenSign)
}
return nil
}
func (e *Enforcer) setBanned(id string, service string, level int, time int64) error {
err := e.notifySetStr(e.spliceBannedKey(id, service), strconv.Itoa(level), time)
return err
@@ -238,11 +336,21 @@ func (e *Enforcer) getByTempToken(service string, tempToken string) string {
return e.adapter.GetStr(e.spliceTempTokenKey(service, tempToken))
}
// spliceSessionKey splice session-id key
// spliceSessionKey splice id-session key
func (e *Enforcer) spliceSessionKey(id string) string {
return e.config.TokenName + ":" + e.loginType + ":session:" + id
}
// spliceRefreshTokenSignKey splice refreshToken-refreshToken key
func (e *Enforcer) spliceRefreshTokenSignKey(refreshToken string) string {
return e.config.TokenName + ":" + e.loginType + ":refreshSign:" + refreshToken
}
// spliceRefreshTokenKey splice token-refreshToken key
func (e *Enforcer) spliceRefreshTokenKey(token string) string {
return e.config.TokenName + ":" + e.loginType + ":refresh:" + token
}
// spliceTokenKey splice token-id key
func (e *Enforcer) spliceTokenKey(token string) string {
return e.config.TokenName + ":" + e.loginType + ":token:" + token

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func NewTestHttpContext(t *testing.T) (error, ctx.Context) {
@@ -564,3 +565,59 @@ func TestEnforcer_SecSafe(t *testing.T) {
t.Fatalf("IsSafe() failed, unexpected return value: %v", isSafe)
}
}
func TestEnforcer_RefreshToken(t *testing.T) {
adapter := NewDefaultAdapter()
tokenConfig := &config.TokenConfig{
DoubleToken: true,
}
enforcer, err := NewEnforcer(adapter, tokenConfig)
if err != nil {
t.Fatalf("NewEnforcer() failed: %v", err)
}
token, err := enforcer.Login("1", nil)
if err != nil {
t.Fatalf("Login() failed: %v", err)
}
t.Logf("login success. token: %v", token)
refreshToken := enforcer.GetRefreshToken(token)
t.Logf("get refreshToken: %v", refreshToken)
err = enforcer.LogoutByToken(token)
t.Logf("1 logout")
if err != nil {
t.Fatalf("LogoutByToken() failed: %v", err)
}
if enforcer.GetRefreshToken(token) != "" {
t.Fatalf("GetRefreshToken() = %v, want is nil", enforcer.GetRefreshToken(token))
}
_, err = enforcer.RefreshToken(token)
if err == nil {
t.Fatalf("RefreshToken() failed: %v", err)
}
token, err = enforcer.LoginByModel("1", &model.Login{
Device: "test",
IsLastingCookie: false,
Timeout: 1,
Token: "",
RefreshTokenTimeout: 200000,
}, nil)
if err != nil {
t.Fatalf("Login() failed: %v", err)
}
refreshToken = enforcer.GetRefreshToken(token)
time.Sleep(time.Second)
loginId, _ := enforcer.GetLoginIdByToken(token)
if loginId != "" {
t.Fatalf("GetLoginIdByToken() failed: %v", loginId)
}
refreshRes, err := enforcer.RefreshToken(refreshToken)
if err != nil {
t.Fatalf("RefreshToken() failed: %v", err)
}
t.Logf(refreshRes.String())
_, err = enforcer.RefreshToken(refreshToken)
if err == nil {
t.Fatalf("RefreshToken() failed: %v", err)
}
}

View File

@@ -1,12 +1,13 @@
package model
type Login struct {
Device string
IsLastingCookie bool
Timeout int64
JwtData map[string]interface{}
Token string
IsWriteHeader bool
Device string
IsLastingCookie bool
Timeout int64
JwtData map[string]interface{}
Token string
RefreshToken string
RefreshTokenTimeout int64
}
func DefaultLoginModel() *Login {
@@ -16,17 +17,16 @@ func DefaultLoginModel() *Login {
Timeout: 60 * 60 * 24 * 30,
JwtData: nil,
Token: "",
IsWriteHeader: true,
}
}
func CreateLoginModelByDevice(device string) *Login {
return &Login{
Device: device,
IsLastingCookie: true,
Timeout: 60 * 60 * 24 * 30,
JwtData: nil,
Token: "",
IsWriteHeader: true,
Device: device,
IsLastingCookie: true,
Timeout: 60 * 60 * 24 * 30,
JwtData: nil,
Token: "",
RefreshTokenTimeout: 60 * 60 * 24 * 30 * 2,
}
}

30
model/refresh.go Normal file
View File

@@ -0,0 +1,30 @@
package model
import "fmt"
type RefreshRes struct {
Token string
RefreshToken string
}
type Refresh struct {
IsLastingCookie bool
Token string
Timeout int64
JwtData map[string]interface{}
RefreshToken string
RefreshTokenTimeout int64
}
func DefaultRefresh() *Refresh {
return &Refresh{
IsLastingCookie: true,
Timeout: 60 * 60 * 24 * 30,
JwtData: nil,
Token: "",
}
}
func (r *RefreshRes) String() string {
return fmt.Sprintf("Token: %s, RefreshToken: %s", r.Token, r.RefreshToken)
}

View File

@@ -7,6 +7,13 @@ import (
"time"
)
type RefreshTokenSign struct {
Id string
Token string
RefreshValue string
Device string
}
type TokenSign struct {
Value string
Device string