I have a web application that is experiencing weird behavior. When you try to bring up the app, it requests that you log in as expected, and takes you to the welcome page (/
) you can then select either the profile (/profile
) page or the search page (/search
). If you try to access any of these pages without logging in, it redirects you to the login page as expected. When you try to submit search criteria or submit a password change, however, a 403 Forbidden is returned.
<security:http use-expressions="true">
<security:intercept-url pattern="/resources/css/*" access="permitAll" />
<security:intercept-url pattern="/resources/images/*" access="permitAll" />
<security:intercept-url pattern="/login" access="permitAll" />
<security:intercept-url pattern="/logout" access="permitAll" />
<security:intercept-url pattern="/accessdenied" access="permitAll" />
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<security:form-login
login-page="/login"
default-target-url="/"
authentication-success-handler-ref="loginSuccessHandler"
authentication-failure-url="/accessdenied"
/>
<security:logout
logout-success-url="/"
logout-url="/perform_logout"
delete-cookies="JSESSIONID"
/>
</security:http>
urls:
/ (Welcome Page [GET])
/search (Search Page [GET])
/search/data (Search Query [POST])
/profile (Profile Page [GET])
/profile/updatePassword (Profile Update [POST])
Profile Controller
@Controller
@RequestMapping({ "/profile" })
public class ProfileController {
@Autowired
UserService userService = null;
@Autowired
ProfileService profileService = null;
@RequestMapping(value = { "/", "" }, method = RequestMethod.GET)
public String getProfile(Model model) {
Profile profile = profileService.getProfile();
model.addAttribute("profile", profile);
return "profile";
}
@RequestMapping(value = { "/updatePassword" }, method = RequestMethod.POST)
public @ResponseBody AjaxResponse updatePassword(@RequestBody Profile profile) {
// do stuff
return new AjaxResponse(response, null, errors);
}
}
Search Controller
@Controller
@RequestMapping({ "/search" })
public class StockKeepingUnitController {
@Autowired(required = true)
private SkuService skuService;
@Autowired(required = true)
private UserService userService;
@RequestMapping(value = {"", "/"}, method = RequestMethod.GET)
public String search() {
return "search";
}
@RequestMapping(value = "/data", method = RequestMethod.POST)
public @ResponseBody AjaxResponse data(@RequestBody SearchCriteria searchCriteria) {
List<StockKeepingUnit> skus = null;
try {
String criteria = searchCriteria.getCriteria();
skus = skuService.listSkusBySearch(criteria);
} catch (Exception ex) {
ex.printStackTrace();
List<String> errors = new ArrayList<>();
errors.add("Error saving ALOT.");
return new AjaxResponse("ERROR", null, errors);
}
return new AjaxResponse("OK", skus, null);
}
}
Search ajax
$.ajax({url: "${pageContext.request.contextPath}/search/data"
, method: "POST"
, contentType: "application/json; charset=utf-8"
, dataType: "json"
, data: JSON.stringify(searchCriteria)
, success: function(ajaxResponse) { /* ... */ }
, error: function(xhr, status, error) { /* ... */ }
});
Profile ajax
$.ajax({
url: "${pageContext.request.contextPath}/profile/updatePassword",
, method: "POST"
, contentType: "application/json; charset=utf-8"
, dataType: "json"
, data: JSON.stringify(profile)
, success : function(ajaxResponse) { /* ... */ }
, error : function(xhr, status, error) { /* ... */ }
});
---EDIT--- jQuery for csrf
$(function() {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
Also I just found out that if I reload each page, the POST submission works. Is there some way the CSRF token changes on each page? I am using jQuery Mobile btw.
The problem is caused because jQuery Mobile doesn't normally load header information on each page request, which is where the CSRF token is stored. So when navigating to a new page, it is using a stale CSRF token when doing a POST, which causes the 403 Forbidden. To overcome this, I forced JQM to link without ajax by including data-ajax="false"
in each link to a page. For example:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<form action="<c:url value="/perform_logout" />" method="POST" name="logoutform">
<input type="hidden" name="${_csrf.parameterName}" value = "${_csrf.token}" />
</form>
<ul data-role="listview" data-theme="a" data-divider-theme="a" style="margin-top: -16px;" class="nav-search">
<li data-icon="delete" style="background-color: #111;"><a href="#" data-rel="close">Close menu</a></li>
<li><a href="${pageContext.request.contextPath}/search" data-ajax="false">Search</a></li>
<li><a href="${pageContext.request.contextPath}/profile" data-ajax="false">Profile</a></li>
<li><a href="#" onclick="document.logoutform.submit();">Logout</a></li>
</ul>