Graças à implementação da ESIA, organizações governamentais e comerciais, desenvolvedores e proprietários de serviços online puderam agilizar e tornar mais seguras as operações relacionadas à entrada e verificação de dados do usuário. O Rusfinance Bank decidiu também aproveitar o potencial do sistema e, ao finalizar o serviço de processamento de empréstimos online (o banco é especialista em financiamento de automóveis), implementou a integração com a plataforma.
Isso não foi tão fácil de fazer. Era necessário cumprir uma série de requisitos e procedimentos para resolver as dificuldades técnicas.
Neste artigo, tentaremos falar sobre os principais pontos e diretrizes metodológicas que são importantes saber para quem deseja implementar de forma independente a integração com o ESIA, bem como fornecer fragmentos de código em Java que ajudarão a superar as dificuldades durante o desenvolvimento (parte da implementação é omitida, mas a sequência geral de ações está claro).
Esperamos que nossa experiência ajude os desenvolvedores Java (e não só) a economizar muito tempo no desenvolvimento e familiarização com as recomendações metodológicas do Ministério de Telecomunicações e Comunicações de Massa.

Por que precisamos de integração com a ESIA?
Em conexão com a pandemia do coronavírus, o número de transações off-line em muitas áreas de empréstimo começou a diminuir. Os clientes começaram a “ficar online” e foi vital para nós reforçar a nossa presença online no mercado de crédito automóvel. No processo de finalização do serviço de Autocredit (já existe um artigo sobre o seu desenvolvimento no Habré ), decidimos tornar a interface de colocação de pedidos de crédito no site do banco o mais cómoda e simples possível. A integração com o ESIA tornou-se um momento fundamental para a resolução deste problema, pois permitiu obter automaticamente os dados pessoais do cliente.

