mirror of
https://github.com/weloe/token-go.git
synced 2025-10-05 15:36:50 +08:00
feat: support double token
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -16,7 +16,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
TokenName = "Tokengo"
|
||||
TokenName = "Tokengo"
|
||||
RefreshToken = "Tokengorefresh"
|
||||
)
|
||||
|
||||
const (
|
||||
|
73
enforcer.go
73
enforcer.go
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
30
model/refresh.go
Normal 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)
|
||||
}
|
@@ -7,6 +7,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RefreshTokenSign struct {
|
||||
Id string
|
||||
Token string
|
||||
RefreshValue string
|
||||
Device string
|
||||
}
|
||||
|
||||
type TokenSign struct {
|
||||
Value string
|
||||
Device string
|
||||
|
Reference in New Issue
Block a user