基於Spring Security+Spring MVC的web應用,為了防止跨站提交攻擊,通常會配置csrf,即:
1 <http ...> 2 ... 3 <csrf /> 4 </http>
如果應用中有Post方式訪問的Rest服務(參考下面的代碼),會很不幸的發現,所有POST方式請求的服務會調用失敗。
1 @RequestMapping(value = "/user/create", method = RequestMethod.POST) 2 @ResponseBody 3 public UserInfo createUser(@RequestBody(required = true) UserInfo user, 4 HttpServletRequest request, HttpServletResponse response) 5 throws Exception { 6 ... 7 }
原因在於:啟用csrf后,所有http請求都被會CsrfFilter攔截,而CsrfFilter中有一個私有類DefaultRequiresCsrfMatcher
1 private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { 2 private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$"); 3 4 /* (non-Javadoc) 5 * @see org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.servlet.http.HttpServletRequest) 6 */ 7 public boolean matches(HttpServletRequest request) { 8 return !allowedMethods.matcher(request.getMethod()).matches(); 9 } 10 }
從這段源碼可以發現,POST方法被排除在外了,也就是說只有GET|HEAD|TRACE|OPTIONS這4類方法會被放行,其它Method的http請求,都要驗證_csrf的token是否正確,而通常post方式調用rest服務時,又沒有_csrf的token,所以校驗失敗。
解決方法:自己弄一個Matcher
1 package com.cnblogs.yjmyzz.utils; 2 3 import java.util.List; 4 import java.util.regex.Pattern; 5 6 import javax.servlet.http.HttpServletRequest; 7 8 import org.springframework.security.web.util.matcher.RequestMatcher; 9 10 public class CsrfSecurityRequestMatcher implements RequestMatcher { 11 private Pattern allowedMethods = Pattern 12 .compile("^(GET|HEAD|TRACE|OPTIONS)$"); 13 14 public boolean matches(HttpServletRequest request) { 15 16 if (execludeUrls != null && execludeUrls.size() > 0) { 17 String servletPath = request.getServletPath(); 18 for (String url : execludeUrls) { 19 if (servletPath.contains(url)) { 20 return false; 21 } 22 } 23 } 24 return !allowedMethods.matcher(request.getMethod()).matches(); 25 } 26 27 /** 28 * 需要排除的url列表 29 */ 30 private List<String> execludeUrls; 31 32 public List<String> getExecludeUrls() { 33 return execludeUrls; 34 } 35 36 public void setExecludeUrls(List<String> execludeUrls) { 37 this.execludeUrls = execludeUrls; 38 } 39 }
這里添加了一個屬性execludeUrls,允許人為排除哪些url。
然后在配置文件里,這樣修改:
1 <http entry-point-ref="loginEntryPoint" use-expressions="true"> 2 ... 3 <intercept-url pattern="/rest/**" access="permitAll" /> 4 ... 5 <csrf request-matcher-ref="csrfSecurityRequestMatcher"/> 6 </http> 7 8 <beans:bean id="csrfSecurityRequestMatcher" class="com.cnblogs.yjmyzz.utils.CsrfSecurityRequestMatcher"> 9 <beans:property name="execludeUrls"> 10 <beans:list> 11 <beans:value>/rest/</beans:value> 12 </beans:list> 13 </beans:property> 14 </beans:bean>
這里約定所有/rest/開頭的都是Rest服務地址,上面的配置就把/rest/排除在csrf驗證的范圍之外了.