【问题标题】:JSON CSRF Interceptor for RestTemplateRestTemplate 的 JSON CSRF 拦截器
【发布时间】:2015-11-08 20:58:06
【问题描述】:

我有一个小型 Rest-Service 应用程序(Java 8、Spring 4.1.6、Spring Security 5.0.1、Jetty 9.3),我正在使用 Spring RestTemplate 通过 JSON 访问一些服务。到目前为止 csfr 被禁用,现在我想启用它。

据了解,csfr 有一个公共令牌(客户端随每个请求发送它,服务器将其存储在会话中),该令牌在服务器端进行比较。如果没有可用的令牌或令牌不同,则拒绝访问。

所以我认为使用拦截器添加令牌是个好主意。我还读到,在 json 中我必须将令牌作为标头参数发送......但我做错了,登录已经失败。

这里是登录源:

MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", username);
form.add("password", password);
return restTemplate.postForLocation(serverUri + "login", form);

这里是拦截器的来源:

import java.io.IOException;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class MyCsfrInterceptor implements ClientHttpRequestInterceptor{

    public static final String CSRF_TOKEN_HEADER_NAME = "X-CSRF-TOKEN";
    public static final String csrfSessionToken = UUID.randomUUID().toString();

    private static Logger LOG = LoggerFactory.getLogger(MyCsfrInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        LOG.info("My interceptor called!");
        if (request.getMethod() == HttpMethod.DELETE ||
                request.getMethod() == HttpMethod.POST ||
                request.getMethod() == HttpMethod.PATCH ||
                request.getMethod() == HttpMethod.PUT){
            LOG.info("Setting csrf token...");
            request.getHeaders().add(CSRF_TOKEN_HEADER_NAME, csrfSessionToken);
        }
        return execution.execute(request, body);
    }

}

这是客户端的日志输出:

