Авторизация для api java

6 способов: как добавить security для Rest сервиса в Java

В данной статье я попытаюсь описать несколько способов, а точнее 6, как добавить security для rest сервиса на Java.

Перед нашей командой была поставлена задача найти все возможные способы добавить security к rest сервису. Проанализировать все за и против и выбрать наиболее подходящий для нашего проекта. Когда я начал искать такую статью в Гугле ничего подходящего не нашел, а были лишь фрагменты и мне пришлось собирать эту информацию по крупицам. Так что думаю, данная статья будет полезна и другим Java разработчикам, пишущим back-end. Я не буду утверждать, что какой-то из этих способов лучше или хуже, все зависит от поставленной задачи и конкретного проекта. Поэтому какой из шести способов подходит больше всего вашему проекту решать только Вам. Я постараюсь описать принцип каждого из подходов и дать небольшой пример с использованием Java и Spring Security.

Способ первый: Basic Authentication

Basic Authentication — юзер или рест клиент указывает свой логин и пароль для получения доступа к рест сервису. Логин и пароль передаются по сети как незашифрованный текст кодированный простым Base64 и может быть легко декодирован любым пользователем. При использовании такого метода, обязательно должен использоваться https протокол для передачи данных.

Конфигурация очень простая, так будет выглядеть security.xml для нашего Spring Security

@RequestMapping("/rest/api") @RestController public class RestController < @RequestMapping public Object getInfo() < return //some response MyClass; >> 

И наконец рест-клиент на базе спринового RestTemplate. В хидер добавляем слово Basic пробел потом логин и пароль без пробелов, разделенный двоеточием и закодированный Base64.

 RestTemplate restTemplate = new RestTemplate(); String url = "http://localhost:8080/rest/api"; HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", "Basic QWxhZGRpbupvcRVuIHNlc2FtZQ= https://github.com/eugenp/tutorials/tree/master/spring-security-rest-digest-auth" rel="nofollow">такого клиента. 

Способ третий: Token Authentication


Суть этого способа заключается в том, что пользователь используя свои креденшелы логинится в приложение и получает токен для доступа к рест сервису. Доступ к сервису, который выдает токены должен обязательно быть осуществлен через https соединение, доступ к рест сервису можно сделать через обычный http. Токен должен содержать логин, пароль, так же может содержать expiration time и роли пользователя, а так же любую нужную для вашего приложения информацию. После того как токен готов и к примеру все его параметры разделены двоеточием или другим удобным для вас символом или сериализованы как json или xml объект его необходимо зашифровать, прежде чем отдать пользователю. Учтите, что только рест сервис должен знать как расшифровывать этот токен. После того как токен приходит на рест сервис он его расшифровывает и получает все необходимые данные для аутентификацияя и если надо авторизации рест клиента. Имплементация будет кардинально отличатся от предидущих двух.

Наш security.xml теперь будет выглядеть вот так:

                  

Бин RestAuthenticationEntryPoint будет выглядеть примерно так:

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint < @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException < response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); >> 

Фильтр CustomTokenAuthenticationFilter, который будет проверять валидность токена, права и тд. и в конечном счете решать, позволено ли данному клиенту работать с нашим рест сервисом или нет, будет выглядеть примерно так, но вы можете его реализовать по другому.

public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter < private AuthenticationManager authenticationManager; @Autowired private CryptService cryptService; //service which can decrypt token public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) < super(defaultFilterProcessesUrl); this.authenticationManager = authenticationManager; super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(new NoOpAuthenticationManager()); setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler()); >public final String HEADER_SECURITY_TOKEN = "My-Rest-Token"; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException < String token = request.getHeader(HEADER_SECURITY_TOKEN); Authentication userAuthenticationToken = parseToken(token); if (userAuthenticationToken == null) < throw new AuthenticationServiceException("here we throw some exception or text"); >return userAuthenticationToken; > @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException < super.successfulAuthentication(request, response, chain, authResult); chain.doFilter(request, response); >// This method makes some validation depend on your application logic private Authentication parseToken(String tokenString) < try < String encryptedToken = cryptService.decrypt(tokenString); Token token = new ObjectMapper().readValue(encryptedToken, Token.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(token.getUsername(), token.getPassword())); >catch (Exception e) < return null; >return null; > > 

Что мы имеем в итоге. Юзер логинится в приложение получает зашифрованный токен, который может использовать спринговый RestTemplate или другой рест клиент добавляя его в хидер, к примеру наш кастомный хидер My-Rest-Token. На стороне сервера фильтр получает значение из этого хидера, расшифровывает токен, парсит его или разберает на составляющие и решает давать или нет доступ клиенту.

