Всем привет. Меня зовут Динис, я старший разработчик в БФТ-Холдинге.
В данной статье приведен план по созданию сервиса авторизации через систему ЕСИА. Тема будет интересна тем, кто внедряет такой сервис для своего приложения. В статье я собрал ключевые выдержки из документации с частями кода, а весь код вы можете найти в конце статьи по ссылке на GitHub.
Что такое ЕСИА?
ЕСИА — единая система идентификации и аутентификации. В ней формируются, учитываются и хранятся сведения об участниках системы — физических и юридических лицах, которые прошли регистрацию с созданием учётной записи. Подтверждённая учётная запись в ЕСИА — своего рода электронный паспорт, с помощью которого можно получать доступ к разным сайтам, порталам и системам, не используя дополнительные средства регистрации и авторизации, например логин и пароль.
У сервиса ЕСИА прекрасная документация, поэтому большая часть данной статьи содержит ее выдержки – они выделены курсивом. Полная версия документации доступна по ссылке: https://digital.gov.ru/ru/documents/6182/
Теперь – к самому интересному.
1. Создание Java-проекта

Для начала добавим эти зависимости:

В качестве криптопровайдера будем использовать российский криптопровайдер Vipnet, сертифицированный ФСБ России как средство криптографической защиты информации и электронной подписи.
Скачаем с сайта (https://infotecs.ru/products/vipnet-jcrypto-sdk/) библиотеки крипторовайдера. Добавим их в директорию jcr/lib в корень проекта.
Добавим зависимости build.gradle:
plugins { id 'java' id 'org.springframework.boot' version '2.7.15' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'ru.habr' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '1.8' } repositories { mavenCentral() } ext { set('springCloudVersion', "2021.0.8") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.cloud:spring-cloud-starter' implementation 'org.springframework.cloud:spring-cloud-starter-config' implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' compileOnly 'org.projectlombok:lombok:1.18.22' annotationProcessor 'org.projectlombok:lombok:1.18.22' // Для работы с шифрованием по ГОСТ. implementation files('jcr/lib/jcrypto-jca-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-ocsp-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-pkcs11-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-pkcs7-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-smime-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-ssl-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-widgets-fx-2.8.6-R42.jar') implementation files('jcr/lib/jcrypto-xmldsig-2.8.6-R42.jar') } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } tasks.named('test') { useJUnitPlatform() }
Заполним наш application.yml:
server: port: 8081 spring: application: name: esia habr: esia: scope: fullname birthdate gender snils id_doc email contacts kid_fullname kid_birthdate kid_gender kid_snils kid_inn kid_birth_cert_doc kid_medical_doc clientId: CLIENT-ID ketStorageDirectory: /home/esia/eiis_keys ketStoragePassword: password ketFile: pkiClient-container host: esia-portal1.test.gosuslugi.ru baseUrl: https://esia-portal1.test.gosuslugi.ru/ authCodeUlr: https://esia-portal1.test.gosuslugi.ru/aas/oauth2/ac redirectUrl: http://127.0.01:8081
Настроим конфигурацию веб-клиента:
@Configuration public class WebClientConfiguration { @Value("${habr.esia.baseUrl}") private String baseUrl; public static final int TIMEOUT = 1000; @Bean public WebClient webClientWithTimeout() { final TcpClient tcpClient = TcpClient .create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TIMEOUT) .doOnConnected(connection -> { connection.addHandlerLast(new ReadTimeoutHandler(TIMEOUT, TimeUnit.MILLISECONDS)); connection.addHandlerLast(new WriteTimeoutHandler(TIMEOUT, TimeUnit.MILLISECONDS)); }); return WebClient.builder() .baseUrl(baseUrl) .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .build(); } }
Так как мы будем работать с jwt токенами, создадим класс JwtUtil:
@Service @Slf4j public class JwtUtil { private ObjectMapper mapper; @PostConstruct public void init() { this.mapper = new ObjectMapper(); } public Map<String, Object> getTokenData(String token) { token = this.withoutBearerToken(token); String[] parts = token.split("\\."); String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); Map<String, Object> jsonMap = new HashMap<String, Object>(); try { // convert JSON string to Map jsonMap = this.mapper.readValue(payload, new TypeReference<Map<String, Object>>() { }); } catch (Exception ex) { log.error("Ошибка парсинга токена: {}", ex.getMessage()); throw new RuntimeException(ex); } return jsonMap; } public String getUserOid(String token) { return String.valueOf(getTokenData(token).get("urn:esia:sbj_id")); } public String withoutBearerToken(String token) { if (token.startsWith("Bearer ")) { return token.substring(7); } return token; } public String withBearerToken(String token) { if (!token.startsWith("Bearer ")) { return String.format("Bearer %s", token); } return token; } }
2. Реализация сервиса авторизации
Общие принципы
Данная модель контроля доступа используется в случаях, когда система-клиент при доступе к ресурсу должна получить разрешение на это действие со стороны владельца ресурса.
В общем виде схема взаимодействия выглядит следующим образом:
-
Система-клиент запрашивает у владельца ресурса разрешение на доступ к соответствующим ресурсам. Обычно этот запрос осуществляется не напрямую к владельцу ресурса, а через сервис авторизации (который, в свою очередь, запрашивает разрешение у владельца ресурса), поскольку сам владелец ресурса не может выдать ни маркер доступа, ни авторизационный код.
-
Система-клиент получает разрешение на доступ (authorization grant) в виде авторизационного кода.
-
Система-клиент запрашивает маркер доступа, предъявив авторизационный код сервису авторизации.
-
Сервис авторизации аутентифицирует систему-клиента, проверяет авторизационный код и выдает маркер доступа и маркер обновления.
-
Система-клиент запрашивает у поставщика защищенный ресурс, предъявляя маркер доступа.
-
Поставщик ресурса проверяет маркер доступа. Если он валиден, то разрешает доступ к защищенному ресурсу.
-
Система-клиент вновь запрашивает с помощью выданного ранее маркера доступ к защищенному ресурсу.
-
Поставщик ресурса проверяет маркер, обнаруживает, что срок его действия истек, возвращает сообщение об ошибке.
-
Система-клиент обращается к сервису авторизации за получением нового маркера доступа, предъявляя маркер обновления.
-
Сервис авторизации проверяет валидность маркера обновления и возвращает два новых маркера: доступы и обновления.
После того, как система-клиент получила маркер доступа, она может неоднократно обращаться за получением защищенного ресурса до конца срока действия этого маркера. Когда это произойдет, системе-клиенту потребуется получить новый маркер доступа.

Метод получения авторизационного кода по определенному URL
Чтобы получить авторизационный код, система-клиент должна получить разрешение на доступ к защищенному ресурсу со стороны его владельца. Если владельцем является пользователь ЕСИА, система-клиент должна направить пользователя на страницу предоставления прав доступа в ЕСИА (пользователь должен быть предварительно аутентифицирован в системе, или она попросит его пройти идентификацию и аутентификацию).
public String getUrl(ServerRequest request) throws EsiaException, UnsupportedEncodingException { ClientSecretResponse clientSecretResponse = this.getClientSecret(); String type = request.pathVariable("redirect"); String timestampUrlEncoded = getTimestampUrlEncoded(clientSecretResponse); String redirectUrlEncoded = getRedirectUrlEncoded(String.format("http://%s:%s/%s", request.uri().getHost(), request.uri().getPort(), type)); UriComponentsBuilder accessTokenRequestBuilder = UriComponentsBuilder.fromHttpUrl(this.authCodeUlr) .queryParam("client_id", URLEncoder.encode(clientId, StandardCharsets.UTF_8.toString())) .queryParam("response_type", URLEncoder.encode("code", StandardCharsets.UTF_8.toString())) .queryParam("access_type", URLEncoder.encode("offline", StandardCharsets.UTF_8.toString())) .queryParam("scope", URLEncoder.encode(scope, StandardCharsets.UTF_8.toString())) .queryParam("state", URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())) .queryParam("client_secret", URLEncoder.encode(clientSecretResponse.getClient_secret(), StandardCharsets.UTF_8.toString())); String url = accessTokenRequestBuilder.toUriString(); url += "×tamp=" + timestampUrlEncoded; url += "&redirect_uri=" + redirectUrlEncoded; return url; }
Эта ссылка содержит:
-
<client_id> – идентификатор системы-клиента (мнемоника системы в ЕСИА, указанная прописными буквами);
-
<client_secret> – подпись запроса в формате PKCS#7 detached signature
в кодировке UTF-8 от значений четырех параметров HTTP-запроса: scope, timestamp, clientId, state (без разделителей). <client_secret> должен быть закодирован в формате base64 url safe. Используемый для проверки подписи сертификат должен быть предварительно зарегистрирован в ЕСИА и привязан к УЗ системы-клиента в ЕСИА. ЕСИА использует сертификаты в формате X.509 и взаимодействует с алгоритмами формирования электронной подписи ГОСТ Р 34.10-2012 и криптографического хэширования ГОСТ Р 34.11-2012; -
<redirect_uri> – ссылка, по которой должен быть направлен пользователь
после того, как даст разрешение на доступ к ресурсу; -
<scope> – область доступа, т.е. запрашиваемые права; например, если система-клиент запрашивает доступ к сведениям о сотрудниках организации,
то область доступа (scope) должна иметь значение http://esia.gosuslugi.ru/org_emps (с необходимыми параметрами);
если запрашивается область доступа (scope) id_doc (данные о пользователе), то не нужно в качестве параметра указывать oid этого пользователя; -
<response_type> – это тип ответа, который ожидается от ЕСИА, имеет значение code, если система-клиент должна получить авторизационный код;
-
<state> – набор случайных символов, имеющий вид 128-битного идентификатора запроса (необходимо для защиты от перехвата), генерируется по стандарту UUID;
-
<timestamp> – время запроса авторизационного кода в формате yyyy.MM.dd HH:mm:ss Z (например, 2013.01.25 14:36:11 +0400), необходимое для фиксации начала временного промежутка, в течение которого будет валиден запрос
с данным идентификатором (<state>); -
<access_type> – принимает значение «offline», если требуется иметь доступ
к ресурсам и тогда, когда владелец не может быть вызван (в этом случае выпускается маркер обновления); значение «online» – доступ требуется только при наличии владельца.
Когда авторизационный код получен, система-клиент может сформировать запрос методом POST на https-адрес ЕСИА для получения маркера доступа. Один авторизационный код можно обменять на один маркер доступа.
public Mono<LinkedHashMap> openEsiaSession(String code, String state, ServerRequest request) throws EsiaException, IOException { ClientSecretResponse clientSecretResponse = this.getClientSecret(); String timestampUrlEncoded = getTimestampUrlEncoded(clientSecretResponse); String redirectUrlEncoded = getRedirectUrlEncoded(String.format("http://%s:%s", request.uri().getHost(), request.uri().getPort())); StringBuilder formData = new StringBuilder("&"); formData.append(URLEncoder.encode("grant_type", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode("authorization_code", StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("client_id", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("code", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(code, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("state", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("token_type", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode("Bearer", StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("scope", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("refresh_token", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("client_secret", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getClient_secret(), StandardCharsets.UTF_8.toString())).append("&"); formData.append("timestamp").append("=").append(timestampUrlEncoded).append("&"); formData.append(URLEncoder.encode("redirect_uri", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(redirectUrlEncoded, StandardCharsets.UTF_8.toString())); return webClient .post() .uri(uriBuilder -> uriBuilder.host(esiaHost) .path("/aas/oauth2/te") .build() ) .header("Content-Type", "application/x-www-form-urlencoded") .bodyValue(formData.toString()) .retrieve() .bodyToMono(LinkedHashMap.class) .timeout(Duration.ofMillis(30000)) .onErrorResume(e -> { LinkedHashMap<String, Object> errorMap = new LinkedHashMap<>(); errorMap.put("error", e.getMessage()); return Mono.just(errorMap); }) .doOnError(error -> { log.error("An error has occurred {}", error.getMessage()); throw new RuntimeException(); }); }
В тело запроса должны быть включены следующие сведения:
-
<client_id> – идентификатор системы-клиента (мнемоника системы в ЕСИА, указанная прописными буквами);
-
<code> – значение авторизационного кода, который был ранее получен
от ЕСИА, его необходимо обменять на маркер доступа; -
<grant_type> – принимает значение «authorization_code»,
если авторизационный код обменивается на маркер доступа; -
<client_secret> – подпись запроса в формате PKCS#7 detached signature
в кодировке UTF-8 от значений четырех параметров HTTP-запроса: scope, timestamp, clientId, state (без разделителей). <client_secret> должен быть закодирован в формате base64 url safe. Используемый для проверки подписи сертификат должен быть предварительно зарегистрирован в ЕСИА и привязан к УЗ системы-клиента в ЕСИА. ЕСИА использует сертификаты в формате X.509 и взаимодействует с алгоритмами формирования электронной подписи ГОСТ Р 34.10-2012
и криптографического хэширования ГОСТ Р 34.11-2012; -
<state> – набор случайных символов, имеющий вид 128-битного идентификатора запроса (необходимо для защиты от перехвата), генерируется по стандарту UUID; этот набор символов должен отличаться от того, который использовался при получении авторизационного кода;
-
<redirect_uri> – ссылка, по которой должен быть направлен пользователь
после того, как даст разрешение на доступ (то же самое значение, которое было указано в запросе на получение авторизационного кода); -
<scope> – область доступа, т.е. запрашиваемые права (то же самое значение, которое было указано в запросе на получение авторизационного кода);
-
<scope_org> – область доступа, т.е. запрашиваемые права для юридических лиц (то же самое значение, которое было указано в запросе на получение авторизационного кода);
-
<timestamp> – время запроса маркера в формате yyyy.MM.dd HH:mm:ss Z (например, 2013.01.25 14:36:11 +0400), необходимое для фиксации начала временного промежутка, в течение которого будет валиден запрос с данным идентификатором (<state>);
-
<token_type> – тип запрашиваемого маркера, в настоящее время ЕСИА поддерживает только значение «Bearer». Параметр необязательный.
Если запрос успешно прошел проверку, то ЕСИА возвращает ответ в формате JSON:
-
<access_token> – маркер доступа для данного ресурса;
-
<expires_in> – время, в течение которого истекает срок действия маркера
(в секундах); -
<state> – набор случайных символов, имеющий вид 128-битного идентификатора запроса, генерируется по стандарту UUID (совпадает
с идентификатором запроса); -
<token_type> – тип предоставленного маркера, в настоящее время ЕСИА поддерживает только значение «Bearer»;
<refresh_token> – маркер обновления для данного ресурса.
Генерируем client_secret
public ClientSecretResponse getClientSecret() throws EsiaException { try { ZonedDateTime now = ZonedDateTime.now(); String timestamp = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx").format(now); String state = UUID.randomUUID().toString(); String msg = String.format("%s%s%s%s", scope, timestamp, clientId, state); byte[] messageAsByte = msg.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream clientSecretOS = new ByteArrayOutputStream(); try ( CMSSignedDataOutputStream signedStream = new CMSSignedDataOutputStream(clientSecretOS)) { signedStream.addCertificates(certificate); signedStream.addSigner(privateKey, certificate); signedStream.write(messageAsByte, 0, messageAsByte.length); } byte[] utf = clientSecretOS.toByteArray(); String clientSecret = new String(Base64.getEncoder().encode(utf)); String clientSecretUrlEncoded = clientSecret.replace("+", "-") .replace("/", "_") .replace("=", ""); log.debug("Generated new clientSecret:" + clientSecretUrlEncoded); return new ClientSecretResponse(timestamp, state, scope, clientSecretUrlEncoded); } catch (Exception error) { throw new EsiaException(error); } }
Если в ходе авторизации не возникло ошибок, то ЕСИА осуществляет перенаправление пользователя по ссылке, указанной в redirect_uri, а также возвращает два обязательных параметра:
-
<code> – значение авторизационного кода;
-
<state> – значение параметра state, который был получен в запросе
на авторизацию; система-клиент должна провести сравнение отправленного
и полученного параметра state.
В случае ошибки сервис авторизации вернет в параметре error код ошибки (например, «access_denied») и перенаправит пользователя по адресу, указанному
в redirect_uri.
Когда авторизационный код получен, система-клиент может сформировать запрос методом POST на https-адрес ЕСИА для получения маркера доступа. Один авторизационный код можно обменять на один маркер доступа. В тело запроса должны быть включены следующие сведения:
-
<client_id> – идентификатор системы-клиента (мнемоника системы в ЕСИА указанная прописными буквами);
-
<code> – значение авторизационного кода, который был ранее получен
от ЕСИА и который необходимо обменять на маркер доступа; -
<grant_type> – принимает значение «authorization_code»,
если авторизационный код обменивается на маркер доступа; -
<client_secret> – подпись запроса в формате PKCS#7 detached signature
в кодировке UTF-8 от значений четырех параметров HTTP-запроса: scope, timestamp, clientId, state (без разделителей). <client_secret> должен быть закодирован в формате base64 url safe. Используемый для проверки подписи сертификат должен быть предварительно зарегистрирован в ЕСИА и привязан к УЗ системы-клиента в ЕСИА. ЕСИА использует сертификаты в формате X.509 и взаимодействует с алгоритмами формирования электронной подписи ГОСТ Р 34.10-2012
и криптографического хэширования ГОСТ Р 34.11-2012; -
<state> – набор случайных символов, имеющий вид 128-битного идентификатора запроса (необходимо для защиты от перехвата), генерируется по стандарту UUID; этот набор символов должен отличаться от того, который использовался при получении авторизационного кода;
-
<redirect_uri> – ссылка, по которой должен быть направлен пользователь
после того, как даст разрешение на доступ (то же самое значение, которое было указано в запросе на получение авторизационного кода); -
<scope> – область доступа, т.е. запрашиваемые права (то же самое значение, которое было указано в запросе на получение авторизационного кода);
-
<scope_org> – область доступа, т.е. запрашиваемые права для юридических лиц (то же самое значение, которое было указано в запросе на получение авторизационного кода);
-
<timestamp> – время запроса маркера в формате yyyy.MM.dd HH:mm:ss Z (например, 2013.01.25 14:36:11 +0400), необходимое для фиксации начала временного промежутка, в течение которого будет валиден запрос с данным идентификатором (<state>);
-
<token_type> – тип запрашиваемого маркера, в настоящее время ЕСИА поддерживает только значение «Bearer». Параметр необязательный.
public Mono<LinkedHashMap> openEsiaSession(String code, String state, ServerRequest request) throws EsiaException, IOException { ClientSecretResponse clientSecretResponse = this.getClientSecret(); String timestampUrlEncoded = getTimestampUrlEncoded(clientSecretResponse); String redirectUrlEncoded = getRedirectUrlEncoded(String.format("http://%s:%s", request.uri().getHost(), request.uri().getPort())); StringBuilder formData = new StringBuilder("&"); formData.append(URLEncoder.encode("grant_type", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode("authorization_code", StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("client_id", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("code", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(code, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("state", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("token_type", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode("Bearer", StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("scope", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("refresh_token", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("client_secret", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getClient_secret(), StandardCharsets.UTF_8.toString())).append("&"); formData.append("timestamp").append("=").append(timestampUrlEncoded).append("&"); formData.append(URLEncoder.encode("redirect_uri", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(redirectUrlEncoded, StandardCharsets.UTF_8.toString())); return webClient .post() .uri(uriBuilder -> uriBuilder.host(esiaHost) .path("/aas/oauth2/te") .build() ) .header("Content-Type", "application/x-www-form-urlencoded") .bodyValue(formData.toString()) .retrieve() .bodyToMono(LinkedHashMap.class) .timeout(Duration.ofMillis(30000)) .onErrorResume(e -> { LinkedHashMap<String, Object> errorMap = new LinkedHashMap<>(); errorMap.put("error", e.getMessage()); return Mono.just(errorMap); }) .doOnError(error -> { log.error("An error has occurred {}", error.getMessage()); throw new RuntimeException(); }); }
Если запрос успешно прошел проверку, то ЕСИА возвращает ответ в формате JSON:
-
<access_token> – маркер доступа для данного ресурса;
-
<expires_in> – время, в течение которого истекает срок действия маркера
(в секундах); -
<state> – набор случайных символов, имеющий вид 128-битного идентификатора запроса, генерируется по стандарту UUID (совпадает
с идентификатором запроса); -
<token_type> – тип предоставленного маркера, в настоящее время ЕСИА поддерживает только значение «Bearer»;
-
<refresh_token> – маркер обновления для данного ресурса.
При использовании маркера доступа системам-клиентам рекомендуется сначала проверять, не истек ли срок его действия. Если маркер просрочен, то для успешного доступа к защищенному ресурсу потребуется предварительно получить новый маркер доступа с использованием маркера обновления. Для этого системе-клиенту следует сформировать запрос методом POST в адрес ЕСИА, имеющий структуру, аналогичную первичному запросу на получение маркера. Особенности значений параметров запроса:
-
<refresh_token> – значение имеющегося у системы-клиента маркера обновления, который следует обменять на новый маркер доступа (указывается вместо <code>)
-
<grant_type> – должно иметь значение «refresh_token», поскольку маркер обновления обменивается на маркер доступа
public Mono<LinkedHashMap<String, Object>> updateEsiaSession(String refreshToken, ParameterizedTypeReference<LinkedHashMap<String, Object>> typeReference, ServerRequest request) throws EsiaException, IOException { ClientSecretResponse clientSecretResponse = this.getClientSecret(); String timestampUrlEncoded = getTimestampUrlEncoded(clientSecretResponse); String redirectUrlEncoded = getRedirectUrlEncoded(String.format("http://%s:%s", request.uri().getHost(), request.uri().getPort())); StringBuilder formData = new StringBuilder("&"); formData.append(URLEncoder.encode("client_id", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("client_secret", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getClient_secret(), StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("refresh_token", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(refreshToken, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("scope", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("grant_type", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode("refresh_token", StandardCharsets.UTF_8.toString())).append("&"); formData.append(URLEncoder.encode("state", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(clientSecretResponse.getState(), StandardCharsets.UTF_8.toString())).append("&"); formData.append("timestamp").append("=").append(timestampUrlEncoded).append("&"); formData.append(URLEncoder.encode("redirect_uri", StandardCharsets.UTF_8.toString())).append("=").append(URLEncoder.encode(redirectUrlEncoded, StandardCharsets.UTF_8.toString())); return webClient .post() .uri(uriBuilder -> uriBuilder.host(esiaHost) .path("/aas/oauth2/te") .build() ) .header("Content-Type", "application/x-www-form-urlencoded") .bodyValue(formData.toString()) .retrieve() .bodyToMono(typeReference) .timeout(Duration.ofMillis(30000)) .onErrorResume(e -> { LinkedHashMap<String, Object> errorMap = new LinkedHashMap<>(); errorMap.put("error", e.getMessage()); return Mono.just(errorMap); }) .doOnError(error -> { log.error("An error has occurred {}", error.getMessage()); throw new RuntimeException(); }); }
Ответ на этот запрос дается в формате JSON и имеет ту же структуру, как и при первичном предоставлении маркера доступа. В этом ответе содержится новый маркер обновления, который система-клиент должна хранить вместо уже использованного маркера обновления.
Весь код данной статьи можно найти по ссылке на GitHub: https://github.com/saetdin/esia/tree/main
ссылка на оригинал статьи https://habr.com/ru/articles/771320/
Добавить комментарий