23:24:40.605 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - Created POST request for "http://localhost:8080/login"
23:24:40.610 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - Writing [{username=[user], password=[user]}] using [org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@618b19ad]
23:24:40.615 [main] INFO  c.m.l.w.client.MyCsfrInterceptor - My interceptor called!
23:24:40.615 [main] INFO  c.m.l.w.client.MyCsfrInterceptor - Setting csrf token...
23:24:40.650 [main] DEBUG o.a.h.c.protocol.RequestAddCookies - CookieSpec selected: best-match
23:24:40.670 [main] DEBUG o.a.h.c.protocol.RequestAuthCache - Auth cache not set in the context
23:24:40.675 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://localhost:8080][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
23:24:40.705 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection leased: [id: 0][route: {}->http://localhost:8080][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
23:24:40.710 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Opening connection {}->http://localhost:8080
23:24:40.715 [main] DEBUG o.a.h.i.c.HttpClientConnectionOperator - Connecting to localhost/127.0.0.1:8080
23:24:40.715 [main] DEBUG o.a.h.i.c.HttpClientConnectionOperator - Connection established 127.0.0.1:54712<->127.0.0.1:8080
23:24:40.715 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Executing request POST /login HTTP/1.1
23:24:40.715 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Target auth state: UNCHALLENGED
23:24:40.720 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Proxy auth state: UNCHALLENGED
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> POST /login HTTP/1.1
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Type: application/x-www-form-urlencoded
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> X-CSRF-TOKEN: 99ca8171-b8e0-4b95-8d06-3759a874c64b
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Length: 27
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Host: localhost:8080
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Connection: Keep-Alive
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> User-Agent: Apache-HttpClient/4.3.4 (java 1.5)
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Accept-Encoding: gzip,deflate
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "POST /login HTTP/1.1[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Type: application/x-www-form-urlencoded[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "X-CSRF-TOKEN: 99ca8171-b8e0-4b95-8d06-3759a874c64b[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Length: 27[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Host: localhost:8080[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.3.4 (java 1.5)[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "username=user&password=user"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 403 Expected CSRF token not found. Has your session expired?[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Date: Sat, 15 Aug 2015 21:24:40 GMT[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Pragma: no-cache[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-XSS-Protection: 1; mode=block[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-Frame-Options: DENY[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-Content-Type-Options: nosniff[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Set-Cookie: JSESSIONID=1bvmexep1lv9h1qja44hflx0wg;Path=/[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Type: text/html;charset=iso-8859-1[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Cache-Control: must-revalidate,no-cache,no-store[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Length: 409[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Server: Jetty(9.3.0.RC1)[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<html>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<head>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<title>Error 403 Expected CSRF token not found. Has your session expired?</title>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</head>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<body><h2>HTTP ERROR 403</h2>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<p>Problem accessing /login. Reason:[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<pre>    Expected CSRF token not found. Has your session expired?</pre></p><hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.0.RC1</a><hr/>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</body>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</html>[\n]"
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << HTTP/1.1 403 Expected CSRF token not found. Has your session expired?
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Date: Sat, 15 Aug 2015 21:24:40 GMT
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Pragma: no-cache
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-XSS-Protection: 1; mode=block
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-Frame-Options: DENY
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-Content-Type-Options: nosniff
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Set-Cookie: JSESSIONID=1bvmexep1lv9h1qja44hflx0wg;Path=/
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Type: text/html;charset=iso-8859-1
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Cache-Control: must-revalidate,no-cache,no-store
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Length: 409
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Server: Jetty(9.3.0.RC1)
23:24:40.846 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Connection can be kept alive indefinitely
23:24:40.861 [main] DEBUG o.a.h.c.p.ResponseProcessCookies - Cookie accepted [JSESSIONID="1bvmexep1lv9h1qja44hflx0wg", version:0, domain:localhost, path:/, expiry:null]
23:24:40.861 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - POST request for "http://localhost:8080/login" resulted in 403 (Expected CSRF token not found. Has your session expired?); invoking error handler
23:24:40.866 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection [id: 0][route: {}->http://localhost:8080] can be kept alive indefinitely

在 spring 安全配置中我没有做任何 csrf 配置。

那怎么了?我是否设置了错误的标题?是否缺少任何配置?

最好的问候,感谢您的宝贵时间。

【问题讨论】:

    标签: json spring-security csrf


    【解决方案1】:

    查看您的代码,您似乎正在自己生成 CSRF 令牌。但是,据我了解,Spring Security CSRF 处理将以这种方式工作:

    1. Spring Security 将生成 CSRF 令牌。
    2. 每当一个请求到来时(比如一个 GET 请求),Spring Security 都会将令牌作为请求参数附加。这有助于将标记作为隐藏字段呈现 JSP 表单,如下所示:

      <form action="/foo/5/update" method="post">
          <input type="text" ... />
          <input type="submit" value="Update" />
          <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      </form>
      
    3. 每当一个类似 POST 的请求到来时(比如提交了一个表单),Spring Security 都会将其令牌与作为参数或标头提交的令牌进行匹配。

    (2) 在我们使用 JSP 等时效果很好。但是,当我们调用 API 时,我们首先需要获取令牌。这样做的一种常见做法是在服务器端有一个过滤器,将令牌作为 cookie 附加。这是我在一个项目中的过滤代码:

    public class CsrfCookieFilter extends OncePerRequestFilter {
    
        public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                        throws ServletException, IOException {      
    
            log.debug("Inside CsrfCookieFilter ...");
    
            CsrfToken csrf = (CsrfToken)
                request.getAttribute(CsrfToken.class.getName()); // Or "_csrf" (See CSRFFilter.doFilterInternal).
    
            if (csrf != null) {
                Cookie cookie = WebUtils.getCookie(
                    request, XSRF_TOKEN_COOKIE_NAME);
                String token = csrf.getToken();
                if (cookie==null ||
                    token!=null && !token.equals(cookie.getValue())) {
                    cookie = new Cookie(XSRF_TOKEN_COOKIE_NAME, token);
                    cookie.setPath("/");
                    response.addCookie(cookie);
                }
            }       
            filterChain.doFilter(request, response);
        }
    }
    

    因此,在 POST 请求之前,应首先发出 GET 请求并将令牌作为 cookie 获取。然后该令牌应在后续请求中作为标头发回。

    我的用于连接此过滤器和更改标题名称的代码在我的安全配置类中如下所示:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http
            ...
            .addFilterAfter(csrfCookieFilter(), CsrfFilter.class)
            ...
    }
    
    protected Filter csrfCookieFilter() {
        return new CsrfCookieFilter();
    }
    

    另外,请注意在 loginlogout 等特定事件之后,Spting Security changes the token。因此,我们需要再次使用 GET 请求获取令牌。

    Spring Angular官方指南已经详细阐述了。

    【讨论】:

    • 嗨!首先感谢您的回答。我已经尝试了您的解决方案,它部分有效(我认为我仍然做错了什么)。在登录之前,我会执行一个虚拟获取请求以接收令牌。这有效,设置了令牌,我读取了 cookie 令牌并将其添加到请求中。登录有效。但是:登录调用失败后的第一个请求(无效的 CSRF 令牌)。原因:登录后没有调用 CsrfCookieFilter 来刷新 cookie 令牌。我在 DelegatingFilterProxy 之后的 web.xml 中添加了 CsrfCookieFilter,但这似乎是错误的。我是否必须更改 filterchan 才能正常工作?
    • 在某些事件(如登录和注销)之后,将需要另一个 GET 请求。有关详细信息,请参阅我的答案的最后一段链接的堆栈溢出问题。
    • 好的,现在可以了。非常感谢。还有一个问题:为什么登录后没有调用 CsrfCookieFilter?因为如果它会在登录后调用,则新令牌已经设置......所以登录后的获取请求将不是必需的。是登录后生成新会话的原因吗?
    • 我做了一些尝试挖掘 Spring Security 代码,但无法弄清楚。我的帖子中与最后第二段相关的 cmets 说明了您的怀疑。但是新的会话 ID 无论如何都会在响应中返回。不知道。
    猜你喜欢
    • 2019-12-22
    • 1970-01-01
    • 2021-05-20
    • 1970-01-01
    • 2014-06-24
    • 2018-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多