䞍芁な問題なしにJavaでESIAずの統合を実装する方法

長い間、垂民を特定する䞻な方法は通垞のパスポヌトでした。 2011幎に、電気通信マス通信省の呜什により、統䞀識別認蚌システムESIAが導入されたずき、状況は倉化したした。これにより、個人の身元を認識し、オンラむンでデヌタを受信できるようになりたした。



ESIAの実装のおかげで、政府および商業組織、オンラむンサヌビスの開発者および所有者は、ナヌザヌデヌタの入力および怜蚌に関連する操䜜を高速化し、より安党にするこずができたした。 Rusfinance Bankはたた、システムの可胜性を利甚するこずを決定し、オンラむンロヌン凊理サヌビス銀行は自動車ロヌンを専門ずしおいたすを完成させる際に、プラットフォヌムずの統合を実装したした。



これはそれほど簡単ではありたせんでした。技術的な問題を解決するには、いく぀かの芁件ず手順を満たす必芁がありたした。



この蚘事では、ESIAずの統合を独立しお実装したい人にずっお知っおおくべき重芁なポむントず方法論のガむドラむンに぀いお説明し、開発䞭の問題を克服するのに圹立぀Java蚀語のコヌドフラグメントを提䟛したす実装の䞀郚は省略されおいたすが、䞀般的な䞀連のアクション明確です。



私たちの経隓が、Java開発者がだけでなくテレコムおよびマスコミュニケヌション省の方法論的掚奚事項を開発しお理解する際に倚くの時間を節玄するのに圹立぀こずを願っおいたす。







ESIAずの統合が必芁なのはなぜですか



コロナりむルスの倧流行に関連しお、貞付の倚くの分野でのオフラむン取匕の数は枛少し始めたした。クラむアントは「オンラむン化」し始め、自動車ロヌン垂堎でのオンラむンプレれンスを匷化するこずが䞍可欠でした。 Autocreditサヌビスを完成させる過皋でHabréはすでにその開発に関する蚘事を持っおいたす、銀行のWebサむトにロヌン申請を配眮するためのむンタヌフェむスをできるだけ䟿利でシンプルにするこずにしたした。 ESIAずの統合は、クラむアントの個人デヌタを自動的に取埗するこずを可胜にしたため、この問題を解決する䞊で重芁な瞬間になりたした。







クラむアントにずっおも、この゜リュヌションは、1回のログむンずパスワヌドを䜿甚しお、クレゞットで車を賌入するためのオンラむン承認サヌビスに登録しお入力するこずができたため、䟿利であるこずがわかりたした。



さらに、ESIAずの統合により、RusfinanceBankは次のこずが可胜になりたした。



  • オンラむン質問祚に蚘入する時間を短瞮したす。
  • 倚数のフィヌルドに手動で入力しようずするずきのナヌザヌバりンスの数を枛らしたす。
  • 「高品質」の怜蚌枈みクラむアントのストリヌムを提䟛したす。


私たちが銀行の経隓に぀いお話しおいるずいう事実にもかかわらず、その情報は金融機関だけでなく圹立぀可胜性がありたす。政府は、他のタむプのオンラむンサヌビスにESIAプラットフォヌムを䜿甚するこずを掚奚しおいたす詳现はこちら。



䜕をどのように行うか



最初は、技術的な芳点からESIAずの統合に特別なこずは䜕もないように芋えたした。これは、RESTAPIを介したデヌタの取埗に関連する暙準的なタスクです。しかし、詳しく調べおみるず、すべおがそれほど単玔ではないこずが明らかになりたした。たずえば、いく぀かのパラメヌタに眲名するために必芁な蚌明曞をどのように凊理するかがわからないこずが刀明したした。私は時間を無駄にしおそれを理解しなければなりたせんでした。しかし、たず最初に。



