[Из песочницы] 6 способов: как добавить security для Rest сервиса в Java
В данной статье я попытаюсь описать несколько способов, а точнее 6, как добавить security для rest сервиса на Java.Перед нашей командой была поставлена задача найти все возможные способы добавить security к rest сервису. Проанализировать все за и против и выбрать наиболее подходящий для нашего проекта. Когда я начал искать такую статью в Гугле ничего подходящего не нашел, а были лишь фрагменты и мне пришлось собирать эту информацию по крупицам. Так что думаю, данная статья будет полезна и другим Java разработчикам, пишущим back-end. Я не буду утверждать, что какой-то из этих способов лучше или хуже, все зависит от поставленной задачи и конкретного проекта. Поэтому какой из шести способов подходит больше всего вашему проекту решать только Вам. Я постараюсь описать принцип каждого из подходов и дать небольшой пример с использованием Java и Spring Security.
Способ первый: Basic AuthenticationBasic 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==»); //here is some login and pass like this login: pass
HttpEntity
Бин 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) Идея этого подхода заключается в использовании криптосистемы с открытым ключом. Суть состоит в том что любой может обратится к рест сервису и получить беспорядочный набор символов, а точнее шифрованный ответ от сервера и только владелец приватного ключа сможет его расшифровать.И так по порядку.Когда регестрируется новый пользователь на сервере генерируется пара ключей для этого пользователя — публичный и приватный Приватный отсылается пользователю и только он сможет расшифровать сообщение (ключ должен отправляться по безопасному каналу, чтобы никто не мог его перехватить) При каждом рест запросе клиент передает свой логин, чтобы сервис мог зашифровать сообщение нужным публичным ключом Сервис шифрует и отправляет сообщение Клиент принимает его и расшифровывает своим ключом Для реализации этого подхода надо написать два фильтра: один на серверной стороне, другой на клиентской. Фильтр на стороне рест сервера будет шифровать риспонс используя ключ клиента, который совершил запрос. Фильтр рест клиента будет расшифровывать риспонс от рест сервиса используя свой приватный ключ.Еще более безопасным можно сделать этот подход если генерировать пару ключей на клиентской стороне с использование javascript библиотек таких как forge. Такой подход позволяет вообще не пересылать приватный ключ по сети, а сразу генерировать на клиентской стороне, что значительно уменьшает риск скомпрометировать этот ключ. Публичный ключ отправляется серверу для дальнейшего использования при шифровании сообщений. Канал для отправки может быть незащищенным, так как нет ничего страшного если публичный ключ будет перехвачен (детали смотри по ссылке выше криптосистемы с открытым ключом).
Способ пятый: Certificate Authentication Вы можете настроить свой сервер таким образом, что если клиент при запросе не предоставляет нужный сертификат ответ от сервера он не получит, точнее получит ответ, что сертификат отсутствует или не подходит. Подробнее про сертификаты можно почитать здесь. Сертификаты бывают двух типов: Trusted — те который может проверить каждый и они зарегистрированы в едином сертификацонном центре Self signed — те которые вы генерите сами и их надо добавлять вашему рест сервису в исключения, чтобы он знал о их существовании и что им можно доверять Ниже приведено несколько шагов, как создать Self signed сертификат с помощью утилиты keytool.generate client and server keyskeytool -genkey -keystore keystore_client -alias clientKeykeytool -genkey -keystore keystore_server -alias serverKey
generate client and server certificateskeytool -export -alias clientKey -rfc -keystore keystore_client > client.certkeytool -export -alias serverKey -rfc -keystore keystore_server > server.cert
import certificates to corresponding truststoreskeytool -import -alias clientCert -file client.cert -keystore truststore_serverkeytool -import -alias serverCert -file server.cert -keystore truststore_client
Теперь полученные сертификаты надо добавить в конфигурацию нашего сервера. В данном, случае используется Tomcat
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 для вашего проекта четко решите, что вам надо, взвесьте все за и против. Не стоит усложнять систему, если этого не требует проект.
Надеюсь, Ваши приложения будут безопасными и надёжными.