Refactoring

This commit is contained in:
oyurov 2022-11-02 13:03:17 +01:00
parent 215a44b7c7
commit 6d34aa237c
13 changed files with 166 additions and 78 deletions

View File

@ -43,6 +43,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
@ -125,7 +127,7 @@ public class AuthController extends BaseController {
sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
ObjectNode response = JacksonUtil.newObjectNode();
response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken());
response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken());
@ -303,7 +305,7 @@ public class AuthController extends BaseController {
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return tokenFactory.createTokenPair(securityUser);
} else {
@ -359,7 +361,7 @@ public class AuthController extends BaseController {
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false));
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
} catch (Exception e) {
throw handleException(e);
}

View File

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.security.model.JwtTokenPair;
@ -371,7 +372,7 @@ public class UserController extends BaseController {
userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
if (!userCredentialsEnabled) {
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
}
} catch (Exception e) {
throw handleException(e);

View File

@ -19,16 +19,14 @@ import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.usersUpdateTime.UsersUpdateTimeCacheEvictEvent;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.HashMap;
import java.util.Optional;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -36,13 +34,14 @@ import static java.util.concurrent.TimeUnit.SECONDS;
@Service
@RequiredArgsConstructor
public class TokenOutdatingService extends AbstractCachedEntityService<UserId, HashMap<String, Long>, UsersUpdateTimeCacheEvictEvent> {
public class TokenOutdatingService {
private final TbTransactionalCache<String, Long> cache;
private final JwtTokenFactory tokenFactory;
private final JwtSettings jwtSettings;
@EventListener(classes = UserAuthDataChangedEvent.class)
public void onUserAuthDataChanged(UserAuthDataChangedEvent event) {
processUserSessions(event);
cache.put(event.getId(), event.getTs());
}
public boolean isOutdated(JwtToken token, UserId userId) {
@ -50,48 +49,36 @@ public class TokenOutdatingService extends AbstractCachedEntityService<UserId, H
long issueTime = claims.getIssuedAt().getTime();
String sessionId = claims.get("sessionId", String.class);
return Optional.ofNullable(cache.get(userId))
.map(outdatageTime -> {
if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId));
Boolean isUserIdOutdated = Optional.ofNullable(cache.get(userId.toString()))
.map(outdatageTimeByUserId -> {
if (refreshTokenNotExpired(outdatageTimeByUserId.get(), System.currentTimeMillis())) {
return accessTokenNotExpired(issueTime, outdatageTimeByUserId.get());
} else {
/*
* Means that since the outdating has passed more than
* the lifetime of refresh token (the longest lived)
* and there is no need to store outdatage time anymore
* as all the tokens issued before the outdatage time
* are now expired by themselves
* */
handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId));
return false;
}
})
.orElse(false);
if (!isUserIdOutdated) {
return Optional.ofNullable(cache.get(sessionId)).map(outdatageTimeBySessionId -> {
if (refreshTokenNotExpired(outdatageTimeBySessionId.get(), System.currentTimeMillis())) {
return accessTokenNotExpired(issueTime, outdatageTimeBySessionId.get());
} else {
return false;
}
}
).orElse(false);
}
return isUserIdOutdated;
}
@TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class)
@Override
public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) {
HashMap<String, Long> userSessions = cache.get(event.getUserId()).get();
if (userSessions != null) {
userSessions.remove(event.getSessionId());
cache.put(event.getUserId(), userSessions);
}
private boolean accessTokenNotExpired(long issueTime, Long outdatageTime) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime);
}
private void processUserSessions(UserAuthDataChangedEvent event) {
if (cache.get(event.getUserId()) != null) {
HashMap<String, Long> userSessions = cache.get(event.getUserId()).get();
if (event.isDropAllSessions()) {
userSessions.replaceAll((k, v) -> event.getTs());
} else {
userSessions.put(event.getSessionId(), event.getTs());
}
cache.put(event.getUserId(), userSessions);
} else {
cache.put(event.getUserId(), new HashMap<>() {{
put(event.getSessionId(), event.getTs());
}});
}
private boolean refreshTokenNotExpired(Long outdatageTime, long currentTime) {
return currentTime - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime());
}
}

