support setting token min refresh interval
This commit is contained in:
@@ -680,5 +680,6 @@ func printUserInfo(user *models.User) {
|
||||
func printTokenInfo(token *models.TokenRecord) {
|
||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.CreatedUnixTime), token.CreatedUnixTime)
|
||||
fmt.Printf("[ExpiredAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.ExpiredUnixTime), token.ExpiredUnixTime)
|
||||
fmt.Printf("[LastSeen] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.LastSeenUnixTime), token.LastSeenUnixTime)
|
||||
fmt.Printf("[UserAgent] %s\n", token.UserAgent)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,10 @@ enable_two_factor = true
|
||||
# Token expired seconds (60 - 4294967295), default is 2592000 (30 days)
|
||||
token_expired_time = 2592000
|
||||
|
||||
# Token minimum refresh interval (0 - 4294967295), the value should be less than token expired time
|
||||
# Set to 0 to refresh the token every time when refreshing the front end, default is 86400 (1 day)
|
||||
token_min_refresh_interval = 86400
|
||||
|
||||
# Temporary token expired seconds (60 - 4294967295), default is 300 (5 minutes)
|
||||
temporary_token_expired_time = 300
|
||||
|
||||
|
||||
+36
-3
@@ -2,12 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -44,8 +46,7 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
|
||||
TokenId: a.tokens.GenerateTokenId(token),
|
||||
TokenType: token.TokenType,
|
||||
UserAgent: token.UserAgent,
|
||||
CreatedAt: token.CreatedUnixTime,
|
||||
ExpiredAt: token.ExpiredUnixTime,
|
||||
LastSeen: token.LastSeenUnixTime,
|
||||
}
|
||||
|
||||
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
||||
@@ -176,6 +177,39 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
|
||||
if now-oldTokenClaims.IssuedAt < int64(settings.Container.Current.TokenMinRefreshInterval) {
|
||||
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
|
||||
|
||||
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
|
||||
} else {
|
||||
tokenRecord := &models.TokenRecord{
|
||||
Uid: oldTokenClaims.Uid,
|
||||
UserTokenId: userTokenId,
|
||||
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
||||
}
|
||||
|
||||
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
||||
|
||||
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
refreshResp := &models.TokenRefreshResponse{
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
|
||||
return refreshResp, nil
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
@@ -183,7 +217,6 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
||||
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||
}
|
||||
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||
oldTokenRecord := &models.TokenRecord{
|
||||
Uid: uid,
|
||||
|
||||
+8
-7
@@ -13,11 +13,12 @@ var (
|
||||
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid duplicate checker type")
|
||||
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
|
||||
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid token expired time")
|
||||
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid temporary token expired time")
|
||||
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid email verify token expired time")
|
||||
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid avatar provider")
|
||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid map provider")
|
||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid amap security verification method")
|
||||
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid password reset token expired time")
|
||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid exchange rates data source")
|
||||
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid token min refresh interval")
|
||||
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid temporary token expired time")
|
||||
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid email verify token expired time")
|
||||
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid avatar provider")
|
||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid map provider")
|
||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid amap security verification method")
|
||||
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid password reset token expired time")
|
||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid exchange rates data source")
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ type TokenRecord struct {
|
||||
UserAgent string `xorm:"VARCHAR(255)"`
|
||||
CreatedUnixTime int64 `xorm:"PK"`
|
||||
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time)"`
|
||||
LastSeenUnixTime int64
|
||||
}
|
||||
|
||||
// TokenRevokeRequest represents all parameters of token revoking request
|
||||
@@ -23,8 +24,8 @@ type TokenRevokeRequest struct {
|
||||
|
||||
// TokenRefreshResponse represents all parameters of token refreshing request
|
||||
type TokenRefreshResponse struct {
|
||||
NewToken string `json:"newToken"`
|
||||
OldTokenId string `json:"oldTokenId"`
|
||||
NewToken string `json:"newToken,omitempty"`
|
||||
OldTokenId string `json:"oldTokenId,omitempty"`
|
||||
User *UserBasicInfo `json:"user"`
|
||||
}
|
||||
|
||||
@@ -33,8 +34,7 @@ type TokenInfoResponse struct {
|
||||
TokenId string `json:"tokenId"`
|
||||
TokenType core.TokenType `json:"tokenType"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
ExpiredAt int64 `json:"expiredAt"`
|
||||
LastSeen int64 `json:"lastSeen"`
|
||||
IsCurrent bool `json:"isCurrent"`
|
||||
}
|
||||
|
||||
@@ -53,5 +53,5 @@ func (a TokenInfoResponseSlice) Swap(i, j int) {
|
||||
|
||||
// Less reports whether the first item is less than the second one
|
||||
func (a TokenInfoResponseSlice) Less(i, j int) bool {
|
||||
return a[i].ExpiredAt > a[j].ExpiredAt
|
||||
return a[i].LastSeen > a[j].LastSeen
|
||||
}
|
||||
|
||||
+27
-1
@@ -58,7 +58,7 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c *core.Context, uid int
|
||||
now := time.Now().Unix()
|
||||
|
||||
var tokenRecords []*models.TokenRecord
|
||||
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
|
||||
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
|
||||
|
||||
return tokenRecords, err
|
||||
}
|
||||
@@ -98,6 +98,31 @@ func (s *TokenService) CreatePasswordResetToken(c *core.Context, user *models.Us
|
||||
return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
||||
}
|
||||
|
||||
// UpdateTokenLastSeen updates the last seen time of specified token
|
||||
func (s *TokenService) UpdateTokenLastSeen(c *core.Context, tokenRecord *models.TokenRecord) error {
|
||||
if tokenRecord.Uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
if tokenRecord.UserTokenId <= 0 {
|
||||
return errs.ErrInvalidUserTokenId
|
||||
}
|
||||
|
||||
tokenRecord.LastSeenUnixTime = time.Now().Unix()
|
||||
|
||||
return s.TokenDB(tokenRecord.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||
updatedRows, err := sess.Cols("last_seen_unix_time").Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Update(tokenRecord)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if updatedRows < 1 {
|
||||
return errs.ErrTokenRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteToken deletes given token from database
|
||||
func (s *TokenService) DeleteToken(c *core.Context, tokenRecord *models.TokenRecord) error {
|
||||
if tokenRecord.Uid <= 0 {
|
||||
@@ -300,6 +325,7 @@ func (s *TokenService) createToken(c *core.Context, user *models.User, tokenType
|
||||
UserAgent: userAgent,
|
||||
CreatedUnixTime: now.Unix(),
|
||||
ExpiredUnixTime: now.Add(expiryDate).Unix(),
|
||||
LastSeenUnixTime: now.Unix(),
|
||||
}
|
||||
|
||||
if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil {
|
||||
|
||||
@@ -124,6 +124,7 @@ const (
|
||||
|
||||
defaultSecretKey string = "ezbookkeeping"
|
||||
defaultTokenExpiredTime uint32 = 2592000 // 30 days
|
||||
defaultTokenMinRefreshInterval uint32 = 86400 // 1 day
|
||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||
@@ -217,6 +218,7 @@ type Config struct {
|
||||
EnableTwoFactor bool
|
||||
TokenExpiredTime uint32
|
||||
TokenExpiredTimeDuration time.Duration
|
||||
TokenMinRefreshInterval uint32
|
||||
TemporaryTokenExpiredTime uint32
|
||||
TemporaryTokenExpiredTimeDuration time.Duration
|
||||
EmailVerifyTokenExpiredTime uint32
|
||||
@@ -572,6 +574,12 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
|
||||
|
||||
config.TokenExpiredTimeDuration = time.Duration(config.TokenExpiredTime) * time.Second
|
||||
|
||||
config.TokenMinRefreshInterval = getConfigItemUint32Value(configFile, sectionName, "token_min_refresh_interval", defaultTokenMinRefreshInterval)
|
||||
|
||||
if config.TokenMinRefreshInterval >= config.TokenExpiredTime {
|
||||
return errs.ErrInvalidTokenMinRefreshInterval
|
||||
}
|
||||
|
||||
config.TemporaryTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
|
||||
|
||||
if config.TemporaryTokenExpiredTime < 60 {
|
||||
|
||||
+4
-4
@@ -40,14 +40,14 @@ export const useTokensStore = defineStore('tokens', {
|
||||
services.refreshToken().then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (data && data.success && data.result && data.result.newToken) {
|
||||
userState.updateToken(data.result.newToken);
|
||||
|
||||
if (data.result.user && isObject(data.result.user)) {
|
||||
if (data && data.success && data.result && data.result.user && isObject(data.result.user)) {
|
||||
const userStore = useUserStore();
|
||||
userStore.storeUserInfo(data.result.user);
|
||||
}
|
||||
|
||||
if (data && data.success && data.result && data.result.newToken) {
|
||||
userState.updateToken(data.result.newToken);
|
||||
|
||||
if (data.result.oldTokenId) {
|
||||
self.revokeToken({
|
||||
tokenId: data.result.oldTokenId,
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
{{ session.deviceType }}
|
||||
</td>
|
||||
<td class="text-sm">{{ session.deviceInfo }}</td>
|
||||
<td class="text-sm">{{ session.createdAt }}</td>
|
||||
<td class="text-sm">{{ session.lastSeen }}</td>
|
||||
<td class="text-sm text-right">
|
||||
<v-btn density="comfortable" color="error" variant="tonal"
|
||||
:disabled="session.isCurrent || loadingSession"
|
||||
@@ -210,7 +210,7 @@ export default {
|
||||
deviceType: this.$t(token.isCurrent ? 'Current' : 'Other Device'),
|
||||
deviceInfo: parseDeviceInfo(token.userAgent),
|
||||
icon: this.getTokenIcon(token),
|
||||
createdAt: this.$locale.formatUnixTimeToLongDateTime(this.userStore, token.createdAt)
|
||||
lastSeen: token.lastSeen ? this.$locale.formatUnixTimeToLongDateTime(this.userStore, token.lastSeen) : '-'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<f7-icon :f7="session.icon"></f7-icon>
|
||||
</template>
|
||||
<template #after>
|
||||
<small>{{ session.createdAt }}</small>
|
||||
<small>{{ session.lastSeen }}</small>
|
||||
</template>
|
||||
<f7-swipeout-actions right v-if="!session.isCurrent">
|
||||
<f7-swipeout-button color="red" :text="$t('Log Out')" @click="revoke(session)"></f7-swipeout-button>
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
deviceType: this.$t(token.isCurrent ? 'Current' : 'Other Device'),
|
||||
deviceInfo: parseDeviceInfo(token.userAgent),
|
||||
icon: this.getTokenIcon(token),
|
||||
createdAt: this.$locale.formatUnixTimeToLongDateTime(this.userStore, token.createdAt)
|
||||
lastSeen: token.lastSeen ? this.$locale.formatUnixTimeToLongDateTime(this.userStore, token.lastSeen) : '-'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user