RESTful-аутентификация через Spring

262

Проблема: у
нас есть RESTful API на основе Spring MVC, который содержит конфиденциальную информацию. API должен быть защищен, однако отправка учетных данных пользователя (user / pass combo) с каждым запросом нежелательна. В соответствии с рекомендациями REST (и внутренними бизнес-требованиями) сервер должен оставаться без состояния. API будет использоваться другим сервером в стиле mashup.

Требования:

  • Клиент делает запрос .../authenticate(незащищенный URL) с учетными данными; Сервер возвращает безопасный токен, который содержит достаточно информации, чтобы сервер мог проверить будущие запросы и остаться без состояния. Скорее всего, он будет содержать ту же информацию, что и токен Spring Security « Remember-Me» .

  • Клиент делает последующие запросы к различным (защищенным) URL-адресам, добавляя ранее полученный токен в качестве параметра запроса (или, менее желательно, заголовок HTTP-запроса).

  • Нельзя ожидать, что клиент будет хранить куки.

  • Поскольку мы уже используем Spring, решение должно использовать Spring Security.

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

Учитывая приведенный выше сценарий, как вы можете решить эту конкретную потребность?

Крис Кэшвелл
источник
49
Привет Крис, я не уверен, что передача этого токена в параметре запроса - лучшая идея. Это будет отображаться в журналах, независимо от HTTPS или HTTP. Заголовки, вероятно, безопаснее. Просто к вашему сведению. Отличный вопрос. +1
jmort253
1
Каково ваше понимание безгражданства? Ваше требование к токену вступает в противоречие с моим пониманием безгражданства. Ответ аутентификации Http кажется мне единственной реализацией без сохранения состояния.
Маркус Малкуш
9
@MarkusMalkusch без сохранения состояния относится к знанию сервера о предыдущих связях с данным клиентом. HTTP по определению не имеет состояния, а сессионные куки делают его состоянием. Время жизни (и источник, в этом отношении) токена не имеет значения; сервер заботится только о том, что он действителен и может быть привязан к пользователю (НЕ сеанс). Следовательно, передача идентификационного токена не мешает сохранению состояния.
Крис Кэшвелл
1
@ChrisCashwell Как вы гарантируете, что токен не был подделан / сгенерирован клиентом? Используете ли вы секретный ключ на стороне сервера для шифрования токена, предоставления его клиенту, а затем используете этот же ключ для расшифровки его во время будущих запросов? Очевидно, что Base64 или какое-либо другое запутывание было бы недостаточно. Можете ли вы рассказать о методах «проверки» этих токенов?
Крейг Отис
6
Хотя это и устарело, и я не трогал и не обновлял код более 2 лет, я создал Gist для дальнейшего расширения этих концепций. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Крис Кэшвелл

Ответы:

190

Нам удалось заставить это работать точно так, как описано в OP, и, надеюсь, кто-то другой может использовать решение. Вот что мы сделали:

Установите контекст безопасности следующим образом:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

Как видите, мы создали кастом AuthenticationEntryPoint, который в основном просто возвращает a, 401 Unauthorizedесли наш запрос не был аутентифицирован в цепочке фильтров AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

Очевидно, TokenUtilsсодержит некоторый секретный (и очень специфичный для конкретного случая) код и не может быть легко распространен. Вот его интерфейс:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Это должно дать вам хорошее начало. Удачного кодирования. :)

Крис Кэшвелл
источник
Нужно ли аутентифицировать токен при отправке токена вместе с запросом. Как насчет получения информации об имени пользователя напрямую и установки в текущем контексте / запросе?
Фишер
1
@ Весна Я не храню их нигде ... вся идея токена в том, что его нужно передавать с каждым запросом, и его можно деконструировать (частично), чтобы определить его достоверность (отсюда и validate(...)метод). Это важно, потому что я хочу, чтобы сервер оставался без сохранения состояния. Я полагаю, вы могли бы использовать этот подход без необходимости использования Spring.
Крис Кэшвелл
1
Если клиент является браузером, как можно сохранить токен? или вам нужно повторить аутентификацию для каждого запроса?
beginner_
2
отличные советы. @ChrisCashwell - часть, которую я не могу найти, - где вы проверяете учетные данные пользователя и отправляете обратно токен? Я думаю, это должно быть где-то в конце конечной точки / authenticate. я прав ? Если нет, то какова цель / аутентификация?
Йонатан Маман
3
что находится внутри AuthenticationManager?
MoienGK
25

Вы могли бы рассмотреть Дайджест-аутентификацию доступа . По сути, протокол выглядит следующим образом:

  1. Запрос сделан от клиента
  2. Сервер отвечает уникальной одноразовой строкой
  3. Клиент предоставляет имя пользователя и пароль (и некоторые другие значения), хэшированные md5 с одноразовым номером; этот хеш известен как HA1
  4. Затем сервер может проверить личность клиента и предоставить запрошенные материалы
  5. Связь с одноразовым номером может продолжаться до тех пор, пока сервер не предоставит новый одноразовый номер (счетчик используется для устранения атак воспроизведения)

Все это сообщение осуществляется через заголовки, которые, как указывает jmort253, в целом более безопасны, чем передача чувствительного материала в параметрах URL.

Дайджест-аутентификация доступа поддерживается Spring Security . Обратите внимание, что, хотя в документах говорится, что у вас должен быть доступ к текстовому паролю вашего клиента, вы можете успешно пройти аутентификацию, если у вас есть хэш HA1 для вашего клиента.

Тим Пот
источник
1
Хотя это и возможный подход, несколько циклов, которые необходимо выполнить для извлечения токена, делает его немного нежелательным.
Крис Кэшвелл
Если ваш клиент следует спецификации HTTP-аутентификации, то эти обходы происходят только при первом вызове и когда происходит 5.
Маркус Малкуш
5

Что касается токенов, несущих информацию, JSON Web Tokens ( http://jwt.io ) - блестящая технология. Основная идея состоит в том, чтобы встраивать информационные элементы (утверждения) в токен, а затем подписывать весь токен, чтобы проверяющая сторона могла убедиться, что утверждения действительно заслуживают доверия.

Я использую эту реализацию Java: https://bitbucket.org/b_c/jose4j/wiki/Home

Существует также модуль Spring (spring-security-jwt), но я не изучал, что он поддерживает.

Лейф Джон
источник