So implementieren Sie die Integration mit ESIA in Java ohne unnötige Probleme

Die Hauptmethode zur Identifizierung der Bürger war lange Zeit ein gewöhnlicher Reisepass. Die Situation änderte sich, als im Jahr 2011 auf Anordnung des Ministeriums für Telekommunikation und Massenkommunikation das einheitliche Identifikations- und Authentifizierungssystem (ESIA) eingeführt wurde, das es ermöglichte, die Identität einer Person zu erkennen und online Daten darüber zu erhalten.



Dank der Implementierung der ESIA konnten Regierungs- und Handelsorganisationen, Entwickler und Eigentümer von Onlinediensten die Eingabe und Überprüfung von Benutzerdaten beschleunigen und sicherer durchführen. Die Rusfinance Bank entschied sich auch, das Potenzial des Systems zu nutzen, und implementierte bei Abschluss des Online-Kreditdienstes (die Bank ist auf Autokredite spezialisiert) die Integration in die Plattform.



Das war nicht so einfach. Es war notwendig, eine Reihe von Anforderungen und Verfahren zu erfüllen, um technische Schwierigkeiten zu lösen.



In diesem Artikel werden wir versuchen, Sie über die wichtigsten Punkte und methodischen Richtlinien zu informieren, die für diejenigen wichtig sind, die die Integration mit ESIA unabhängig implementieren möchten, sowie Codefragmente in Java bereitzustellen, die helfen, Schwierigkeiten während der Entwicklung zu überwinden (ein Teil der Implementierung wird weggelassen, aber die allgemeine Abfolge der Aktionen ist klar).



Wir hoffen, dass unsere Erfahrung Java-Entwicklern (und nicht nur) dabei hilft, viel Zeit zu sparen, wenn sie sich entwickeln und mit den methodischen Empfehlungen des Ministeriums für Telekommunikation und Massenkommunikation vertraut machen.







Warum brauchen wir die Integration mit ESIA?



Im Zusammenhang mit der Coronavirus-Pandemie begann die Anzahl der Offline-Transaktionen in vielen Bereichen der Kreditvergabe zu sinken. Die Kunden begannen, "online zu gehen", und es war für uns von entscheidender Bedeutung, unsere Online-Präsenz auf dem Autokreditmarkt zu stärken. Bei der Fertigstellung des Autocredit-Dienstes (Habré hat bereits einen Artikel über seine Entwicklung veröffentlicht ) haben wir beschlossen, die Schnittstelle für die Platzierung von Kreditanträgen auf der Website der Bank so bequem und einfach wie möglich zu gestalten. Die Integration in ESIA ist zu einem Schlüsselmoment bei der Lösung dieses Problems geworden, da es möglich war, die persönlichen Daten des Kunden automatisch abzurufen.







Für den Kunden erwies sich diese Lösung auch als praktisch, da es möglich war, sich mit einem einzigen Login und Passwort zu registrieren und den Online-Genehmigungsdienst für Anträge auf Kauf eines Autos auf Kredit einzugeben.



Darüber hinaus ermöglichte die Integration mit ESIA der Rusfinance Bank:



  • Verkürzen Sie die Zeit zum Ausfüllen von Online-Fragebögen.
  • Reduzieren Sie die Anzahl der Benutzersprünge, wenn Sie versuchen, eine große Anzahl von Feldern manuell auszufüllen.
  • einen Strom von "Qualität", verifizierten Kunden bereitzustellen.


Trotz der Tatsache, dass wir über die Erfahrungen unserer Bank sprechen, können die Informationen nicht nur für Finanzinstitute nützlich sein. Die Regierung empfiehlt, die ESIA-Plattform für andere Arten von Online-Diensten zu verwenden (weitere Einzelheiten hier ).



Was tun und wie?



Zunächst schien es uns, dass die Integration in die ESIA aus technischer Sicht nichts Besonderes war - eine Standardaufgabe, die mit dem Abrufen von Daten über die REST-API verbunden ist. Bei näherer Betrachtung wurde jedoch klar, dass nicht alles so einfach ist. Es stellte sich beispielsweise heraus, dass wir keine Ahnung haben, wie wir mit den Zertifikaten arbeiten sollen, die zum Signieren mehrerer Parameter erforderlich sind. Ich musste Zeit verschwenden und es herausfinden. Aber das Wichtigste zuerst.



Zunächst war es wichtig, einen Aktionsplan zu skizzieren. Unser Plan umfasste die folgenden Hauptschritte:



  1. Registrierung im ESIA-Technologieportal;
  2. Anträge auf Verwendung der ESIA-Softwareschnittstellen in einer Test- und Industrieumgebung einreichen;
  3. unabhängig einen Mechanismus für die Interaktion mit der ESIA entwickeln (gemäß dem aktuellen Dokument "Methodische Empfehlungen für die Verwendung der ESIA");
  4. Testen Sie die Funktionsweise des Mechanismus im Test- und Industrieumfeld der ESIA.


