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


262

Проблема: у
нас є джерело API RESTful на базі MVC, який містить конфіденційну інформацію. API повинен бути захищеним, однак надсилати облікові дані користувача (користувач / пропуск комбо) з кожним запитом не бажано. Згідно з рекомендаціями REST (та внутрішніми вимогами бізнесу), сервер повинен залишатися без стану. Інтерфейс API буде використовуватися іншим сервером у стилі мешанки.

Вимоги:

  • Клієнт робить запит на .../authenticate(незахищену URL-адресу) з обліковими записами; сервер повертає захищений маркер, який містить достатньо інформації для сервера для перевірки майбутніх запитів і залишається без стану. Це, ймовірно, складається з тієї ж інформації, що і Spring -пам’ятка Remember-Me .

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

  • Не можна очікувати, що клієнт зберігає файли cookie.

  • Оскільки ми вже використовуємо Spring, рішення має використовувати Spring Security.

Ми стукали головою об стіну, намагаючись зробити цю роботу, тому, сподіваємось, хтось там уже вирішив цю проблему.

Зважаючи на описаний вище сценарій, як ви могли вирішити цю конкретну потребу?


49
Привіт Кріс, я не впевнений, що передача цього маркера в параметр запиту - найкраща ідея. Це відображатиметься в журналах, незалежно від HTTPS чи HTTP. Заголовки, ймовірно, безпечніші. Просто FYI. Хоча велике питання. +1
jmort253

1
Яке ваше розуміння без громадянства? Ваша вимога лексеми суперечить моєму розумінню стану без громадянства. Відповідь Http аутентифікації здається мені єдиною реалізацією без стану.
Маркус Малкуш

9
@MarkusMalkusch без стану посилається на знання сервера про попередню комунікацію з даним клієнтом. HTTP за визначенням не має статусу, а файли cookie сеансу роблять його стаціонарним. Тривалість життя (і джерело, з цього приводу) лексеми не має значення; сервер дбає лише про те, що він дійсний і може бути прив'язаний до користувача (НЕ сеанс). Таким чином, передача ідентифікаційного маркера не перешкоджає ставленності.
Кріс Кешвелл

1
@ChrisCashwell Як ви гарантуєте, що маркер не підробляє / генерує клієнт? Чи використовуєте ви приватний ключ на стороні сервера, щоб зашифрувати маркер, надати його клієнту, а потім використовуйте той самий ключ, щоб розшифрувати його під час майбутніх запитів? Очевидно, що Base64 або якесь інше припухлість було б недостатньо. Чи можете ви детальніше розглянути методики "валідації" цих жетонів?
Крейг Отіс

6
Хоча ця дата і я не торкався і не оновлював код протягом двох років, я створив історію для подальшого розширення цих понять. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Відповіді:


190

Нам вдалося зробити так, як це описано в ОП, і, сподіваємось, хтось інший може використати рішення. Ось що ми зробили:

Налаштуйте контекст безпеки так:

<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." );
    }
}

АутентифікаціяTokenProcessingFilter :

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
@Spring Я не зберігаю їх ніде ... вся ідея маркера полягає в тому, що його потрібно передавати з кожним запитом, і його можна деконструювати (частково), щоб визначити його дійсність (звідси і validate(...)метод). Це важливо, тому що я хочу, щоб сервер залишався без стану. Я б міг уявити, що ти можеш використовувати цей підхід без використання Spring.
Кріс Кешвелл

1
Якщо клієнт - це браузер, то як маркер можна зберігати? чи вам потрібно повторити автентифікацію для кожного запиту?
початківець_

2
чудові поради. @ChrisCashwell - частина, яку я не можу знайти, - це те, де ви перевіряєте облікові дані користувачів і повертаєте токен? Я б припустив, що це має бути десь в імп / кінцевої точки автентифікації. маю рацію ? Якщо ні, то яка мета / автентифікація?
Йонатан Маман

3
що всередині AuthenticationManager?
MoienGK

25

Ви можете розглянути можливість автентифікації дайджесту доступу . По суті протокол такий:

  1. Запит здійснюється від клієнта
  2. Сервер відповідає за допомогою унікальної рядка без значень
  3. Клієнт надає ім'я користувача та пароль (та деякі інші значення) хеш-файлу md5 з нею; цей хеш відомий як HA1
  4. Потім сервер може перевірити особу клієнта та подати запитувані матеріали
  5. Спілкування з nonce може тривати, поки сервер не надасть нове поняття (лічильник використовується для усунення атак повторного відтворення)

Все це повідомлення здійснюється через заголовки, які, як зазначає jmort253, як правило, більш безпечні, ніж передача чутливого матеріалу в параметрах URL.

Підтримка автентичності доступу підтримується Spring Security . Зауважте, що, хоча документи говорять, що ви повинні мати доступ до простого тексту свого клієнта, ви можете успішно пройти автентифікацію, якщо у вас є хеш HA1 для вашого клієнта.


1
Незважаючи на те, що це можливий підхід, кілька маршрутних поїздок, які необхідно здійснити, щоб отримати маркер, робить його небажаним.
Кріс Кешвелл

Якщо ваш клієнт дотримується специфікації HTTP Authentication, ці зворотні поїздки відбуваються лише під час першого дзвінка і коли відбувається 5.
Маркус Малкуш

5

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

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

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.