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

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/security
	http://www.springframework.org/schema/security/spring-security.xsd">

    <http auto-config="true">
        <intercept-url pattern="/rest/**" access="ROLE_USER" />
        <logout/>
    </http>

    <authentication-manager>
        <authentication-provider>
            <user-service >
                <user name="user" password="pass" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

Это наш рест контроллер:

@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=="); //here is some login and pass like this login:pass
        HttpEntity<String> request = new HttpEntity<String>(headers);
        MyClass myclass = restTemplate.exchange(url, HttpMethod.GET, request, MyClass.class).getBody();

Способ второй: Digest authentication


Digest authentication — Это почти тоже самое что первый метод, только логин и пароль передаются в зашифрованном виде, а не как обычный текст. Логин и пароль шифруются MD5 алгоритмом и его достаточно сложно расшифровать. При этом подходе можно использовать незащищенное http соединение и не боятся что пароль будет перехвачен злоумышленниками. Реализация серверной части остается такой же. Надо немного поменять клиент, чтобы он умел отсылать пароль в зашифрованном виде. Тут нам на помощь придет http Apache client.
Чтобы не заниматься копипастом я приведу ссылку на проект на Гитхабе с реализацией такого клиента.

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


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

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

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/security
	http://www.springframework.org/schema/security/spring-security.xsd">

    <beans:bean id="restAuthenticationEntryPoint" class="com.example.rest.security.RestAuthenticationEntryPoint"/>

    <http  pattern="/rest/**" entry-point-ref="restAuthenticationEntryPoint" use-expressions="true" auto-config="false" create-session="stateless" >
        <custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER"   />
        <intercept-url pattern="/rest/**" access="isAuthenticated()" />
        <logout />
    </http>

    <beans:bean class="com.example.rest.security.CustomTokenAuthenticationFilter" id="authenticationTokenProcessingFilter">
        <beans:constructor-arg type="java.lang.String" value="/rest/**"/>
        <beans:constructor-arg
                type="org.springframework.security.authentication.AuthenticationManager" ref="authManager">
        </beans:constructor-arg>
    </beans:bean>

    <http auto-config="true">
        <intercept-url pattern="/token/**" access="ROLE_USER" />
    </http>

    <authentication-manager alias="authManager" erase-credentials="false">
        <authentication-provider>
            <user-service >
                <user name="user" password="pass" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

Бин 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 — те которые вы генерите сами и их надо добавлять вашему рест сервису в исключения, чтобы он знал о их существовании и что им можно доверять

Ниже приведено несколько шагов, как создать Self signed сертификат с помощью утилиты keytool.

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
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
       maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
       keystoreFile="${catalina.home}/conf/cert/keystore_server" keystorePass="changeit"
       truststoreFile="${catalina.home}/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 для вашего проекта четко решите, что вам надо, взвесьте все за и против. Не стоит усложнять систему, если этого не требует проект.

Надеюсь, Ваши приложения будут безопасными и надёжными.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 16

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

    а по https:// обратиться разве не проще?
      –3
      Проще, но https (точнее у протоколов, которые он использует, SSL или TLS) есть свои уязвимости, позволяющие совершать на него атаки типа man-in-the-middle. Особенно в случае если используется самоподписанные сертификаты. Подробнее про атаки на SSL или TLS можно посмотреть хотя бы в википедии. В любом случае полезно знать разные варианты реализации безопасности в приложении, поэтому эту статья для меня очень интересно было прочитать, в первый раз вижу что все варианты разложены по полочкам.
        +1
        Если вы внимательно посмотрите на описание пятого способа, поймете, что это и есть TLS.
        0
        Да так лучше (я про генерацию ключа на стороне клиента), но надо не забывать про атаку Man in the middle. При приеме открытого ключа, обязательно надо убедиться что он действительно от владельца.
        0
        зачем это в способе три? «На стороне сервера фильтр получает значение из этого хидера, расшифровывает токен, парсит его или разберает на составляющие и решает давать или нет доступ клиенту»
        разве не лучше будет использовать токен от HTTP-сессии веб-сервера и брать данные на стороне сервера из сессии?
          +1
          Чем лучше? :)
          Вариант с зашифрованным токеном и данными авторизации в куках/хидере выигрывает в его stateless — не нужен synchronized cache/backing store — меньше кода, и нагрузка на сервер, проще система сама по себе.
          +2
          MD5 уже не является надежным алгоритмом хеширования и рекомендуется к шифрованию паролей. Пруфы гуглятся по «decrypt md5 opencl»
            0
            Я так и написал
            Логин и пароль шифруются MD5 алгоритмом и его достаточно сложно расшифровать.
            но это не значит, что невозможно. Это понятно что брутфорсом подобрать можно, но не так быстро. Время зависит от сложности пароля.
            Пруфы гуглятся по «decrypt md5 opencl»
            — гуглятся только те, что есть в базе.
            Я не утверждаю что этот метод самый надежный, скорее наоборот, но он есть и возможно кому-то пригодится.
              0
              Брутфорсом очень быстро сейчас подобрать хеш md5. Даже одна видеокарта перебирает миллиард хешей за секунду.
            0
            А почему в п.4 вы поменяли направление общения и у вас уже сервис обращается к клиенту? Нужен скорее обратный пример. И мд5 — это не шифрование.
              0
              Уточню: общение в пункте №4 инициируется всё-таки клиентом, но кто этот клиент, имеет ли он право совершать операцию — в этом сценарии это никого почему-то не интересует. Зато сервер шифрует свой ответ о_О
                0
                Суть состоит в том что любой может обратится к рест сервису и получить беспорядочный набор символов, а точнее шифрованный ответ от сервера и только владелец приватного ключа сможет его расшифровать.

                Любой клиент может совершать операцию, но расшифровать может только один
                0
                В четвертом примере конечно же надо использовать не MD5, а к примеру javax.crypto пакет, MD5 только для Digest.
                +1
                В качестве общего образования пойдет.
                Но на практике, все что сложнее второго пункта, обычно заменяется на SSO, что-то вроде Jasig CAS.
                  0
                  В наших проектах применялся пятый и шестой, так же. Четвертый, тоже имеет право на жизнь. Третий немного надуманный и в практике, на реальных проектах его не встречал. Но как я уже написал в конце, это лишь стартовая точка, чтобы натолкнуть читателей на собственные решения или выбрать из существующих.
                    0
                    Да, спасибо, что напомнили про SSO. Можно еще использовать OpenAm или Spring CAS. Это номер 7.

                  Only users with full accounts can post comments. Log in, please.