Почему моя конфигурация безопасности Spring, похоже, отклоняет действительный токен csrf

Я изучал Spring Security (и фронтенд-разработку), пройдя несколько руководств. Однако меня очень сбивают с толку токены CSRF, и я явно делаю что-то не так.

Моя Spring Security настроена с использованием java, и когда я отключаю CSRF (используя следующий фрагмент), форма отправляется без проблем.

 http.csrf().disable();

Насколько я понимаю, здесь заключается в том, что мне нужно выполнить следующие действия:

    1) Use proper verbs 
    2) Enable csrf protection
    3) include _csrf as hidden fields in form.  

Все эти шаги кажутся простыми, но мне кажется, что они не работают, и я получаю сообщение об ошибке:

HTTP Status 403 - Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.

когда я пытаюсь отправить свою регистрационную форму.

Я проверил еще немного и включил поле _csrf как видимое поле в форме в качестве промежуточного шага. Я предполагал, что это не было заполнено, но токен четко виден непосредственно перед тем, как я отправил форму, поэтому проблема, похоже, возникает при публикации данных, а не при их создании.

Полный синтаксис формы показан ниже:

        <form method="POST" th:object="${individualRegistrationInfo}">
            <table>
                <tr>
                    <td>Name:</td>
                    <td><input type="text" th:field="*{name}" /></td>
                    <td th:if="${#fields.hasErrors('name')}"><p th:errors="*{name}">Incorrect Name</p></td>
                </tr>

                <tr>
                    <td>Username:</td>
                    <td><input type="text" th:field="*{username}" /></td>
                    <td th:if="${#fields.hasErrors('username')}"><p th:errors="*{username}">Incorrect Username</p></td>
                </tr>
                <tr>
                    <td>Password:</td>
                    <td><input type="password" th:field="*{password}" /></td>
                    <td th:if="${#fields.hasErrors('password')}"><p th:errors="*{password}">Incorrect Password</p></td>
                </tr>
                <tr>
                    <td>Email:</td>
                    <td><input type="email" th:field="*{email}" /></td>
                    <td th:if="${#fields.hasErrors('email')}"><p th:errors="*{email}">Incorrect Email</p></td>
                </tr>

                <tr>
                    <td>Confirm Email:</td>
                    <td><input type="email" th:field="*{confirmEmail}" /></td>
                    <td th:if="${#fields.hasErrors('confirmEmail')}"><p th:errors="*{confirmEmail}">Incorrect Email Confirmation</p></td>
                </tr>  
                <tr>
                    <td>Region:</td>
                    <td><select th:field="*{regionName}">
                    <option value="NONE">----Select----</option>
                      <option th:each="region : ${regions}" th:value="${region}" th:text="${region}">RegionTemplate</option>    
                    </select></td>
                    <td th:if="${#fields.hasErrors('regionName')}"><p th:errors="*{regionName}">Region Name</p></td>
                </tr> 
                <tr><td>
                <span th:text= "${_csrf.parameterName}">CSRF Parm Name</span></td>
               <td> <span th:text= "${_csrf.token}">CSRF Token value</span> </td></tr>
                <tr>
                    <td colspan="3">
                        <input type="submit" value="Register" />
                    </td>
                </tr>
                  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
            </table>
        </form>

В предыдущей статье упоминается, в частности, следующее: «Одна проблема заключается в том, что ожидаемый токен CSRF хранится в HttpSession, поэтому, как только HttpSession истечет, ваш настроенный AccessDeniedHandler получит InvalidCsrfTokenException. Если вы используете AccessDeniedHandler по умолчанию, браузер получит HTTP 403 и покажет плохое сообщение об ошибке ".

Я вообще не контролирую HttpSession, поэтому я не уверен, что установлено для тайм-аута сеанса (или как его переопределить). Однако, поскольку я перестраиваю приложение, а затем тестирую сразу после этого, тайм-аут сеанса кажется маловероятным.

Я использую следующие зависимости Gradle:

compile group: 'org.springframework', name: 'spring-webmvc', version:'4.0.5.RELEASE'
compile group: 'org.springframework', name: 'spring-context-support', version:'4.0.5.RELEASE'

compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version:'1.1.4.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version:'1.1.4.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version:'1.1.4.RELEASE'
testCompile group: 'org.springframework.security', name:'spring-security-test', version:'4.0.0.M1'

Итак, когда я провожу последнюю проверку по трем предлагаемым шагам:

    1) Use proper verbs (POST is clearly visible on the form code snippet)
    2) Enable csrf protection (http.csrf().disable(); is commented out and _CSRF shows in form)
    3) include _csrf as hidden fields in form. (clearly visible on the form code snippet) 

И таким образом я прихожу к

    4) I am missing something ! 

Может ли кто-нибудь подсказать, что мне может не хватать?

Спасибо, что нашли время, чтобы прочитать это.

Обновлено в полдень 29 июля

Существует другое руководство, которое включает дополнительная информация о защите CSRF.

В частности, существует AccessDeniedHandler, который можно использовать.

