Merge branch 'hotfix/1.9.7' of 10.10.31.70:/ervu-lkrp-ul into hotfix/1.9.7

This commit is contained in:
Zaripov Emil 2025-01-21 11:46:45 +03:00
commit 25c0c15773
12 changed files with 249 additions and 62 deletions

View file

@ -1,14 +1,17 @@
package ervu.client.okopf;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.TimeoutException;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.zip.ZipInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
@ -19,37 +22,27 @@ import org.springframework.stereotype.Component;
*/
@Component
public class EsnsiOkopfClient {
private static final Logger logger = LoggerFactory.getLogger(EsnsiOkopfClient.class);
@Value("${esnsi.okopf.url}")
private String uri;
private String url;
@Retryable(value = {TimeoutException.class}, backoff =
@Backoff(delay = 2000))
@Retryable(value = IOException.class, maxAttemptsExpression = "${esnsi.okopf.retry.max.attempts.load:3}", backoff =
@Backoff(delayExpression = "${esnsi.okop.retry.delay.load:30000}"))
public String getJsonOkopFormData() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
try {
HttpResponse<InputStream> response = client.send(request,
HttpResponse.BodyHandlers.ofInputStream()
);
if (response.statusCode() >= 200 && response.statusCode() <= 202) {
return unzipFile(new ZipInputStream(response.body()));
try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
ZipInputStream archiveStream = new ZipInputStream(in);
BufferedReader br = new BufferedReader(
new InputStreamReader(archiveStream, StandardCharsets.UTF_8))) {
if (Objects.nonNull(archiveStream.getNextEntry())) {
logger.info("Received an archive in response.");
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
throw new RuntimeException("The returned status " + response.statusCode() + " is incorrect. Json file has not be unzip");
logger.info("Received an empty archive in response. Skipping load okpof file process");
}
catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
catch (SecurityException | IOException e) {
logger.error("Failed to send HTTP request {} or process the response for okopf file.", url, e);
}
}
private String unzipFile(ZipInputStream zis) throws IOException {
if (zis.getNextEntry() != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(zis.readAllBytes())));
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
throw new RuntimeException("ZipInputStream is empty and has not been unzipped");
return null;
}
}

View file

@ -10,7 +10,7 @@ import ervu.model.okopf.OkopfModel;
* @author Artyom Hackimullin
*/
public interface OkopfDao {
void save(List<OkopfModel> recordModels);
void saveOrUpdate(List<OkopfModel> recordModels);
String fetchTitleByLeg(String leg);
}

View file