たず、行動蚈画の抂芁を説明するこずが重芁でした。私たちの蚈画には、次の䞻なステップが含たれおいたした。



  1. ESIAテクノロゞヌポヌタルに登録したす。
  2. テストおよび産業環境でESIA゜フトりェアむンタヌフェヌスを䜿甚するための申請曞を提出する。
  3. ESIAずの盞互䜜甚のメカニズムを独自に開発したす珟圚の文曞「ESIAの䜿甚に関する方法論的掚奚事項」に埓っお。
  4. ESIAのテストおよび産業環境でメカニズムの動䜜をテストしたす。


私たちは通垞、Javaでプロゞェクトを開発したす。したがっお、゜フトりェアの実装には次のものを遞択したした。



  • IntelliJ IDEA;
  • CryptoPro JCPたたはCryptoPro Java CSP;
  • Java 8;
  • Apache HttpClient;
  • ロンボク;
  • FasterXML /ゞャク゜ン。


リダむレクトURLの取埗



最初のステップは、認蚌コヌドを取埗するこずです。私たちの堎合、これは、State Servicesポヌタルの承認ペヌゞぞのリダむレクトを䌎う別のサヌビスによっお行われたすこれに぀いお詳しく説明したす。



たず、倉数ESIA_AUTH_URLESIAアドレスずAPI_URL認蚌が成功した堎合にリダむレクトが発生するアドレスを初期化したす。その埌、ESIAぞのリク゚ストのパラメヌタをフィヌルドに含むEsiaRequestParamsオブゞェクトを䜜成し、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();
}
      
      





わかりやすくするために、EsiaRequestParamsクラスがどのように芋えるかを瀺したしょう。



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





その埌、ナヌザヌをESIA認蚌サヌビスにリダむレクトする必芁がありたす。ナヌザヌはナヌザヌ名ずパスワヌドを入力し、システムのデヌタぞのアクセスを確認したす。次に、ESIAは、認蚌コヌドを含む応答をオンラむンサヌビスに送信したす。このコヌドは、ESIAぞのさらなる問い合わせに必芁になりたす。



ESIAぞの各芁求には、PKCS7圢匏Public Key Cryptography Standardの分離された電子眲名であるclient_secretパラメヌタヌがありたす。この堎合、眲名には蚌明曞が䜿甚されたす。この蚌明曞は、ESIAずの統合䜜業を開始する前に認蚌センタヌによっお受信されたした。キヌストアの操䜜方法は、この䞀連の蚘事で詳しく説明されおいたす。



䟋ずしお、CryptoProによっお提䟛されるキヌストアがどのように芋えるかを瀺したしょう。







この堎合、秘密鍵ず公開鍵の呌び出しは次のようになりたす。



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





JCP.HD_STORE_NAMEがCryptoProのストレヌゞ名であるのに察し、esiaKeyStoreParams.getNameはコンテナヌ名、esiaKeyStoreParams.getValueです。ToCharArrayはコンテナヌパスワヌドです。

この堎合、このストレヌゞの名前を指定するずきにキヌがすでに存圚するため、loadメ゜ッドを䜿甚しおストレヌゞにデヌタをロヌドする必芁はありたせん。



ここで、フォヌムで眲名を取埗するこずを芚えおおくこずが重芁です。



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





ESIAはPKCS7圢匏の分離された眲名を必芁ずするため、それだけでは十分ではありたせん。したがっお、PKCS7圢匏の眲名を生成する必芁がありたす。



デタッチされた眲名を返すメ゜ッドの䟋は次のようになりたす。



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





ここでは、公開鍵ずESIA公開鍵を確認したす。cmsSignメ゜ッドには機密情報が含たれおいる可胜性があるため、開瀺したせん。



詳现は次のずおりです。



  • rawClientSecret.getBytes-スコヌプ、タむムスタンプ、clientId、および状態のバむト配列。
  • localPrivateKey-コンテナからの秘密鍵。
  • localCertificate-コンテナからの公開鍵。
  • true-眲名パラメヌタヌのブヌル倀-チェックアりトするかどうか。


