From 1960d995045d6b9292db6e63c04a0d0577472c21 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 17 Mar 2023 13:50:49 +0200 Subject: [PATCH] REST template concurrency --- .../settings/DefaultJwtSettingsValidator.java | 4 +- pom.xml | 6 ++ rest-client/pom.xml | 4 + .../thingsboard/rest/client/RestClient.java | 92 +++++++++++-------- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 274630cc39..dddb011fd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -36,10 +36,10 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { throw new DataValidationException("JWT token issuer should be specified!"); } - if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(15)) { throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); } - if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(1)) { throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); } if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { diff --git a/pom.xml b/pom.xml index 49f30df835..15e27d2ebb 100755 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ 2.13.4 2.13.4.2 1.3.4 + 4.2.1 2.2.6 3.0.0 2.0.0-M5 @@ -1429,6 +1430,11 @@ classmate ${fasterxml-classmate.version} + + com.auth0 + java-jwt + ${auth0-jwt.version} + com.github.fge json-schema-validator diff --git a/rest-client/pom.xml b/rest-client/pom.xml index cb85c2134e..1bf93e4dc4 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -47,6 +47,10 @@ org.thingsboard.common util + + com.auth0 + java-jwt + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index c2c0a6d794..8dcdb86eab 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rest.client; +import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -28,9 +29,6 @@ import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -161,7 +159,6 @@ import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.io.Closeable; -import java.io.IOException; import java.net.URI; import java.util.Collections; import java.util.HashMap; @@ -171,6 +168,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.StringUtils.isEmpty; @@ -178,40 +176,50 @@ import static org.thingsboard.server.common.data.StringUtils.isEmpty; /** * @author Andrew Shvayka */ -public class RestClient implements ClientHttpRequestInterceptor, Closeable { +public class RestClient implements Closeable { private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; - protected final RestTemplate restTemplate; - protected final String baseURL; - private String token; - private String refreshToken; - private final ObjectMapper objectMapper = new ObjectMapper(); - private ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass()); - + private static final long AVG_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(30); protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass()); + protected final RestTemplate restTemplate; + protected final RestTemplate loginRestTemplate; + protected final String baseURL; + + private String username; + private String password; + private String mainToken; + private String refreshToken; + private long mainTokenExpTs; + private long refreshTokenExpTs; + private long clientServerTimeDiff; + public RestClient(String baseURL) { this(new RestTemplate(), baseURL); } public RestClient(RestTemplate restTemplate, String baseURL) { this.restTemplate = restTemplate; + this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory()); this.baseURL = baseURL; - } - - @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { - HttpRequest wrapper = new HttpRequestWrapper(request); - wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); - ClientHttpResponse response = execution.execute(wrapper, bytes); - if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { - synchronized (this) { - restTemplate.getInterceptors().remove(this); - refreshToken(); - wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); - return execution.execute(wrapper, bytes); + this.restTemplate.getInterceptors().add((request, bytes, execution) -> { + HttpRequest wrapper = new HttpRequestWrapper(request); + long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT; + if (calculatedTs > mainTokenExpTs) { + synchronized (RestClient.this) { + if (calculatedTs > mainTokenExpTs) { + if (calculatedTs < refreshTokenExpTs) { + refreshToken(); + } else { + doLogin(); + } + } + } } - } - return response; + wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + mainToken); + return execution.execute(wrapper, bytes); + }); } public RestTemplate getRestTemplate() { @@ -219,7 +227,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public String getToken() { - return token; + return mainToken; } public String getRefreshToken() { @@ -229,22 +237,32 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { public void refreshToken() { Map refreshTokenRequest = new HashMap<>(); refreshTokenRequest.put("refreshToken", refreshToken); - ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class); - setTokenInfo(tokenInfo.getBody()); + long ts = System.currentTimeMillis(); + ResponseEntity tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class); + setTokenInfo(ts, tokenInfo.getBody()); } public void login(String username, String password) { + this.username = username; + this.password = password; + doLogin(); + } + + private void doLogin() { + long ts = System.currentTimeMillis(); Map loginRequest = new HashMap<>(); loginRequest.put("username", username); loginRequest.put("password", password); - ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class); - setTokenInfo(tokenInfo.getBody()); + ResponseEntity tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class); + setTokenInfo(ts, tokenInfo.getBody()); } - private void setTokenInfo(JsonNode tokenInfo) { - this.token = tokenInfo.get("token").asText(); + private synchronized void setTokenInfo(long ts, JsonNode tokenInfo) { + this.mainToken = tokenInfo.get("token").asText(); this.refreshToken = tokenInfo.get("refreshToken").asText(); - restTemplate.getInterceptors().add(this); + this.mainTokenExpTs = JWT.decode(this.mainToken).getExpiresAtAsInstant().toEpochMilli(); + this.refreshTokenExpTs = JWT.decode(refreshToken).getExpiresAtAsInstant().toEpochMilli(); + this.clientServerTimeDiff = JWT.decode(this.mainToken).getIssuedAtAsInstant().toEpochMilli() - ts; } public Optional getAdminSettings(String key) { @@ -3553,9 +3571,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @Override public void close() { - if (service != null) { - service.shutdown(); - } + service.shutdown(); } }