@ -1,11 +1,15 @@
package ervu.dao.okopf;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import ervu.model.okopf.OkopfModel;
import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.records.OkopfRecordsRecord;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import static ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.Tables.OKOPF_RECORDS;
@ -20,18 +24,13 @@ public class OkopfDaoImpl implements OkopfDao {
private DSLContext dsl;
@Override
public void save(List<OkopfModel> recordModels) {
var queries = recordModels.stream().map(record ->
dsl.insertInto(OKOPF_RECORDS, OKOPF_RECORDS.OKOPF_RECORDS_ID, OKOPF_RECORDS.NAME, OKOPF_RECORDS.VERSION)
.values(record.getCode(), record.getName(), record.getVersion())
.onConflict(OKOPF_RECORDS.OKOPF_RECORDS_ID)
.doUpdate()
.set(OKOPF_RECORDS.NAME, record.getName())
.set(OKOPF_RECORDS.VERSION, record.getVersion())
.where(OKOPF_RECORDS.OKOPF_RECORDS_ID.eq(record.getCode()))
).toList();
dsl.batch(queries).execute();
@Transactional
public void saveOrUpdate(List<OkopfModel> okopfModels) {
deleteNotActualOkopfRecords(okopfModels);
dsl.batchMerge(okopfModels.stream()
.map(this::mapOkopfModelToRecord)
.toList())
.execute();
}
@Override
@ -41,4 +40,23 @@ public class OkopfDaoImpl implements OkopfDao {
.where(OKOPF_RECORDS.OKOPF_RECORDS_ID.eq(leg))
.fetchOne(OKOPF_RECORDS.NAME);
}
private void deleteNotActualOkopfRecords(List<OkopfModel> recordModels) {
Set<String> ids = recordModels
.stream()
.map(OkopfModel::getCode)
.collect(Collectors.toSet());
dsl.deleteFrom(OKOPF_RECORDS)
.where(OKOPF_RECORDS.OKOPF_RECORDS_ID.notIn(ids))
.execute();
}
private OkopfRecordsRecord mapOkopfModelToRecord(OkopfModel model) {
OkopfRecordsRecord record = dsl.newRecord(OKOPF_RECORDS);
record.setValue(OKOPF_RECORDS.OKOPF_RECORDS_ID, model.getCode());
record.setValue(OKOPF_RECORDS.NAME, model.getName());
record.setValue(OKOPF_RECORDS.VERSION, model.getVersion());
return record;
}
}

View file

@ -5,7 +5,6 @@ import java.util.List;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.client.okopf.EsnsiOkopfClient;
import ervu.dao.okopf.OkopfDao;
@ -24,6 +23,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.scheduling.config.ScheduledTaskRegistrar.CRON_DISABLED;
import static org.springframework.util.StringUtils.hasText;
/**
@ -48,11 +48,11 @@ public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerServic
@Transactional
public void init() {
if (!cronLoad.equals(CRON_DISABLED)) {
logger.info("Synchronization with OKOPF enabled");
logger.info("Synchronization with esnsi okopf enabled");
load();
}
else {
logger.info("Synchronization with OKOPF disabled");
logger.info("Synchronization with esnsi okopf disabled");
}
}
@ -60,16 +60,22 @@ public class EsnsiOkopfSchedulerServiceImpl implements EsnsiOkopfSchedulerServic
@SchedulerLock(name = "loadOkopf")
@Transactional
public void load() {
logger.info("Loading okopf file");
try {
logger.info("Loading okopf file");
String data = esnsiOkopfClient.getJsonOkopFormData();
OkopfOrgModel orgModel = mapper.readValue(data, OkopfOrgModel.class);
int currentVersion = mapper.readTree(data).findValue("version").asInt();
List<OkopfModel> okopfRecords = mapToOkopfRecords(orgModel.getData(), currentVersion);
okopfDao.save(okopfRecords);
if (hasText(data)) {
logger.info("Parsing from json file to okopf model");
OkopfOrgModel orgModel = mapper.readValue(data, OkopfOrgModel.class);
int currentVersion = mapper.readTree(data).findValue("version").asInt();
List<OkopfModel> okopfRecords = mapToOkopfRecords(orgModel.getData(), currentVersion);
logger.info("Finished parsing from json file to okopf model");
logger.info("Loaded {} okopf records with version {}. Beginning to save okopf data", okopfRecords.size(), currentVersion);
okopfDao.saveOrUpdate(okopfRecords);
logger.info("Successfully saved okopf data");
}
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
catch (Exception e) {
logger.error("Failed to load okopf data", e);
}
}

View file

@ -56,6 +56,12 @@ public class EsiaConfig {
@Value("${esia.upload.data.role}")
private String esiaUploadDataRole;
@Value("${sign.verify.url}")
private String signVerifyUrl;
@Value("${esia.issuer.url}")
private String esiaIssuerUrl;
public String getEsiaOrgScopes() {
String[] scopeItems = esiaOrgScopes.split(",");
return String.join(" ", Arrays.stream(scopeItems).map(item -> orgScopeUrl + item.trim()).toArray(String[]::new));
@ -108,7 +114,15 @@ public class EsiaConfig {
return logoutRedirectUrl;
}
public String getSignVerifyUrl() {
return signVerifyUrl;
}
public String getEsiaUploadDataRole() {
return esiaUploadDataRole;
}
public String getEsiaIssuerUrl() {
return esiaIssuerUrl;
}
}

View file

@ -0,0 +1,55 @@
package ru.micord.ervu.security.esia.model;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Eduard Tihomirov
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class EsiaHeader implements Serializable {
private static final long serialVersionUID = 1L;
private String alg;
private String ver;
private String sbt;
private String typ;
public String getAlg() {
return alg;
}
public void setAlg(String alg) {
this.alg = alg;
}
public String getVer() {
return ver;
}
public void setVer(String ver) {
this.ver = ver;
}
public String getSbt() {
return sbt;
}
public void setSbt(String sbt) {
this.sbt = sbt;
}
public String getTyp() {
return typ;
}
public void setTyp(String typ) {
this.typ = typ;
}
}

View file

@ -23,6 +23,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.service.okopf.OkopfService;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaHeader;
import ru.micord.ervu.security.esia.model.EsiaTokenResponse;
import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.esia.token.EsiaTokensStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,11 +47,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaTokenResponse;
import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
@ -219,6 +220,10 @@ public class EsiaAuthService {
}
esiaAccessTokenStr = tokenResponse.getAccess_token();
String esiaRefreshTokenStr = tokenResponse.getRefresh_token();
String verifyResult = verifyToken(esiaAccessTokenStr);
if (verifyResult != null) {
throw new EsiaException(verifyResult);
}
EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
prnOid = esiaAccessToken.getSbj_id();
expiresIn = tokenResponse.getExpires_in();
@ -308,6 +313,10 @@ public class EsiaAuthService {
}
String esiaAccessTokenStr = tokenResponse.getAccess_token();
String esiaNewRefreshToken = tokenResponse.getRefresh_token();
String verifyResult = verifyToken(esiaAccessTokenStr);
if (verifyResult != null) {
throw new EsiaException(verifyResult);
}
EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
String prnOid = esiaAccessToken.getSbj_id();
Long expiresIn = tokenResponse.getExpires_in();
@ -470,4 +479,57 @@ public class EsiaAuthService {
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private String verifyToken(String accessToken) {
EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken);
EsiaHeader esiaHeader = ulDataService.readHeader(accessToken);
if (!esiaHeader.getSbt().equals("access")) {
return "Token invalid. Token sbt: " + esiaHeader.getSbt() + " invalid";
}
if (!esiaHeader.getTyp().equals("JWT")) {
return "Token invalid. Token type: " + esiaHeader.getTyp() + " invalid";
}
if (!esiaAccessToken.getClient_id().equals(esiaConfig.getClientId())) {
return "Token invalid. Token clientId: " + esiaAccessToken.getClient_id() + " invalid";
}
if (!esiaAccessToken.getIss().equals(esiaConfig.getEsiaIssuerUrl())) {
return "Token invalid. Token issuer:" + esiaAccessToken.getIss() + " invalid";
}
//TODO SUPPORT-8750
// LocalDateTime iatTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getIat()),
// ZoneId.systemDefault()
// );
// LocalDateTime expTime = LocalDateTime.ofInstant(Instant.ofEpochSecond(esiaAccessToken.getExp()),
// ZoneId.systemDefault()
// );
// LocalDateTime currentTime = LocalDateTime.now();
// if (!currentTime.isAfter(iatTime) || !expTime.isAfter(iatTime)) {
// return "Token invalid. Token expired";
// }
HttpResponse<String> response = signVerify(accessToken);
if (response.statusCode() != 200) {
if (response.statusCode() == 401) {
return "Token invalid. " + response.body();
}
return "Error in verify module. Error status " + response.statusCode();
}
return null;
}
private HttpResponse<String> signVerify(String accessToken) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(esiaConfig.getSignVerifyUrl()))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString(accessToken, StandardCharsets.UTF_8))
.build();
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
.build()
.send(request, HttpResponse.BodyHandlers.ofString());
}
catch (Exception e) {
throw new EsiaException(e);
}
}
}

View file

@ -2,6 +2,7 @@ package ru.micord.ervu.security.esia.service;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaHeader;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.esia.model.PersonModel;
@ -23,4 +24,6 @@ public interface UlDataService {
EsiaAccessToken readToken(String accessToken);
String getAllUserRoles(String accessToken);
EsiaHeader readHeader(String accessToken);
}

View file

@ -15,7 +15,13 @@ import ru.micord.ervu.security.esia.config.EsiaConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.esia.model.*;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.BrhsModel;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaHeader;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.esia.model.PersonModel;
/**
* @author Eduard Tihomirov
@ -146,6 +152,25 @@ public class UlDataServiceImpl implements UlDataService {
}
}
@Override
public EsiaHeader readHeader(String accessToken) {
try {
byte[] decodedBytes = Base64.getDecoder()
.decode(
accessToken.substring(0, accessToken.indexOf('.'))
.replace('-', '+')
.replace('_', '/'));
String decodedString = new String(decodedBytes);
EsiaHeader esiaHeader = objectMapper.readValue(decodedString,
EsiaHeader.class
);
return esiaHeader;
}
catch (Exception e) {
throw new EsiaException(e);
}
}
private OrganizationModel getOrgModel(String orgOid, String accessToken) {
try {
String url = esiaConfig.getEsiaBaseUri() + "rs/orgs/" + orgOid;

View file

@ -783,7 +783,12 @@ JBPM использует 3 корневых категории логирова
#### Взаимодействие с ЕСНСИ в части получения справочника ОКОПФ
- `ESNSI_OKOPF_URL` - url который обращается к еснси для получения справочника и скачивает данные спровочников организации в виде заархивированного json файла.
- `ESNSI_OKOPF_CRON_LOAD` - настройка, которая указывет расписание для загрузки справочника окопф и сохранение данных по справкам в БД
- `ESNSI_OKOPF_CRON_LOAD` - настройка, которая указывет расписание для загрузки справочника окопф и
сохранение данных по справкам в БД
- `ESNSI_OKOPF_RETRY_DELAY_LOAD` - настройка, которая указывет на повторную попытку загрузить
справочник окопф с задержкой
- `ESNSI_OKOPF_RETRY_MAX_ATTEMPTS_LOAD` - настройка, которая указывет на максимальное кол-во попыток
повторно загрузить справочник окопф
#### Взаимодействие с WebDav

View file

@ -41,6 +41,10 @@ ERVU_KAFKA_USERNAME=user1
ERVU_KAFKA_PASSWORD=Blfi9d2OFG
ERVU_KAFKA_EXCERPT_REPLY_TOPIC=ervu.lkrp.excerpt.response
ERVU_KAFKA_EXCERPT_REQUEST_TOPIC=ervu.lkrp.excerpt.request
ESNSI_OKOPF_URL=https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/16271/file?extension=JSON&encoding=UTF_8
ESNSI_OKOPF_CRON_LOAD=0 0 */1 * * *
ESNSI_OKOPF_RETRY_MAX_ATTEMPTS_LOAD=3
ESNSI_OKOPF_RETRY_DELAY_LOAD=30000
ERVU_FILE_UPLOAD_MAX_FILE_SIZE=5242880
ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456

View file

@ -83,7 +83,9 @@
<property name="ervu.kafka.username" value="user1"/>
<property name="ervu.kafka.password" value="Blfi9d2OFG"/>
<property name="esnsi.okopf.cron.load" value="0 0 */1 * * *"/>
<property name="esnsi.okopf.url" value="https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/11465/file?extension=JSON&amp;encoding=UTF_8"/>
<property name="esnsi.okopf.url" value="https://esnsi.gosuslugi.ru/rest/ext/v1/classifiers/16271/file?extension=JSON&amp;encoding=UTF_8"/>
<property name="esnsi.okop.retry.delay.load" value="30000"/>
<property name="esnsi.okopf.retry.max.attempts.load" value="3"/>
<property name="ervu.kafka.journal.request.topic" value="ervu.organization.journal.request"/>
<property name="ervu.kafka.journal.reply.topic" value="ervu.organization.journal.response"/>
<property name="db.journal.excluded.statuses" value="Направлено в ЕРВУ,Получен ЕРВУ"/>