Я изучал 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, я включил несколько операторов отладки, чтобы показать идентификатор сеанса на ключевых этапах.
Надеюсь, это поможет кому-то понять, что я делаю неправильно.