Wir entwickeln unsere Projekte normalerweise in Java. Daher haben wir für die Software-Implementierung Folgendes ausgewählt:



  • IntelliJ IDEA;
  • CryptoPro JCP (oder CryptoPro Java CSP);
  • Java 8;
  • Apache HttpClient;
  • Lombok;
  • FasterXML / Jackson.


Abrufen der Weiterleitungs-URL



Der erste Schritt besteht darin, einen Autorisierungscode zu erhalten. In unserem Fall erfolgt dies durch einen separaten Dienst mit einer Weiterleitung zur Autorisierungsseite des State Services-Portals (wir werden Sie darüber ausführlicher informieren).



Zunächst initialisieren wir die Variablen ESIA_AUTH_URL (die ESIA-Adresse) und API_URL (die Adresse, an die die Umleitung bei erfolgreicher Autorisierung erfolgt). Danach erstellen wir das EsiaRequestParams-Objekt, das die Parameter der Anforderung an die ESIA in ihren Feldern enthält, und bilden den esiaAuthUri-Link.



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();
}
      
      





Lassen Sie uns zur Verdeutlichung zeigen, wie die EsiaRequestParams-Klasse aussehen könnte:



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;
  }
}
      
      





Danach müssen Sie den Benutzer zum ESIA-Authentifizierungsdienst umleiten. Der Benutzer gibt sein Benutzername-Passwort ein und bestätigt den Zugriff auf Daten für unser System. Anschließend sendet ESIA eine Antwort an den Onlinedienst, die einen Autorisierungscode enthält. Dieser Code wird für weitere Anfragen an die ESIA benötigt.



Jede Anforderung an die ESIA verfügt über einen client_secret-Parameter, bei dem es sich um eine getrennte elektronische Signatur im PKCS7-Format (Public Key Cryptography Standard) handelt. In unserem Fall wird ein Zertifikat zum Signieren verwendet, das beim Zertifizierungszentrum eingegangen ist, bevor mit den Arbeiten zur Integration in die ESIA begonnen wurde. Die Arbeit mit einem Schlüsselspeicher wird in dieser Artikelserie ausführlich beschrieben .



Lassen Sie uns als Beispiel zeigen, wie der von CryptoPro bereitgestellte Schlüsselspeicher aussieht:







In diesem Fall sieht der Aufruf des privaten und des öffentlichen Schlüssels folgendermaßen aus:



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()); //  ,   –  .
      
      





Während JCP.HD_STORE_NAME der Speichername in CryptoPro ist, ist esiaKeyStoreParams.getName () der Containername und esiaKeyStoreParams.getValue (). ToCharArray () ist das Containerkennwort.

In unserem Fall müssen keine Daten mit der load () -Methode in den Speicher geladen werden, da die Schlüssel bereits vorhanden sind, wenn der Name dieses Speichers angegeben wird.



Hierbei ist zu beachten, dass Sie eine Unterschrift im Formular erhalten



final Signature signature = Signature.getInstance(SIGN_ALGORITHM, PROVIDER_NAME);
signature.initSign(privateKey);
signature.update(data);
final byte[] sign = signature.sign();
      
      





Dies reicht uns nicht aus, da die ESIA eine getrennte Signatur des PKCS7-Formats erfordert. Daher sollte eine Signatur im PKCS7-Format generiert werden.



Ein Beispiel für unsere Methode zur Rückgabe einer getrennten Signatur sieht folgendermaßen aus:



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);
  }
      
      





Hier überprüfen wir unseren öffentlichen Schlüssel und den öffentlichen ESIA-Schlüssel. Da die Methode cmsSign () möglicherweise vertrauliche Informationen enthält, werden wir diese nicht weitergeben.



Hier nur einige Details:



  • rawClientSecret.getBytes () - Byte-Array aus Bereich, Zeitstempel, Client-ID und Status;
  • localPrivateKey - privater Schlüssel aus dem Container;
  • localCertificate - öffentlicher Schlüssel aus dem Container;
  • true - Boolescher Wert des Signaturparameters - Auschecken oder nicht.


Ein Beispiel für die Erstellung einer Signatur finden Sie in der CryptoPro-Java-Bibliothek, in der der PKCS7-Standard CMS heißt. Und auch im Programmierhandbuch, das im Quellcode der heruntergeladenen Version von CryptoPro enthalten ist.



Einen Token bekommen



Der nächste Schritt besteht darin, ein Zugriffstoken (auch als Token bezeichnet) im Austausch gegen einen Autorisierungscode zu erhalten, der bei erfolgreicher Benutzerautorisierung im State Services-Portal als Parameter empfangen wurde.