Способ четвертый: Digital Signature (public/private key pair)

  1. Когда регестрируется новый пользователь на сервере генерируется пара ключей для этого пользователя — публичный и приватный
  2. Приватный отсылается пользователю и только он сможет расшифровать сообщение (ключ должен отправляться по безопасному каналу, чтобы никто не мог его перехватить)
  3. При каждом рест запросе клиент передает свой логин, чтобы сервис мог зашифровать сообщение нужным публичным ключом
  4. Сервис шифрует и отправляет сообщение
  5. Клиент принимает его и расшифровывает своим ключом

Еще более безопасным можно сделать этот подход если генерировать пару ключей на клиентской стороне с использование javascript библиотек таких как forge. Такой подход позволяет вообще не пересылать приватный ключ по сети, а сразу генерировать на клиентской стороне, что значительно уменьшает риск скомпрометировать этот ключ. Публичный ключ отправляется серверу для дальнейшего использования при шифровании сообщений. Канал для отправки может быть незащищенным, так как нет ничего страшного если публичный ключ будет перехвачен (детали смотри по ссылке выше криптосистемы с открытым ключом).

Способ пятый: Certificate Authentication

  • Trusted — те который может проверить каждый и они зарегистрированы в едином сертификацонном центре
  • Self signed — те которые вы генерите сами и их надо добавлять вашему рест сервису в исключения, чтобы он знал о их существовании и что им можно доверять

generate client and server keys
keytool -genkey -keystore keystore_client -alias clientKey
keytool -genkey -keystore keystore_server -alias serverKey

generate client and server certificates
keytool -export -alias clientKey -rfc -keystore keystore_client > client.cert
keytool -export -alias serverKey -rfc -keystore keystore_server > server.cert

import certificates to corresponding truststores
keytool -import -alias clientCert -file client.cert -keystore truststore_server
keytool -import -alias serverCert -file server.cert -keystore truststore_client

Теперь полученные сертификаты надо добавить в конфигурацию нашего сервера. В данном, случае используется Tomcat

/conf/cert/keystore_server" keystorePass="changeit" truststoreFile="$/conf/cert/truststore_server" truststorePass="changeit" clientAuth="true" sslProtocol="TLS" /> 

Ниже приведен рест клиент с использованием Http Apache Client, который способен предоставить сертификат рест сервису и осуществит все необходимые «рукопожатия» для получения ответа от сервера

public class CertificateAuthenticationServiceImpl implements CertificateAuthenticationService < private static final String keyStorePass = "changeit"; private static final String trustedStorePass = "changeit"; private static final File keyStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/keystore_client").getPath()); private static final File trustedStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/truststore_client").getPath()); private static final String certificateType = "jks"; public String httpGet(URL url) < String resp = null; try < final HttpParams httpParams = new BasicHttpParams(); final KeyStore keystore = KeyStore.getInstance(certificateType); keystore.load(new FileInputStream(keyStore), keyStorePass.toCharArray()); final KeyStore truststore = KeyStore.getInstance(certificateType); truststore.load(new FileInputStream(trustedStore), trustedStorePass.toCharArray()); final SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme(url.toURI().getScheme(), new SSLSocketFactory(keystore, keyStorePass, truststore), url.getPort())); final DefaultHttpClient httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, schemeRegistry), httpParams); try < HttpGet httpget = new HttpGet(url.toString()); CloseableHttpResponse response = httpClient.execute(httpget); try < HttpEntity entity = response.getEntity(); if (entity != null) < resp = EntityUtils.toString(entity); >EntityUtils.consume(entity); > finally < response.close(); >> finally < httpClient.close(); >> catch (Exception e) < throw new RuntimeException(e); >return resp; > > 

Способ шестой: OAuth2 authorization

Ну и на закуску я оставил самый сложны для понимания и реализации способ. Зато он очень гибкий и хорошо подходит для больших порталов. Опять же не буду заниматься копипастом, чтобы почитать, что такое OAuth и как он работает идем сюда.
Spring security предоставляет нам класс OAuthTemplate, который значительно облегчает нам жизнь.
Все идеи для реализации своей OAuth имплементации я почерпнул из этой замечательной статьи там даже есть рабочий проект, который можно скачать.

Заключение

Что ж, я надеюсь мне немного удалось прояснить общую картину и помочь Вам в реализации собственных проектов. Это конечно же не все способы защитить Ваш рест сервис, но есть решения на любой вкус. Примеры реализаций не являются общими решениями, а лишь приведены для полного понимания картины. Вы можете писать свои имплементации в зависимости от нужд проекта.

