Bagaimana menerapkan integrasi dengan ESIA di Java tanpa masalah yang tidak perlu

Untuk waktu yang lama, cara utama mengidentifikasi warga adalah paspor biasa. Situasi berubah ketika, pada tahun 2011, atas perintah Kementerian Telekomunikasi dan Komunikasi Massa, Sistem Identifikasi dan Otentikasi Terpadu (ESIA) diperkenalkan - memungkinkan untuk mengenali identitas seseorang dan menerima data tentangnya secara online.



Berkat implementasi ESIA, organisasi pemerintah dan komersial, pengembang dan pemilik layanan online dapat mempercepat dan membuat operasi yang lebih aman terkait dengan input dan verifikasi data pengguna. Rusfinance Bank juga memutuskan untuk menggunakan potensi sistem dan, ketika menyelesaikan layanan pemrosesan pinjaman online (bank mengkhususkan diri pada pinjaman mobil), menerapkan integrasi dengan platform tersebut.



Ini ternyata tidak mudah. Sejumlah persyaratan dan prosedur harus dipenuhi, untuk mengatasi kesulitan teknis.



Pada artikel ini, kami akan mencoba memberi tahu Anda tentang poin-poin utama dan pedoman metodologis yang penting diketahui bagi mereka yang ingin menerapkan integrasi dengan ESIA secara mandiri, serta menyediakan fragmen kode dalam bahasa Java yang akan membantu mengatasi kesulitan selama pengembangan (bagian dari implementasi dihilangkan, tetapi urutan tindakan umum jelas).



Kami berharap pengalaman kami akan membantu pengembang Java (dan tidak hanya) menghemat banyak waktu saat mengembangkan dan membiasakan diri dengan rekomendasi metodologis dari Kementerian Telekomunikasi dan Komunikasi Massa.







Mengapa kita membutuhkan integrasi dengan ESIA?



Akibat pandemi virus corona, jumlah transaksi offline di berbagai bidang peminjaman mulai menurun. Pelanggan mulai "online" dan sangat penting bagi kami untuk memperkuat kehadiran online kami di pasar kredit mobil. Dalam proses penyelesaian layanan Autocredit (sudah ada artikel tentang perkembangannya di Habré ), kami memutuskan untuk membuat antarmuka untuk menempatkan aplikasi pinjaman di situs web bank semudah dan sesederhana mungkin. Integrasi dengan ESIA telah menjadi momen penting dalam menyelesaikan masalah ini, karena memungkinkan untuk mendapatkan data pribadi klien secara otomatis.







Untuk klien, solusi ini juga ternyata nyaman, karena memungkinkan untuk menggunakan satu login dan kata sandi untuk mendaftar dan memasuki layanan persetujuan online untuk aplikasi pembelian mobil secara kredit.



Selain itu, integrasi dengan ESIA memungkinkan Rusfinance Bank untuk:



  • kurangi waktu untuk mengisi kuesioner online;
  • kurangi jumlah pantulan pengguna saat mencoba mengisi sejumlah besar bidang secara manual;
  • memberikan aliran klien yang lebih "berkualitas" dan terverifikasi.


Terlepas dari kenyataan bahwa kami menceritakan tentang pengalaman bank kami, informasi tersebut dapat berguna tidak hanya untuk lembaga keuangan. Pemerintah merekomendasikan penggunaan platform ESIA untuk jenis layanan online lainnya (lebih detail di sini ).



Apa yang harus dilakukan dan bagaimana caranya?



Pada awalnya, bagi kami tampaknya tidak ada yang istimewa dalam integrasi dengan ESIA dari sudut pandang teknis - tugas standar yang terkait dengan memperoleh data melalui REST API. Namun, setelah diteliti lebih dekat, menjadi jelas bahwa tidak semuanya sesederhana itu. Misalnya, ternyata kami tidak tahu cara bekerja dengan sertifikat yang diperlukan untuk menandatangani beberapa parameter. Saya harus membuang waktu dan mencari tahu. Tapi hal pertama yang pertama.



