Merge branch 'hotfix/1.9.9' into feature/SUPPORT-8822

This commit is contained in:
Eduard Tihomirov 2025-03-06 11:20:49 +03:00
commit b6d7f96ed0
138 changed files with 440 additions and 338 deletions

View file

@ -1,11 +1,14 @@
package ru.micord.ervu.security.esia;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.ExpiringState;
import ru.micord.ervu.security.esia.model.ExpiringToken;
@ -16,7 +19,7 @@ public class EsiaAuthInfoStore {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Map<String, ExpiringToken> ACCESS_TOKENS_MAP = new ConcurrentHashMap<>();
private static final Map<String, ExpiringToken> REFRESH_TOKENS_MAP = new ConcurrentHashMap<>();
private static final Map<String, ExpiringState> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
private static final Map<String, List<ExpiringState>> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
public static void addAccessToken(String prnOid, String token, long expiresIn) {
if (token != null) {
@ -79,13 +82,34 @@ public class EsiaAuthInfoStore {
REFRESH_TOKENS_MAP.remove(prnOid);
}
public static void addState(String prnsUUID, String state, long expiresIn) {
public static void addState(String prnsUUID, String state, long expiresIn, long attemptsCount) {
long expiryTime = System.currentTimeMillis() + expiresIn * 1000L;
PRNS_UUID_STATE_MAP.put(prnsUUID, new ExpiringState(state, expiryTime));
ExpiringState newState = new ExpiringState(state, expiryTime);
PRNS_UUID_STATE_MAP.compute(prnsUUID, (key, states) -> {
if (states == null) {
states = new CopyOnWriteArrayList<>();
}
if (states.size() >= attemptsCount) {
throw new EsiaException("The number of login attempts has been exceeded.");
}
states.add(newState);
return states;
});
}
public static String getState(String prnsUUID) {
return PRNS_UUID_STATE_MAP.get(prnsUUID).getState();
public static boolean containsState(String prnsUUID, String state) {
List<ExpiringState> states = PRNS_UUID_STATE_MAP.get(prnsUUID);
if (states == null) {
return false;
}
long currentTime = System.currentTimeMillis();
states.removeIf(expiringState -> expiringState.getExpiryTime() < currentTime);
for (ExpiringState expiringState : states) {
if (expiringState.getState().equals(state)) {
return true;
}
}
return false;
}
public static void removeState(String prnsUUID) {
@ -94,10 +118,10 @@ public class EsiaAuthInfoStore {
public static void removeExpiredState() {
for (String key : PRNS_UUID_STATE_MAP.keySet()) {
ExpiringState state = PRNS_UUID_STATE_MAP.get(key);
if (state != null && state.isExpired()) {
PRNS_UUID_STATE_MAP.remove(key);
}
PRNS_UUID_STATE_MAP.computeIfPresent(key, (k, states) -> {
states.removeIf(ExpiringState::isExpired);
return states.isEmpty() ? null : states;
});
}
}
}
}

View file

@ -59,6 +59,9 @@ public class EsiaConfig {
@Value("${esia.state.cookie.life.time:300}")
private long esiaStateCookieLifeTime;
@Value("${esia.login.attempts.count:5}")
private long esiaLoginAttemptsCount;
public String getEsiaScopes() {
String[] scopeItems = esiaScopes.split(",");
@ -123,4 +126,8 @@ public class EsiaConfig {
public long getEsiaStateCookieLifeTime() {
return esiaStateCookieLifeTime;
}
public long getEsiaLoginAttemptsCount() {
return esiaLoginAttemptsCount;
}
}

View file

@ -32,8 +32,8 @@ public class EsiaController {
private JwtTokenService jwtTokenService;
@RequestMapping(value = "/esia/url")
public String getEsiaUrl(HttpServletResponse response) {
return esiaAuthService.generateAuthCodeUrl(response);
public String getEsiaUrl(HttpServletResponse response, HttpServletRequest request) {
return esiaAuthService.generateAuthCodeUrl(response, request);
}
@GetMapping(value = "/esia/auth")

View file

@ -14,6 +14,7 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
@ -30,9 +31,9 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.util.WebUtils;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.service.AuditService;
import org.springframework.web.util.WebUtils;
import ru.micord.ervu.kafka.model.Document;
import ru.micord.ervu.kafka.model.Person;
import ru.micord.ervu.kafka.model.Response;
@ -45,7 +46,6 @@ import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.PersonModel;
import ru.micord.ervu.security.esia.model.SignResponse;
import ru.micord.ervu.security.esia.EsiaAuthInfoStore;
import ru.micord.ervu.security.esia.model.SignResponse;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
@ -59,9 +59,6 @@ import ru.micord.ervu.security.webbpm.jwt.model.Token;
import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils;
import static ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil.getCurrentUserEsiaId;
import ru.cg.webbpm.modules.core.runtime.api.MessageBundleUtils;
/**
* @author Eduard Tihomirov
*/
@ -99,13 +96,17 @@ public class EsiaAuthService {
@Value("${ervu.kafka.request.topic}")
private String requestTopic;
public String generateAuthCodeUrl(HttpServletResponse response) {
public String generateAuthCodeUrl(HttpServletResponse response, HttpServletRequest request) {
try {
String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now();
String timestamp = dt.format(formatter);
String prnsUUID = UUID.randomUUID().toString();
Cookie oldPrnsCookie = WebUtils.getCookie(request, PRNS_UUID);
if (oldPrnsCookie != null) {
prnsUUID = oldPrnsCookie.getValue();
}
String redirectUrl = esiaConfig.getRedirectUrl();
String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A")
.replaceAll("/", "%2F");
@ -121,7 +122,7 @@ public class EsiaAuthService {
SignResponse signResponse = signMap(parameters);
String state = signResponse.getState();
String clientSecret = signResponse.getSignature();
EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime());
EsiaAuthInfoStore.addState(prnsUUID, state, esiaConfig.getEsiaStateCookieLifeTime(), esiaConfig.getEsiaLoginAttemptsCount());
ResponseCookie prnsCookie = securityHelper.createCookie(PRNS_UUID, prnsUUID, "/")
.maxAge(esiaConfig.getEsiaStateCookieLifeTime())
.build();
@ -334,6 +335,9 @@ public class EsiaAuthService {
tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty";
throw new IllegalStateException("Esia response error. " + errMsg);
}
if (!tokenResponse.getState().equals(state)) {
throw new EsiaException("Token invalid. State from request not equals with state from response.");
}
String esiaAccessTokenStr = tokenResponse.getAccessToken();
String verifyResult = verifyToken(esiaAccessTokenStr);
if (verifyResult != null) {
@ -479,8 +483,11 @@ public class EsiaAuthService {
ZoneId.systemDefault()
);
LocalDateTime currentTime = LocalDateTime.now();
if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) {
return "Token invalid. Token expired";
if (currentTime.getNano() > 0) {
currentTime = currentTime.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
}
if (currentTime.isBefore(iatTime) || expTime.isBefore(iatTime) || currentTime.isAfter(expTime)) {
return "Token invalid. Token expired, current: " + currentTime + " iat: " + iatTime + " exp: " + expTime;
}
HttpResponse<String> response = signVerify(accessToken);
if (response.statusCode() != 200) {
@ -516,8 +523,7 @@ public class EsiaAuthService {
return "State invalid. Cookie not found";
}
String prnsUUID = cookie.getValue();
String oldState = EsiaAuthInfoStore.getState(prnsUUID);
if (oldState == null || !oldState.equals(state)) {
if (!EsiaAuthInfoStore.containsState(prnsUUID, state)) {
return "State invalid. State from ESIA not equals with state before";
}
EsiaAuthInfoStore.removeState(prnsUUID);

View file

@ -1 +1 @@
error.unknown=The system is temporarily unavailable. Please try again later.
error.unknown=The system is temporarily unavailable. Please try again later.