眲名の䜜成䟋は、PKCS7暙準がCMSず呌ばれるCryptoProjavaラむブラリにありたす。たた、ダりンロヌドしたバヌゞョンのCryptoProの゜ヌスコヌドに含たれおいるプログラマヌズマニュアルにも蚘茉されおいたす。



トヌクンの取埗



次のステップは、認蚌コヌドず匕き換えにアクセストヌクン別名トヌクンを受信するこずです。認蚌コヌドは、StateServicesポヌタルでのナヌザヌ認蚌が成功したずきにパラメヌタヌずしお受信されたした。



統合識別システムでデヌタを受信するには、アクセストヌクンを取埗する必芁がありたす。これを行うために、統合された識別および認蚌システムに芁求を䜜成したす。ここでの䞻なリク゚ストフィヌルドは同じ方法で圢成され、コヌドは次のようになりたす。



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

      
      





答えを受け取ったら、それを解析しおトヌクンを取埗したす。



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

      
      





トヌクンは、ピリオドで区切られた3぀の郚分からなる文字列ですHEADER.PAYLOAD.SIGNATURE、ここで



  • HEADERは、眲名アルゎリズムを含むトヌクンのプロパティを持぀ヘッダヌです。
  • PAYLOADは、トヌクンずサブゞェクトに関する情報であり、州のサヌビスに芁求したす。
  • 眲名はHEADER.PAYLOADの眲名です。


トヌクンの怜蚌



ステヌトサヌビスからの応答を確実に受信するには、ステヌトサヌビスのWebサむトからダりンロヌドできる蚌明曞公開キヌぞのパスを指定しおトヌクンを怜蚌する必芁がありたす。受信した文字列dataず眲名dataSignatureをisEsiaSignatureValidメ゜ッドに枡すこずにより、怜蚌結果をブヌル倀ずしお取埗できたす。



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





ガむドラむンに埓い、トヌクンの有効期間を確認する必芁がありたす。有効期間が終了した堎合は、远加のパラメヌタヌを䜿甚しお新しいリンクを䜜成し、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();
      
      





ナヌザヌデヌタの取埗



私たちの堎合、あなたはあなたのフルネヌム、生幎月日、パスポヌトの詳现ず連絡先を取埗する必芁がありたす。

ナヌザヌデヌタの受信に圹立぀機胜むンタヌフェむスを䜿甚したす。



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





ナヌザヌデヌタの取埗



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





連絡先の取埗は、ナヌザヌデヌタの取埗ほど明癜ではなくなりたした。たず、連絡先ぞのリンクのリストを取埗する必芁がありたす。



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





このリストを逆シリアル化し、esiaListDtoオブゞェクトを取埗したす。ESIAマニュアルのフィヌルドは異なる堎合があるため、経隓的に確認する䟡倀がありたす。



次に、リストの各リンクをたどっお、各ナヌザヌの連絡先を取埗する必芁がありたす。次のようになりたす。



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





状況は、ドキュメントのリストを取埗する堎合ず同じです。たず、ドキュメントぞのリンクのリストを取埗したす。



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

      
      





次に、それを逆シリアル化したす。



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

      
      





では、このすべおのデヌタをどうするのでしょうか。



デヌタを解析しお、必芁なフィヌルドを持぀オブゞェクトを取埗できたす。ここでは、各開発者は、参照条件に埓っお、必芁に応じおクラスを蚭蚈できたす。



必須フィヌルドを持぀オブゞェクトを取埗する䟋



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

      
      





esiaPersonDtoオブゞェクトに、連絡先などの必芁なデヌタを入力したす。



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

      
      





EsiaPersonDtoクラスは次のようになりたす。



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





ESIAは静止しおいないため、サヌビスの改善䜜業は継続されたす。



All Articles