Перед выбором security для вашего проекта четко решите, что вам надо, взвесьте все за и против. Не стоит усложнять систему, если этого не требует проект.

Надеюсь, Ваши приложения будут безопасными и надёжными.

Источник

Реализация JWT в Spring Boot

Реализация JWT в Spring Boot

Теперь, когда мы знаем что такое JWT токен и как он устроен, пришло время использовать наши теоретические знания на практике.

Одно из преимуществ аутентификации с использованием JWT – это возможность выделить выдачу токенов и работу с данными пользователей в отдельное приложение, переложив механизм валидации токенов на клиентские приложения. Этот механизм отлично подходит микросервисной архитектуре.

Сначала сделаем приложение, которое будет совмещать в себе бизнес-логику и выдачу токенов. А чуть позже мы сделаем два приложения: одно будет отвечать за выдачу токенов, а другое содержать бизнес логику. Сервис с бизнес логикой не сможет выдавать токены, но сможет их валидировать. Таких приложений может быть много.

Давайте рассмотрим механизм аутентификации по шагам. Обычно в обучающих статьях опускают наличие refresh токена. Однако, refresh токен является важной частью в аутентификации с помощью JWT, поэтому мы рассмотрим и его. Вот как будет выглядеть механизм аутентификации:

  • Клиент API, чаще всего это фронт, присылает нам объект с логином и паролем.
  • Если пароль подходит, то мы генерируем access и refresh токены и отправляем их в ответ.
  • Клиент API использует access токен для взаимодействия с остальным нашим эндпойнтами нашего API.
  • Через пять минут, когда access токен протухает, фронт отправляет refresh токен и получает новый access токен. И так по кругу, пока не протухнет refresh токен.
  • Refresh токен выдается на 30 дней. Примерно на 25-29 день клиент API отправляет валидный refresh токен вместе с валидным access токеном и взамен получает новую пару токенов.

В этой статье будет чистый REST-сервис без фронтенда. Подразумевается, что front написан отдельно.

Используемые версии

Java: 17
SpringBoot: 2.7.0
jsonwebtoken: 0.11.5
jaxb-api: 2.3.1

История изменений

21.06.2022: Актуализировал версию спрингбута и перевел проект на 17 Java. Удалил все устаревшие классы и методы, заменив их на аналогичные современные варианты. Детализировал некоторые моменты в статье.

Создание сервера аутентификации

Если вы хотите повторять все действия из статьи, чтобы лучше понять что происходит. То для вас я собрал начальную конфигурацию приложения на сайте start.spring.io, вам остается только скачать ее, распаковать, и открыть с помощью IDEA.

После этого в pom.xml добавим зависимости. Они понадобятся нам для генерации JWT токенов.

После этого берем access токен и вcтавляем его на вкладке Authorization, выбрав тип "Bearer token".

Далее выполняем запрос и получаем заветный результат.

Также попробуем получить сообщение предназначенное только для админов.

Видим 403 ошибку доступа, все работает. Если подождать 5 минут, то и по эндпойнту /api/hello/user увидим ту же ошибку, так как access токен протух.

Чтобы получить новый access токен отправляем запрос на /api/auth/token . В теле запроса указываем наш refresh токен.

Все работает. А теперь взамен текущего refresh токена получим новые access и refresh токены. Для этого вызовем /api/auth/refresh и передадим наш текущий refresh токен в теле запроса. Также не стоит забывать, что это защищенный метод, поэтому во вкладке Authorization вставляем наш access токен.

Отлично, мы получили новую пару токенов. А что если попробовать снова вызвать этот же запрос?

Мы получим ошибку, так как этого токена больше нет в сохраненных.

На этом основная часть закончена. Мы реализовали JWT аутентификацию. Теперь вы запросто реализуете приложение клиент, если это необходимо. Нужно только оставить функционал проверки токенов и аутентификации, и убрать функционал по выдаче новых токенов. Для удобства я сделал одно небольшое приложение клиент для демонстрации.

Склонируйте его себе и запустите. Токены выдает наш сервер аутентификации, а приложение клиент может их использовать для доступа к своему API.

Резюмирую

В этой статье мы реализовали аутентифицкаицю в SpringBoot сервисе с использованием JWT.

Теперь вы знаете, что можно выдавать токены в одном сервисе, а в других сервисах их валидировать, тем самым уменьшая запросы к сервису аутентификации. Такой подход можно использовать в микросервисной архитектуре.

Источник

Читайте также:  Целые числа python максимальное значение
Оцените статью