Я реализовал это и включил в эту процедуру несколько дополнительных деталей регистрации. В настоящее время я наблюдаю, что токен _csrf, который отображается на странице перед отправкой, также сообщается в AccessDeniedHandler.

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

    static class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException, ServletException {

        logger.warn("Arrived in custom access denied handler.");
        HttpSession session = request.getSession();
        System.out.println("Session is " +session );
        System.out.println("Session id = " + session.getId());
        System.out.println("Session max interval="+session.getMaxInactiveInterval());
        System.out.println("Session last used="+session.getLastAccessedTime());
        System.out.println("Time now="+new Date().getTime());

        System.out.println();
        System.out.println("csrf:");
        Object csrf = request.getAttribute("_csrf");

        if (csrf==null) { 
            System.out.println("csrf is null");
        } else { 
            System.out.println(csrf.toString());
            if (csrf instanceof DefaultCsrfToken) { 
                DefaultCsrfToken token = (DefaultCsrfToken) csrf;
                System.out.println("Parm name " + token.getParameterName());
                System.out.println("Token " + token.getToken());
            }

        }
        System.out.println();
        System.out.println("Request:");
        System.out.println(request.toString());
        System.out.println();
        System.out.println("Response:");
        System.out.println(response.toString());
        System.out.println();
        System.out.println("Exception:");
        System.out.println(accessDeniedException.toString());
    }
}

Результатом этой дополнительной регистрации является:

Session is org.apache.catalina.session.StandardSessionFacade@579ebbb8
Session id = 7CC03DAFF6BC34E28F5E91974C7E4BA5
Session max interval=1800
Session last used=1406630832436
Time now=1406630878254

csrf:
org.springframework.security.web.csrf.DefaultCsrfToken@763659f8
Parm name _csrf
Token 1e9cb3cf-c111-4b05-aace-4f8480b7d67b

Request:
   org.springframework.security.web.context.HttpSessionSecurityContextRepository$Servlet3SaveToSessionRequestWrapper@6a4ce569

Response:
    org.springframework.security.web.context.HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper@5e698704

Exception:
org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.

В этом конкретном случае токен 1e9cb3cf-c111-4b05-aace-4f8480b7d67b был встроен в форму.

Ошибка указывает, что токен CSRF 'null' был найден, но напечатанные строки показывают, что значение '_csrf' присутствует в атрибутах запроса.

Обновление от 30 июля. Я воспроизвел эту проблему в небольшом примере проекта, который теперь доступен на GitHub: https://github.com/Mark-Allen/csrf-example.git

В исходном состоянии приложение принимает имя, а затем повторно отображает это имя. Когда строка 52 SecurityWebConfiguration закомментирована (см. Ниже код, который необходимо прокомментировать), тогда приложение не работает.

 http.csrf().disable();

Следуя предложению @Bart, я включил несколько операторов отладки, чтобы показать идентификатор сеанса на ключевых этапах.

Надеюсь, это поможет кому-то понять, что я делаю неправильно.


person Mark 3884599    schedule 28.07.2014    source источник
comment
Сохраняется ли идентификатор сеанса при последующих запросах?   -  person Bart    schedule 29.07.2014
comment
@Bart Я явно не использую идентификатор сеанса, за исключением журналов, которые я только что обновил в этом вопросе. Я не знаю, как ответить на этот вопрос.   -  person Mark 3884599    schedule 29.07.2014
comment
@Bart - Хорошо, было довольно просто перенести сеанс в различные методы, и этот идентификатор сеанса кажется согласованным во всем приложении (без включенного csrf). Приложение не работает с включенным csrf. Спасибо за предложение !   -  person Mark 3884599    schedule 30.07.2014
comment
Я пошел посмотреть ваш пример на GitHub, но он не содержит кода.   -  person Bart    schedule 30.07.2014
comment
Упс - я не включил папку src / в репозиторий git! Теперь это исправлено.   -  person Mark 3884599    schedule 30.07.2014


Ответы (2)


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

Я использую аналогичную настройку, и похоже, что вы тоже используете Thymeleaf.

Когда я читал документацию Thymeleaf по интеграции Spring Security (http://www.thymeleaf.org/doc/springsecurity.html), я заметил, что они используют "th: action" вместо "action" в теге формы. Это решило мою проблему.

Похоже, что без атрибута th: action Thymeleaf не знает, как «ввести» токен csrf.

person raydogg    schedule 15.09.2014
comment
Спасибо тебе за это, Рэй. Я использую Thymeleaf, и я вообще не указывал действие. Я попробовал ваше предложение, но оно все еще не работает. Рад, что он работает на вас! Спасибо за ваш ответ. - person Mark 3884599; 16.09.2014
comment
Здесь та же проблема. Мне потребовалось 2 часа, чтобы выяснить это. Не удалось найти никакой документации, в которой упоминается, что th: action является обязательным. - person Mario Eis; 23.10.2014

Слишком стар, но наконец я нахожу ответ на этот вопрос. Имя скрытого поля должно быть _csrf.

<input type="hidden" name="_csrf" value="${_csrf.token}"/>
person Leandro Dantas    schedule 16.06.2016