Para o cliente, esta solução também revelou-se cómoda, uma vez que possibilitou a utilização de um único login e palavra-passe para efectuar o registo e aceder ao serviço de homologação online de pedidos de compra de automóvel a crédito.
Além disso, a integração com a ESIA permitiu que o Rusfinance Bank:
- reduzir o tempo de preenchimento de questionários online;
- reduzir o número de saltos do usuário ao tentar preencher um grande número de campos manualmente;
- fornecer um fluxo de clientes verificados com mais "qualidade".
Apesar de contarmos sobre a experiência do nosso banco, as informações podem ser úteis não só para as instituições financeiras. O governo recomenda o uso da plataforma ESIA para outros tipos de serviços online (mais detalhes aqui ).
O que fazer e como?
A princípio, pareceu-nos que não havia nada de especial na integração com o ESIA do ponto de vista técnico - uma tarefa padrão associada à obtenção de dados por meio da API REST. No entanto, após um exame mais detalhado, ficou claro que nem tudo é tão simples. Por exemplo, descobrimos que não temos ideia de como trabalhar com os certificados necessários para assinar vários parâmetros. Eu tive que perder tempo e descobrir isso. Mas as primeiras coisas primeiro.
Para começar, era importante traçar um plano de ação. Nosso plano incluiu as seguintes etapas principais:
- registrar-se no portal de tecnologia da ESIA;
- submeter pedidos de utilização de interfaces de software ESIA em ambiente de teste e industrial;
- desenvolver de forma independente um mecanismo para interação com a ESIA (de acordo com o documento atual "Recomendações metodológicas para o uso da ESIA");
- testar a operação do mecanismo no ambiente de teste e industrial da ESIA.
Normalmente desenvolvemos nossos projetos em Java. Portanto, para implementação de software, escolhemos:
- IntelliJ IDEA;
- CryptoPro JCP (ou CryptoPro Java CSP);
- Java 8;
- Apache HttpClient;
- Lombok;
- FasterXML / Jackson.
Obter o URL de redirecionamento
A primeira etapa é obter um código de autorização. Em nosso caso, isso é feito por um serviço separado com um redirecionamento para a página de autorização do portal de Serviços do Estado (falaremos sobre isso com mais detalhes).
Primeiro, inicializamos as variáveis ESIA_AUTH_URL (o endereço ESIA) e API_URL (o endereço para o qual o redirecionamento ocorre em caso de autorização bem-sucedida). Em seguida, criamos o objeto EsiaRequestParams, que contém os parâmetros da solicitação ao ESIA em seus campos, e forma o link esiaAuthUri.
public Response loginByEsia() throws Exception {
final String ESIA_AUTH_URL = dao.getEsiaAuthUrl(); //
final String API_URL = dao.getApiUrl(); // ,
EsiaRequestParams requestDto = new EsiaRequestParams(API_URL);
URI esiaAuthUri = new URIBuilder(ESIA_AUTH_URL)
.addParameters(Arrays.asList(
new BasicNameValuePair(RequestEnum.CLIENT_ID.getParam(), requestDto.getClientId()),
new BasicNameValuePair(RequestEnum.SCOPE.getParam(), requestDto.getScope()),
new BasicNameValuePair(RequestEnum.RESPONSE_TYPE.getParam(), requestDto.getResponseType()),
new BasicNameValuePair(RequestEnum.STATE.getParam(), requestDto.getState()),
new BasicNameValuePair(RequestEnum.TIMESTAMP.getParam(), requestDto.getTimestamp()),
new BasicNameValuePair(RequestEnum.ACCESS_TYPE.getParam(), requestDto.getAccessType()),
new BasicNameValuePair(RequestEnum.REDIRECT_URI.getParam(), requestDto.getRedirectUri()),
new BasicNameValuePair(RequestEnum.CLIENT_SECRET.getParam(), requestDto.getClientSecret())
))
.build();
return Response.temporaryRedirect(esiaAuthUri).build();
}
Para maior clareza, vamos mostrar como a classe EsiaRequestParams pode se parecer:
public class EsiaRequestParams {
String clientId;
String scope;
String responseType;
String state;
String timestamp;
String accessType;
String redirectUri;
String clientSecret;
String code;
String error;
String grantType;
String tokenType;
public EsiaRequestParams(String apiUrl) throws Exception {
this.clientId = CLIENT_ID;
this.scope = Arrays.stream(ScopeEnum.values())
.map(ScopeEnum::getName)
.collect(Collectors.joining(" "));
responseType = RESPONSE_TYPE;
state = EsiaUtil.getState();
timestamp = EsiaUtil.getUrlTimestamp();
accessType = ACCESS_TYPE;
redirectUri = apiUrl + RESOURCE_URL + "/" + AUTH_REQUEST_ESIA;
clientSecret = EsiaUtil.generateClientSecret(String.join("", scope, timestamp, clientId, state));
grantType = GRANT_TYPE;
tokenType = TOKEN_TYPE;
}
}
Depois disso, você precisa redirecionar o usuário para o serviço de autenticação ESIA. O usuário insere seu nome de usuário-senha, confirma o acesso aos dados para o nosso sistema. Em seguida, a ESIA envia uma resposta ao serviço online, que contém um código de autorização. Este código será necessário para consultas adicionais à ESIA.
Cada solicitação ao ESIA possui um parâmetro client_secret, que é uma assinatura eletrônica separada no formato PKCS7 (Public Key Cryptography Standard). No nosso caso, é usado um certificado para assinatura, que foi recebido pelo centro de certificação antes de iniciar o trabalho de integração com a ESIA. Como trabalhar com um armazenamento de chaves é bem descrito nesta série de artigos .
Como exemplo, vamos mostrar como o armazenamento de chaves fornecido pelo CryptoPro se parece com:

