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())) {
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()) {

View File

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

View File

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

View File

@ -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<String, String> refreshTokenRequest = new HashMap<>();
refreshTokenRequest.put("refreshToken", refreshToken);
ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class);
setTokenInfo(tokenInfo.getBody());
long ts = System.currentTimeMillis();
ResponseEntity<JsonNode> 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<String, String> loginRequest = new HashMap<>();
loginRequest.put("username", username);
loginRequest.put("password", password);
ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
setTokenInfo(tokenInfo.getBody());
ResponseEntity<JsonNode> 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<AdminSettings> getAdminSettings(String key) {
@ -3553,9 +3571,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
@Override
public void close() {
if (service != null) {
service.shutdown();
}
service.shutdown();
}
}