REST template concurrency

This commit is contained in:
Andrii Shvaika 2023-03-17 13:50:49 +02:00
parent 444c05a76c
commit 1960d99504
4 changed files with 66 additions and 40 deletions

View File

@ -36,10 +36,10 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
throw new DataValidationException("JWT token issuer should be specified!"); 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!"); 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!"); throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
} }
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {

View File

@ -67,6 +67,7 @@
<jackson.version>2.13.4</jackson.version> <jackson.version>2.13.4</jackson.version>
<jackson-databind.version>2.13.4.2</jackson-databind.version> <jackson-databind.version>2.13.4.2</jackson-databind.version>
<fasterxml-classmate.version>1.3.4</fasterxml-classmate.version> <fasterxml-classmate.version>1.3.4</fasterxml-classmate.version>
<auth0-jwt.version>4.2.1</auth0-jwt.version>
<json-schema-validator.version>2.2.6</json-schema-validator.version> <json-schema-validator.version>2.2.6</json-schema-validator.version>
<californium.version>3.0.0</californium.version> <californium.version>3.0.0</californium.version>
<leshan.version>2.0.0-M5</leshan.version> <leshan.version>2.0.0-M5</leshan.version>
@ -1429,6 +1430,11 @@
<artifactId>classmate</artifactId> <artifactId>classmate</artifactId>
<version>${fasterxml-classmate.version}</version> <version>${fasterxml-classmate.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${auth0-jwt.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.fge</groupId> <groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId> <artifactId>json-schema-validator</artifactId>

View File

@ -47,6 +47,10 @@
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId> <artifactId>util</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.rest.client; package org.thingsboard.rest.client;
import com.auth0.jwt.JWT;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; 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.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; 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.http.client.support.HttpRequestWrapper;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; 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 org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -171,6 +168,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isEmpty; 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 * @author Andrew Shvayka
*/ */
public class RestClient implements ClientHttpRequestInterceptor, Closeable { public class RestClient implements Closeable {
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
protected final RestTemplate restTemplate; private static final long AVG_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
protected final String baseURL;
private String token;
private String refreshToken;
private final ObjectMapper objectMapper = new ObjectMapper();
private ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass());
protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; 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) { public RestClient(String baseURL) {
this(new RestTemplate(), baseURL); this(new RestTemplate(), baseURL);
} }
public RestClient(RestTemplate restTemplate, String baseURL) { public RestClient(RestTemplate restTemplate, String baseURL) {
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory());
this.baseURL = baseURL; this.baseURL = baseURL;
} this.restTemplate.getInterceptors().add((request, bytes, execution) -> {
HttpRequest wrapper = new HttpRequestWrapper(request);
@Override long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT;
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { if (calculatedTs > mainTokenExpTs) {
HttpRequest wrapper = new HttpRequestWrapper(request); synchronized (RestClient.this) {
wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); if (calculatedTs > mainTokenExpTs) {
ClientHttpResponse response = execution.execute(wrapper, bytes); if (calculatedTs < refreshTokenExpTs) {
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { refreshToken();
synchronized (this) { } else {
restTemplate.getInterceptors().remove(this); doLogin();
refreshToken(); }
wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); }
return execution.execute(wrapper, bytes); }
} }
} wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + mainToken);
return response; return execution.execute(wrapper, bytes);
});
} }
public RestTemplate getRestTemplate() { public RestTemplate getRestTemplate() {
@ -219,7 +227,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
} }
public String getToken() { public String getToken() {
return token; return mainToken;
} }
public String getRefreshToken() { public String getRefreshToken() {
@ -229,22 +237,32 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public void refreshToken() { public void refreshToken() {
Map<String, String> refreshTokenRequest = new HashMap<>(); Map<String, String> refreshTokenRequest = new HashMap<>();
refreshTokenRequest.put("refreshToken", refreshToken); refreshTokenRequest.put("refreshToken", refreshToken);
ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class); long ts = System.currentTimeMillis();
setTokenInfo(tokenInfo.getBody()); ResponseEntity<JsonNode> tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class);
setTokenInfo(ts, tokenInfo.getBody());
} }
public void login(String username, String password) { public void login(String username, String password) {
this.username = username;
this.password = password;
doLogin();
}
private void doLogin() {
long ts = System.currentTimeMillis();
Map<String, String> loginRequest = new HashMap<>(); Map<String, String> loginRequest = new HashMap<>();
loginRequest.put("username", username); loginRequest.put("username", username);
loginRequest.put("password", password); loginRequest.put("password", password);
ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class); ResponseEntity<JsonNode> tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
setTokenInfo(tokenInfo.getBody()); setTokenInfo(ts, tokenInfo.getBody());
} }
private void setTokenInfo(JsonNode tokenInfo) { private synchronized void setTokenInfo(long ts, JsonNode tokenInfo) {
this.token = tokenInfo.get("token").asText(); this.mainToken = tokenInfo.get("token").asText();
this.refreshToken = tokenInfo.get("refreshToken").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<AdminSettings> getAdminSettings(String key) { public Optional<AdminSettings> getAdminSettings(String key) {
@ -3553,9 +3571,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
@Override @Override
public void close() { public void close() {
if (service != null) { service.shutdown();
service.shutdown();
}
} }
} }