Neste caso, chamar as chaves privadas e públicas será parecido com este:
KeyStore keyStore = KeyStore.getInstance("HDImageStore"); //
keyStore.load(null, null);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(esiaKeyStoreParams.getName(), esiaKeyStoreParams.getValue().toCharArray()); //
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(esiaKeyStoreParams.getName()); // , – .
Onde JCP.HD_STORE_NAME é o nome do armazenamento no CryptoPro, esiaKeyStoreParams.getName () é o nome do contêiner e esiaKeyStoreParams.getValue (). ToCharArray () é a senha do contêiner.
Em nosso caso, não há necessidade de carregar dados no armazenamento usando o método load (), uma vez que as chaves já estarão lá ao especificar o nome deste armazenamento.
É importante lembrar aqui que a obtenção de uma assinatura no formulário
final Signature signature = Signature.getInstance(SIGN_ALGORITHM, PROVIDER_NAME);
signature.initSign(privateKey);
signature.update(data);
final byte[] sign = signature.sign();
não é suficiente para nós, uma vez que a ESIA exige uma assinatura destacada do formato PKCS7. Portanto, uma assinatura de formato PKCS7 deve ser gerada.
Um exemplo do nosso método que retorna uma assinatura destacada é assim:
public String generateClientSecret(String rawClientSecret) throws Exception {
if (this.localCertificate == null || this.esiaCertificate == null) throw new RuntimeException("Signature creation is unavailable");
return CMS.cmsSign(rawClientSecret.getBytes(), localPrivateKey, localCertificate, true);
}
Aqui, verificamos nossa chave pública e a chave pública ESIA. Como o método cmsSign () pode conter informações confidenciais, não iremos divulgá-las.
Aqui estão apenas alguns detalhes:
- rawClientSecret.getBytes () - array de bytes de escopo, carimbo de data / hora, clientId e estado;
- localPrivateKey - chave privada do contêiner;
- localCertificate - chave pública do contêiner;
- true - valor booleano do parâmetro de assinatura - checkout ou não.
Um exemplo de criação de assinatura pode ser encontrado na biblioteca Java CryptoPro, onde o padrão PKCS7 é chamado de CMS. E também no manual do programador, que acompanha o código-fonte da versão baixada do CryptoPro.
Obtendo um token
A próxima etapa é receber um token de acesso (também conhecido como token) em troca de um código de autorização que foi recebido como um parâmetro após a autorização bem-sucedida do usuário no portal de serviços de estado.
Para receber quaisquer dados no sistema de identificação unificado, você precisa obter um token de acesso. Para isso, solicitamos ao Sistema Unificado de identificação e autenticação. Os principais campos de solicitação aqui são formados da mesma maneira, o código se parece com o seguinte:
URI getTokenUri = new URIBuilder(ESIA_TOKEN_API_URL)
.addParameters(Arrays.asList(
new BasicNameValuePair(RequestEnum.CLIENT_ID.getParam(), requestDto.getClientId()),
new BasicNameValuePair(RequestEnum.CODE.getParam(), code),
new BasicNameValuePair(RequestEnum.GRANT_TYPE.getParam(), requestDto.getGrantType()),
new BasicNameValuePair(RequestEnum.CLIENT_SECRET.getParam(), requestDto.getClientSecret()),
new BasicNameValuePair(RequestEnum.STATE.getParam(), requestDto.getState()),
new BasicNameValuePair(RequestEnum.REDIRECT_URI.getParam(), requestDto.getRedirectUri()),
new BasicNameValuePair(RequestEnum.SCOPE.getParam(), requestDto.getScope()),
new BasicNameValuePair(RequestEnum.TIMESTAMP.getParam(), requestDto.getTimestamp()),
new BasicNameValuePair(RequestEnum.TOKEN_TYPE.getParam(), requestDto.getTokenType())
))
.build();
HttpUriRequest getTokenPostRequest = RequestBuilder.post()
.setUri(getTokenUri)
.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.build();
Depois de receber a resposta, analise-a e obtenha o token:
try (CloseableHttpResponse response = httpClient.execute(getTokenPostRequest)) {
HttpEntity tokenEntity = response.getEntity();
String tokenEntityString = EntityUtils.toString(tokenEntity);
tokenResponseDto = extractEsiaGetResponseTokenDto(tokenEntityString);
}
O token é uma string de três partes separada por pontos: HEADER.PAYLOAD.SIGNATURE, onde:
- HEADER é um cabeçalho que possui as propriedades de um token, incluindo um algoritmo de assinatura;
- PAYLOAD são informações sobre o token e o assunto, que solicitamos aos Serviços de Estado;
- A assinatura é a assinatura de HEADER.PAYLOAD.
Validação de token
Para ter certeza de que recebemos uma resposta dos Serviços do Estado, é necessário validar o token especificando o caminho para o certificado (chave pública), que pode ser baixado do site dos Serviços do Estado. Ao passar a string recebida (dados) e a assinatura (dataSignature) para o método isEsiaSignatureValid (), você pode obter o resultado da validação como um valor booleano.
public static boolean isEsiaSignatureValid(String data, String dataSignature) throws Exception {
InputStream inputStream = EsiaUtil.class.getClassLoader().getResourceAsStream(CERTIFICATE); // ,
CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); // X.509
X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(inputStream);
Signature signature = Signature.getInstance(certificate.getSigAlgName(), new JCP()); // Signature JCP
signature.initVerify(certificate.getPublicKey()); //
signature.update(data.getBytes()); // ,
return signature.verify(Base64.getUrlDecoder().decode(dataSignature));
}
De acordo com as diretrizes, é necessário verificar o período de validade do token. Caso o período de validade tenha expirado, será necessário criar um novo link com parâmetros adicionais e fazer uma solicitação usando o cliente http:
URI refreshTokenUri = new URIBuilder(ESIA_TOKEN_API_URL)
.addParameters(Arrays.asList(
new BasicNameValuePair(RequestEnum.CLIENT_ID.getParam(), requestDto.getClientId()),
new BasicNameValuePair(RequestEnum.REFRESH_TOKEN.getParam(), tokenResponseDto.getRefreshToken()),
new BasicNameValuePair(RequestEnum.CODE.getParam(), code),
new BasicNameValuePair(RequestEnum.GRANT_TYPE.getParam(), EsiaConstants.REFRESH_GRANT_TYPE),
new BasicNameValuePair(RequestEnum.STATE.getParam(), requestDto.getState()),
new BasicNameValuePair(RequestEnum.SCOPE.getParam(), requestDto.getScope()),
new BasicNameValuePair(RequestEnum.TIMESTAMP.getParam(), requestDto.getTimestamp()),
new BasicNameValuePair(RequestEnum.TOKEN_TYPE.getParam(), requestDto.getTokenType()),
new BasicNameValuePair(RequestEnum.CLIENT_SECRET.getParam(), requestDto.getClientSecret()),
new BasicNameValuePair(RequestEnum.REDIRECT_URI.getParam(), requestDto.getRedirectUri())
))
.build();
Recuperando dados do usuário
No nosso caso, você precisa obter seu nome completo, data de nascimento, dados do passaporte e contatos.
Usamos uma interface funcional que ajudará a receber os dados do usuário:
Function<String, String> esiaPersonDataFetcher = (fetchingUri) -> {
try {
URI getDataUri = new URIBuilder(fetchingUri).build();
HttpGet dataHttpGet = new HttpGet(getDataUri);
dataHttpGet.addHeader("Authorization", requestDto.getTokenType() + " " + tokenResponseDto.getAccessToken());
try (CloseableHttpResponse dataResponse = httpClient.execute(dataHttpGet)) {
HttpEntity dataEntity = dataResponse.getEntity();
return EntityUtils.toString(dataEntity);
}
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
};
Obtendo dados do usuário:
String personDataEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId);
Obter contatos não é mais tão óbvio quanto obter dados do usuário. Primeiro, você deve obter uma lista de links para contatos:
String contactsListEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId + "/ctts");
EsiaListDto esiaListDto = objectMapper.readValue(contactsListEntityString, EsiaListDto.class);
Desserialize essa lista e obtenha o objeto esiaListDto. Os campos do manual da ESIA podem ser diferentes, portanto, vale a pena verificar empiricamente.
Em seguida, você precisa seguir cada link da lista para obter cada contato do usuário. Isso parecerá assim:
for (String contactUrl : esiaListDto.getElementUrls()) {
String contactEntityString = esiaPersonDataFetcher.apply(contactUrl);
EsiaContactDto esiaContactDto = objectMapper.readValue(contactEntityString, EsiaContactDto.class);
}
A situação é a mesma com a obtenção de uma lista de documentos. Primeiro, obtemos uma lista de links para documentos:
String documentsListEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId + "/docs");
Em seguida, desserialize-o:
EsiaListDto esiaDocumentsListDto = objectMapper.readValue(documentsListEntityString, EsiaListDto.class);
:
for (String documentUrl : esiaDocumentsListDto.getElementUrls()) {
String documentEntityString = esiaPersonDataFetcher.apply(documentUrl);
EsiaDocumentDto esiaDocumentDto = objectMapper.readValue(documentEntityString, EsiaDocumentDto.class);
}
Agora o que fazer com todos esses dados?
Podemos analisar os dados e obter objetos com os campos obrigatórios. Aqui, cada desenvolvedor pode projetar classes conforme suas necessidades, de acordo com os termos de referência.
Um exemplo de obtenção de um objeto com os campos obrigatórios:
final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String personDataEntityString = esiaPersonDataFetcher
.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId);
EsiaPersonDto esiaPersonDto = objectMapper
.readValue(personDataEntityString, EsiaPersonDto.class);
Preenchemos o objeto esiaPersonDto com os dados necessários, por exemplo, contatos:
for (String contactUrl : esiaListDto.getElementUrls()) {
String contactEntityString = esiaPersonDataFetcher.apply(contactUrl);
EsiaContactDto esiaContactDto = objectMapper.readValue(contactEntityString, EsiaContactDto.class); //
if (esiaContactDto.getType() == null) continue;
switch (esiaContactDto.getType().toUpperCase()) {
case EsiaContactDto.MBT: // , mobilePhone
esiaPersonDto.setMobilePhone(esiaContactDto.getValue());
break;
case EsiaContactDto.EML: // , email
esiaPersonDto.setEmail(esiaContactDto.getValue());
}
}
A classe EsiaPersonDto se parece com isto:
@Data
@FieldNameConstants(prefix = "")
public class EsiaPersonDto {
private String firstName;
private String lastName;
private String middleName;
private String birthDate;
private String birthPlace;
private Boolean trusted; // - (“true”) / (“false”)
private String status; // - Registered () /Deleted ()
// , /prns/{oid}
private List<String> stateFacts;
private String citizenship;
private Long updatedOn;
private Boolean verifying;
@JsonProperty("rIdDoc")
private Integer documentId;
private Boolean containsUpCfmCode;
@JsonProperty("eTag")
private String tag;
// ----------------------------------------
private String mobilePhone;
private String email;
@javax.validation.constraints.Pattern(regexp = "(\\d{2})\\s(\\d{2})")
private String docSerial;
@javax.validation.constraints.Pattern(regexp = "(\\d{6})")
private String docNumber;
private String docIssueDate;
@javax.validation.constraints.Pattern(regexp = "([0-9]{3})\\-([0-9]{3})")
private String docDepartmentCode;
private String docDepartment;
@javax.validation.constraints.Pattern(regexp = "\\d{14}")
@JsonProperty("snils")
private String pensionFundCertificateNumber;
@javax.validation.constraints.Pattern(regexp = "\\d{12}")
@JsonProperty("inn")
private String taxPayerNumber;
@JsonIgnore
@javax.validation.constraints.Pattern(regexp = "\\d{2}")
private String taxPayerCertificateSeries;
@JsonIgnore
@javax.validation.constraints.Pattern(regexp = "\\d{10}")
private String taxPayerCertificateNumber;
}
O trabalho de melhoria do serviço vai continuar, porque a ESIA não pára.