- Что делать и как?
- Введение
- Что теперь делать со всеми этими данными?
- Запрос токена идентификации
- Зачем нам есиа?
- Зачем нам нужна интеграция с есиа?
- Получение url для переадресации
- Получение данных о пользователе
- Получение данных организации
- Получение данных пользователя
- Получение токена доступа
- Предисловие
- Создание ссылки для редиректа на страницу есиа
- Стек и схема интеграции
- Формирование подписи
- Заключение
Что делать и как?
Сначала нам показалось, что в интеграции с ЕСИА нет ничего особенного с технической точки зрения — стандартная задача, связанная с получением данных посредством REST API. Однако, при ближайшем рассмотрении стало понятно, что не всё так просто. Например, выяснилось, что у нас нет представления о том, как работать с сертификатами, необходимыми для подписи нескольких параметров. Пришлось тратить время и разбираться. Но обо всем по порядку.
Для начала важно было наметить план действий. Наш план включал следующие основные шаги:
- зарегистрироваться на технологическом портале ЕСИА;
- подать заявки на использование программных интерфейсов ЕСИА в тестовой и промышленной среде;
- самостоятельно разработать механизм взаимодействия с ЕСИА (в соответствии с действующим документом «Методические рекомендации по использованию ЕСИА»);
- протестировать работу механизма в тестовой и промышленной среде ЕСИА.
Обычно мы разрабатываем наши проекты на Java. Поэтому для программной реализации выбрали:
Введение
Стоит упомянуть, что есть компании, которые имеют готовые решения для интеграции с ЕСИА, например эта или вот эта — если вам лень во всем этом разбираться, можно воспользоваться их услугами. Сами не пользовались, советовать не можем.
Подробное описание общего механизма интеграции представлено на сайте Министерства цифрового развития… Методичка довольно увесистая, но даже из нее не все детали очевидны при реализации. Если в кратце, то основные шаги выглядят следующим образом:
- Регистрация ИС в регистре информационных систем ЕСИА
- Регистрация ИС в тестовой среде
- Выполнение доработки системы для взаимодействия с ЕСИА
В данной статье будет описан только 3 пункт, предыдущие 2 – бюрократия, оставим ее за рамками Хабра. В методичке предлагают реализовать интеграцию 2 способами: SAML или OpenID Connect. Говорят,
с 01.01.2022 г. взаимодействие по протоколу SAML 2.0 больше не будет разрешено (только для действующих систем). Для подключения к ЕСИА необходимо будет использовать протокол OAuth 2.0 / OpenID Connect (сейчас доступны оба варианта).
Поэтому мы выбрали красную таблетку второй вариант. Согласно методичке, базовый сценарий аутентификации представляет собой примерно следующие шаги:
Что теперь делать со всеми этими данными?
Мы можем сделать парсинг данных и получать объекты с требуемыми полями. Здесь каждый разработчик может оформлять классы как ему необходимо, в соответствии с техническим заданием.
Пример получения объекта с необходимыми полями:
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;
}
Работа по усовершенствованию сервиса будет продолжаться, ведь ЕСИА не стоит на месте.
Запрос токена идентификации
После успешной аутентификации на странице ЕСИА пользователь возвращается на фронтенд приложения по указанному нами адресу. ЕСИА передаёт авторизационный токен в виде get-параметра code. Этот токен необходимо передать на бэкенд и запросить с его помощью идентификационный токен пользователя.
Зачем нам есиа?
Согласно Федеральному закону № 225-ФЗ от 28.06.2021 «О внесении изменений в часть первую Гражданского кодекса Российской Федерации», многие организаций в РФ получили право проводить официальные собрания и голосования по корпоративным вопросам дистанционно.
Ранее решения с юридической силой требовали очных собраний или голосований по почте. Голосования по почте не отличаются надежностью, а собрать много руководителей со всей России в одном месте — это кошмар с точки зрения затрат.
Чтобы проводить мероприятия принятия решения дистанционно в соответствии с новым федеральным законом, необходимо предоставить возможность достоверного установления личности участников. В России это возможно через проверку доступа к верифицированному аккаунту на Госуслугах.
Зачем нам нужна интеграция с есиа?
В связи с пандемией коронавируса количество офлайн сделок во многих направлениях кредитования начало сокращаться. Клиенты стали «уходить в онлайн», и для нас было жизненно важно укрепить своё онлайн-присутствие на рынке автокредитования. В процессе доработки сервиса «Автокредит» (на Хабре уже есть
) мы решили сделать интерфейс заведения кредитных заявок на сайте банка максимально удобным и простым. Интеграция с ЕСИА стала ключевым моментом в решении этой задачи, поскольку позволила автоматически получить персональные данные клиента.
Получение url для переадресации
Первый шаг ― это получение авторизационного кода. В нашем случае это делает отдельный сервис с переадресацией на страницу авторизации портала Госуслуг (расскажем об этом немного подробнее).
Сначала мы инициализируем переменные ESIA_AUTH_URL (адрес ЕСИА) и API_URL (адрес, на который происходит редирект в случае успешной авторизации). После этого создаем объект 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;
}
}
Получение данных о пользователе
Идентификационный токен пользователя необходимо проверить с помощью публичного RSA ключа от ЕСИА и получить из него id пользователя. С помощью этого id и accessToken, который мы получили в предыдущем шаге, мы уже наконец можем запросить персональные данные пользователя.
Получение данных организации
Полученный токен доступа для запроса данных по организации мы использовать не можем, т.к. он завязан на определенный scope. Поэтому нужно получить отдельный токен. По сути методы получения токена доступа для организации и информации по ней не сильно отличаются от рассмотренных ранее.
Получение данных пользователя
В нашем случае необходимо получить ФИО, дату рождения, паспортные данные и контакты.
Используем функциональный интерфейс, который поможет получать данные пользователя:
Получение токена доступа
Для получение каких-либо данных в ЕСИА нам нужно получить токен доступа. Для этого формируем POST запрос в ЕСИА (для тестовой среды базовый url такой:
Предисловие
Однажды в далекой-далекой галактике… потребовалось нам реализовать аутентификацию пользователей с помощью учетной записи ЕСИА на ГосУслугах. Т.к. обитаем мы в галактике .Net, первым делом был изучен весь гугол на предмет готового космолета дабы не костылить все самим, но поиски ни к чему путному не привели. Поэтому решено было изучить тему и реализовать-таки космолет своими силами.
Создание ссылки для редиректа на страницу есиа
Все начинается с того, что пользователь решает пройти авторизацию через ЕСИА. Создаем на бэкенде ссылку для перехода с использованием нашего инструмента формирования подписей.
async getAuthLink(redirectLink: string) {
const params = await this.signParams({
redirect_uri: redirectLink,
response_type: 'code',
access_type: 'offline',
})
const authQuery = new URLSearchParams(params)
const authURL = `${this.esiaHost}/aas/oauth2/ac`
return `${authURL}?${authQuery}`
}
В redirectLink необходимо указать адрес страницы, на которую ЕСИА перенаправит пользователя после успешной аутентификации. Созданную ссылку возвращаем на фронтенд и перенаправляем на нее пользователя.
Стек и схема интеграции
Для интеграции мы используем:

Формирование подписи
Прежде чем разбирать все по порядку, кое о чем стоит подумать заранее. В отличие от других интеграций, запросы к ЕСИА должны сопровождаться подписью ГОСТ Р 34.10/11-2022, а не просто API key. Создать такую подпись можно с помощью утилиты КриптоПро CSP.
Для нас основная задача здесь — правильно обернуть эту утилиту в Docker, чтобы с ней можно было работать как с отдельным сервисом в рамках нашей инфраструктуры. Получившийся сервис мы выложили в открытый доступ на гитхабе. Инструкция по запуску есть в README.md.
В процесс сборки Docker образа сервиса с утилитой КриптоПро мы встроили:
Таким образом вся криптография собрана в отдельном самостоятельном компоненте, который можно использовать, когда необходимо что-нибудь подписать. Вот как это выглядит на бэкенде:
private async signParams(params: Record<string, string>) {
const time = moment().format('YYYY.MM.DD HH:mm:ss ZZ')
const state = uuid()
const clientId = this.clientId
const scope = this.scope
const { data: { result: clientSecret } } = await axios.post<{ result: string }>(
`${this.cryptoProServiceAddress}/cryptopro/sign`,
{ text: [scope, time, clientId, state].join('') },
)
return {
...params,
timestamp: time,
client_id: clientId,
scope,
state,
client_secret: clientSecret.replace(/n/g, ''),
}
}
С созданием подписей разобрались, теперь последовательно разберем, как реализовать схему выше.
Заключение
Пожалуй, этого достаточно для базового сценария взаимодействия с ЕСИА. В целом, если знать особенности реализации, программное подключение системы к ЕСИА не займет более 1 дня. Если у вас появятся вопросы, добро пожаловать в комменты. Спасибо, что дочитали мой пост до конца, надеюсь, он будет полезен.