Untuk memulainya, penting untuk membuat garis besar rencana tindakan. Rencana kami mencakup langkah-langkah utama berikut:



  1. mendaftar di portal teknologi ESIA;
  2. mengajukan aplikasi untuk penggunaan antarmuka perangkat lunak ESIA dalam pengujian dan lingkungan industri;
  3. secara mandiri mengembangkan mekanisme untuk interaksi dengan ESIA (sesuai dengan dokumen saat ini "Rekomendasi metodologis untuk penggunaan ESIA");
  4. menguji pengoperasian mekanisme dalam pengujian dan lingkungan industri ESIA.


Kami biasanya mengembangkan proyek kami di Jawa. Oleh karena itu, untuk implementasi perangkat lunak kami memilih:



  • IDE IntelliJ;
  • CryptoPro JCP (atau CryptoPro Java CSP);
  • Jawa 8;
  • Apache HttpClient;
  • Lombok;
  • FasterXML / Jackson.


Mendapatkan URL pengalihan



Langkah pertama adalah mendapatkan kode otorisasi. Dalam kasus kami, ini dilakukan oleh layanan terpisah dengan pengalihan ke halaman otorisasi portal Layanan Negara (kami akan memberi tahu Anda tentang hal ini secara lebih rinci).



Pertama, kami menginisialisasi variabel ESIA_AUTH_URL (alamat ESIA) dan API_URL (alamat tempat pengalihan terjadi jika otorisasi berhasil). Setelah itu, kami membuat objek EsiaRequestParams, yang berisi parameter permintaan ke ESIA di bidangnya, dan membentuk tautan 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();
}
      
      





Untuk kejelasan, mari tunjukkan bagaimana kelas EsiaRequestParams terlihat:



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





Setelah itu, Anda perlu mengarahkan pengguna ke layanan otentikasi ESIA. Pengguna memasukkan nama pengguna-kata sandi, mengonfirmasi akses ke data untuk sistem kami. Kemudian ESIA mengirimkan tanggapan ke layanan online, yang berisi kode otorisasi. Kode ini akan diperlukan untuk pertanyaan lebih lanjut ke ESIA.



Setiap permintaan ke ESIA memiliki parameter client_secret, yang merupakan tanda tangan elektronik terpisah dalam format PKCS7 (Standar Kriptografi Kunci Publik). Dalam kasus kami, sertifikat digunakan untuk penandatanganan, yang diterima oleh pusat sertifikasi sebelum mulai mengerjakan integrasi dengan ESIA. Bagaimana bekerja dengan penyimpanan kunci dijelaskan dengan baik dalam seri artikel ini .



Sebagai contoh, kami akan menunjukkan tampilan keystore yang disediakan oleh CryptoPro:







Memanggil kunci privat dan publik dalam hal ini akan terlihat seperti ini:



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





Di mana JCP.HD_STORE_NAME adalah nama penyimpanan di CryptoPro, esiaKeyStoreParams.getName () adalah nama penampung dan esiaKeyStoreParams.getValue (). ToCharArray () adalah sandi penampung.

Dalam kasus kami, tidak perlu memuat data ke penyimpanan menggunakan metode load (), karena kunci sudah ada di sana saat menentukan nama penyimpanan ini.



Penting untuk diingat di sini bahwa mendapatkan tanda tangan dalam formulir



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





itu tidak cukup bagi kami, karena ESIA memerlukan tanda tangan terpisah dari format PKCS7. Oleh karena itu, tanda tangan format PKCS7 harus dibuat.



Contoh metode kami yang mengembalikan tanda tangan terpisah terlihat seperti ini:



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





Di sini kami memeriksa kunci publik kami dan kunci publik ESIA. Karena metode cmsSign () mungkin berisi informasi rahasia, kami tidak akan mengungkapkannya.