Um Daten im Unified Identification System zu erhalten, benötigen Sie ein Zugriffstoken. Dazu stellen wir eine Anfrage an die ESIA. Die Hauptanforderungsfelder hier werden auf die gleiche Weise gebildet, der Code sieht wie folgt aus:



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

      
      





Nachdem Sie die Antwort erhalten haben, analysieren Sie sie und erhalten Sie den Token:



try (CloseableHttpResponse response = httpClient.execute(getTokenPostRequest)) {
  HttpEntity tokenEntity = response.getEntity();
  String tokenEntityString = EntityUtils.toString(tokenEntity);
  tokenResponseDto = extractEsiaGetResponseTokenDto(tokenEntityString);
}

      
      





Das Token ist eine dreiteilige Zeichenfolge, die durch Punkte getrennt ist: HEADER.PAYLOAD.SIGNATURE, wobei:



  • HEADER ist ein Header mit den Eigenschaften eines Tokens, einschließlich eines Signaturalgorithmus.
  • PAYLOAD ist eine Information über das Token und das Thema, die wir von den staatlichen Diensten anfordern.
  • Unterschrift ist die Unterschrift von HEADER.PAYLOAD.


Token-Validierung



Um sicherzustellen, dass wir eine Antwort von den State Services erhalten haben, muss das Token validiert werden, indem der Pfad zum Zertifikat (öffentlicher Schlüssel) angegeben wird, der von der State Services-Website heruntergeladen werden kann. Durch Übergeben der empfangenen Zeichenfolge (Daten) und Signatur (dataSignature) an die Methode isEsiaSignatureValid () können Sie das Validierungsergebnis als booleschen Wert abrufen.



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));
}
      
      





Gemäß den Richtlinien muss die Gültigkeitsdauer des Tokens überprüft werden. Wenn die Gültigkeitsdauer abgelaufen ist, müssen Sie einen neuen Link mit zusätzlichen Parametern erstellen und eine Anfrage über den http-Client stellen:



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





Benutzerdaten abrufen



In unserem Fall müssen Sie Ihren vollständigen Namen, Ihr Geburtsdatum, Ihre Passdaten und Ihre Kontakte angeben.

Wir verwenden eine funktionale Schnittstelle, die beim Empfang von Benutzerdaten hilft:



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);
  }
};
      
      





Benutzerdaten abrufen:



String personDataEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId);
      
      





Das Abrufen von Kontakten ist nicht mehr so ​​offensichtlich wie das Abrufen von Benutzerdaten. Zunächst sollten Sie eine Liste mit Links zu Kontakten erhalten:



String contactsListEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId + "/ctts");
EsiaListDto esiaListDto = objectMapper.readValue(contactsListEntityString, EsiaListDto.class);
      
      





Deserialisieren Sie diese Liste und rufen Sie das Objekt esiaListDto ab. Die Felder aus dem ESIA-Handbuch können abweichen, daher lohnt es sich, sie empirisch zu überprüfen.



Als nächstes müssen Sie jedem Link aus der Liste folgen, um jeden Benutzerkontakt zu erhalten. Es wird so aussehen:



for (String contactUrl : esiaListDto.getElementUrls()) {
  String contactEntityString = esiaPersonDataFetcher.apply(contactUrl);
  EsiaContactDto esiaContactDto = objectMapper.readValue(contactEntityString, EsiaContactDto.class);
}
      
      





Ähnlich verhält es sich mit dem Erhalt einer Liste von Dokumenten. Zunächst erhalten wir eine Liste mit Links zu Dokumenten:



String documentsListEntityString = esiaPersonDataFetcher.apply(ESIA_REST_API_URL + "/prns/" + esiaAccountId + "/docs");

      
      





Dann deserialisieren Sie es:



EsiaListDto esiaDocumentsListDto = objectMapper.readValue(documentsListEntityString, EsiaListDto.class);
      :
for (String documentUrl : esiaDocumentsListDto.getElementUrls()) {
  String documentEntityString = esiaPersonDataFetcher.apply(documentUrl);
  EsiaDocumentDto esiaDocumentDto = objectMapper.readValue(documentEntityString, EsiaDocumentDto.class);
}

      
      





Was tun mit all diesen Daten?



Wir können die Daten analysieren und Objekte mit den erforderlichen Feldern abrufen. Hier kann jeder Entwickler Klassen nach Bedarf gemäß den Bestimmungen entwerfen.



Ein Beispiel für das Abrufen eines Objekts mit den erforderlichen Feldern:



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);

      
      





Wir füllen das esiaPersonDto-Objekt mit den erforderlichen Daten, zum Beispiel Kontakten:



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());
  }
}

      
      





Die EsiaPersonDto-Klasse sieht folgendermaßen aus:



@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;
}
      
      





Die Arbeiten zur Verbesserung des Dienstes werden fortgesetzt, da die ESIA nicht stillsteht.



All Articles