从Spring Security 4开始,默认启用CSRF机制,本来这也不算什么大事,但与Spring Boot结合在一起,那么实现起来就比较麻烦了,尤其是采用前后端分离式的开发架构后,配置CSRF机制就更困难了,几乎所有网上的解决办法都无法解决如何获取CSRF编码的难题,首先以表单登陆的错误镇楼:

There was an unexpected error (type=Forbidden, status=403).
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.

1. 禁用CSRF机制

禁用CSRF机制能非常完美地兼容Spring Security老版本,但在Spring Boot的实现中,如果继续沿用老版本的配置,大部分的人会碰到以下错误:

An Authentication object was not found in the SecurityContext

这是因为Spring MVC自动代理所有的请求导致的问题,所以解决办法很简单,将多个“http”配置转换为一个“http”配置,并且禁止使用“security=”none””,下面是错误的写法:

<sec:http pattern="/login.html*" security="none"/>

正确的写法如下:

<sec:http use-expressions="false">
    <sec:intercept-url pattern="/login.html*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
</sec:http>

上述两种写法的差异在于第二种会穿越所有的安全过滤器,例如“SecurityContextPersistenceFilter”、“LogoutFilter”,“CsrfFilter”等,而第一种则不会。很显然,对于静态资源,第一种的性能要高很多,但是如果你的静态委托Spring Boot进行管理,那么你只能采用第二种配置方式。

禁用CSRF机制很简单,只需要将其设置为“disabled”即可,如下:

<sec:csrf disabled="true"/>

但是在实战中,我发现很多人在登陆过程中,会提示如下错误:

There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'POST' not supported

经过跟踪调试,发现问题不在于CSRF,而在于“form-login”配置错误,如下:

<sec:form-login login-page="/login.html"
                password-parameter="password"
                username-parameter="username"
                default-target-url="/admin.html"
                <!-- 问题就在于此项配置,forward -->
                authentication-success-forward-url="/admin.html"        
                login-processing-url="/j_security_check"/>

问题就在注释的那一行代码,“authentication-success-forward-url”意味着会将登陆请求转发给新地址,这就要求新地址必须支持“POST”,否则将出现405错误,所以解决办法也很简单,要么将那项配置注销,要么将转发的地址必须支持“POST”请求。

2. 启用CSRF机制

启用CSRF机制必须要做到如下几点:
1. 首先命名CSRF Cookie;
2. 从Cookie中获取CSRF Token;
3. 在提交表单时必须添加CSRF Token;
4. 注销系统时必须采用POST方式(必然要添加CSRF Token);
上面四件事少做一件,都会提示如下错误:

There was an unexpected error (type=Forbidden, status=403).
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.

2.1 命名CSRF Cookie

命名CSRF Cookie要结合使用如下两项配置,如下:

<sec:csrf token-repository-ref="tokenRepository"/>

<bean id="tokenRepository"
      class="org.springframework.security.web.csrf.CookieCsrfTokenRepository">
    <property name="cookieHttpOnly" value="false"/>
    <property name="cookieName" value="X-XSRF-TOKEN"/>
    <property name="headerName" value="X-XSRF-TOKEN"/>
</bean>

2.2 获取CSRF Token

在前后端分离式结构中,每次视图刷新时都会返回新的CSRF Token,但在我们的第一步,已将CSRF Token放置在Cookie中,所以只需要将其从Cookie中取出即可,如下:

//  将Cookie转换为JS Object
function initCookies() {
    var cookie = document.cookie,
        items = cookie.split(";"),
        keys = {};
    items.forEach(function(item) {
        var kv = item.split('=');
        keys[$.trim(kv[0])] = $.trim(kv[1]);
    });
    return keys;
}
//  获取CSRF Token
var _csrf = initCookies()['X-XSRF-TOKEN'];

2.3 在AJAX请求中添加CSRF Token

有了第二步,现在添加CSRF Token就容易多了,如下:

//  提交数据
$.post(url, {
    userId : code,
    _csrf : cookies['X-XSRF-TOKEN']
}, function(datas) {
    //  TODO something
})

2.4 注销系统改为POST方式

参照上述的操作即可,略。

结论

禁用CSRF容易,启用CSRF难,并且差异较大,请密切注意它们对安全过滤器的影响。