Berikut ini beberapa detailnya:



  • rawClientSecret.getBytes () - array byte lingkup, timestamp, clientId dan status;
  • localPrivateKey - kunci pribadi dari wadah;
  • localCertificate - kunci publik dari wadah;
  • true - nilai boolean dari parameter tanda tangan - checkout atau tidak.


Contoh pembuatan tanda tangan dapat ditemukan di pustaka java CryptoPro, di mana standar PKCS7 disebut CMS. Dan juga dalam manual pemrogram, yang disertakan dengan kode sumber dari versi unduhan CryptoPro.



Mendapatkan token



Langkah selanjutnya adalah mendapatkan token akses (alias token) dengan imbalan kode otorisasi, yang diterima sebagai parameter setelah otorisasi pengguna yang berhasil di portal Layanan Negara.



Untuk menerima data apa pun dalam sistem identifikasi Terpadu, Anda perlu mendapatkan token akses. Untuk melakukan ini, kami mengajukan permintaan ke ESIA. Bidang permintaan utama di sini dibentuk dengan cara yang sama, kodenya terlihat seperti berikut:



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

      
      





Setelah menerima jawabannya, parsing dan dapatkan tokennya:



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

      
      





Token adalah string tiga bagian yang dipisahkan oleh titik: HEADER.PAYLOAD.SIGNATURE, di mana:



  • HEADER adalah tajuk yang memiliki properti token, termasuk algoritme tanda tangan;
  • PAYLOAD adalah informasi tentang token dan subjek, yang kami minta dari Layanan Negara;
  • Tanda tangan adalah tanda tangan HEADER.PAYLOAD.


Validasi token



Untuk memastikan bahwa kami menerima tanggapan dari Layanan Negara, perlu untuk memvalidasi token dengan menentukan jalur ke sertifikat (kunci publik), yang dapat diunduh dari situs web Layanan Negara. Dengan meneruskan string (data) dan tanda tangan (dataSignature) yang diterima ke metode isEsiaSignatureValid (), Anda bisa mendapatkan hasil validasi sebagai nilai boolean.



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





Sesuai dengan pedoman, perlu dilakukan pengecekan masa berlaku token. Jika masa berlaku telah habis, maka Anda perlu membuat tautan baru dengan parameter tambahan dan membuat permintaan menggunakan klien 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();
      
      





Mengambil data pengguna



Dalam kasus kami, Anda perlu mendapatkan nama lengkap, tanggal lahir, detail paspor, dan kontak Anda.

Kami menggunakan antarmuka fungsional yang akan membantu menerima data pengguna:



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





Mendapatkan data pengguna:



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





Mendapatkan kontak tidak lagi sejelas mendapatkan data pengguna. Pertama, Anda harus mendapatkan daftar tautan ke kontak:



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





Deserialize daftar ini dan dapatkan objek esiaListDto. Kolom dari manual ESIA mungkin berbeda, jadi perlu diperiksa secara empiris.



Selanjutnya, Anda perlu mengikuti setiap tautan dari daftar untuk mendapatkan setiap kontak pengguna. Ini akan terlihat seperti ini:



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





Situasinya sama dengan mendapatkan daftar dokumen. Pertama, kami mendapatkan daftar tautan ke dokumen:



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

      
      





Kemudian deserialize itu:



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

      
      





Sekarang apa yang harus dilakukan dengan semua data ini?



Kita dapat mengurai data dan mendapatkan objek dengan bidang yang diperlukan. Di sini setiap pengembang dapat merancang kelas sesuai kebutuhannya, sesuai dengan kerangka acuan.



Contoh mendapatkan objek dengan bidang yang wajib diisi:



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

      
      





Kami mengisi objek esiaPersonDto dengan data yang diperlukan, misalnya, kontak:



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

      
      





Kelas EsiaPersonDto terlihat seperti ini:



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





Pekerjaan untuk meningkatkan layanan akan terus berlanjut, karena ESIA tidak berhenti.



All Articles