View File

@ -66,7 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
} else {
securityUser = authenticateByPublicId(principal.getValue());
}
securityUser.setSessionId(unsafeUser.getSessionId());
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
throw new CredentialsExpiredException("Token is outdated");
}

View File

@ -120,7 +120,9 @@ public class JwtTokenFactory {
if (customerId != null) {
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
}
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
UserPrincipal principal;
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
@ -163,7 +165,9 @@ public class JwtTokenFactory {
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setUserPrincipal(principal);
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
return securityUser;
}

View File

@ -33,6 +33,8 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -106,7 +108,7 @@ public class TokenOutdatingTest {
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
SECONDS.sleep(1);
@ -124,7 +126,7 @@ public class TokenOutdatingTest {
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
@ -140,7 +142,7 @@ public class TokenOutdatingTest {
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(CredentialsExpiredException.class, () -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
@ -152,7 +154,7 @@ public class TokenOutdatingTest {
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
SECONDS.sleep(1);
@ -175,7 +177,8 @@ public class TokenOutdatingTest {
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
tokenOutdatingService.onUserAuthDataChanged(new UserSessionInvalidationEvent(securityUser.getSessionId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
@ -186,6 +189,34 @@ public class TokenOutdatingTest {
});
}
@Test
public void testResetAllSessions() throws InterruptedException {
JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal());
JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser);
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
}
private RawAccessJwtToken getRawJwtToken(JwtToken token) {
return new RawAccessJwtToken(token.getToken());

View File

@ -24,13 +24,10 @@ import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.UserId;
import java.util.HashMap;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UsersUpdateTimeCache")
public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache<UserId, HashMap<String, Long>> {
public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache<String, Long> {
@Autowired
public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {

View File

@ -16,10 +16,8 @@
package org.thingsboard.server.cache.usersUpdateTime;
import lombok.Data;
import org.thingsboard.server.common.data.id.UserId;
@Data
public class UsersUpdateTimeCacheEvictEvent {
private final UserId userId;
private final String sessionId;
private final String key;
}

View File

@ -21,14 +21,11 @@ import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.UserId;
import java.util.HashMap;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UsersUpdateTimeCache")
public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache<UserId, HashMap<String, Long>> {
public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache<String, Long> {
@Autowired
public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) {

View File

@ -20,18 +20,7 @@ import org.thingsboard.server.common.data.id.UserId;
import java.io.Serializable;
@Data
public class UserAuthDataChangedEvent implements Serializable {
private final UserId userId;
private final String sessionId;
private final long ts;
private final boolean dropAllSessions;
public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) {
this.userId = userId;
this.sessionId = sessionId;
this.dropAllSessions = dropAllSessions;
this.ts = System.currentTimeMillis();
}
public abstract class UserAuthDataChangedEvent implements Serializable {
public abstract String getId();
public abstract long getTs();
}

View File

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.security.event;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.common.data.id.UserId;
@Getter
@EqualsAndHashCode(callSuper = true)
public class UserCredentialsInvalidationEvent extends UserAuthDataChangedEvent {
private final UserId userId;
private final long ts;
public UserCredentialsInvalidationEvent(UserId userId) {
this.userId = userId;
this.ts = System.currentTimeMillis();
}
@Override
public String getId() {
return userId.toString();
}
@Override
public long getTs() {
return ts;
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.security.event;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class UserSessionInvalidationEvent extends UserAuthDataChangedEvent {
private final String sessionId;
private final long ts;
public UserSessionInvalidationEvent(String sessionId) {
this.sessionId = sessionId;
this.ts = System.currentTimeMillis();
}
@Override
public String getId() {
return sessionId;
}
@Override
public long getTs() {
return ts;
}
}

View File

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
@ -211,7 +212,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true));
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
}
@Override