With Spring Security 3.2.0.RELEASE, how can I get the CSRF token in a page that is purely HTML with no tag libs

前端 未结 4 1884
不思量自难忘°
不思量自难忘° 2020-12-02 12:35

Today I upgraded from Spring Security 3.1.4 with the separate java config dependency, to the new 3.2.0 release which includes java config. CSRF is on by default and I know

相关标签:
4条回答
  • 2020-12-02 12:56

    Note: I'm using CORS and AngularJS.

    Note²: I found Stateless Spring Security Part 1: Stateless CSRF protection which would be interesting to keep the AngularJS' way to handle CSRF.

    Instead of using Spring Security CSRF Filter which is based on answers (especially @Rob Winch's one), I used the method described in The Login Page: Angular JS and Spring Security Part II.

    In addition to this, I had to add Access-Control-Allow-Headers: ..., X-CSRF-TOKEN (due to CORS).

    Actually, I find this method cleaner than adding headers to the response.

    Here is the code :

    HttpHeaderFilter.java

    @Component("httpHeaderFilter")
    public class HttpHeaderFilter extends OncePerRequestFilter {
        @Autowired
        private List<HttpHeaderProvider> providerList;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            providerList.forEach(e -> e.filter(request, response));
    
            if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
                response.setStatus(HttpStatus.OK.value());
            }
            else {
                filterChain.doFilter(request, response);
            }
        }
    }
    

    HttpHeaderProvider.java

    public interface HttpHeaderProvider {
        void filter(HttpServletRequest request, HttpServletResponse response);
    }
    

    CsrfHttpHeaderProvider.java

    @Component
    public class CsrfHttpHeaderProvider implements HttpHeaderProvider {
        @Override
        public void filter(HttpServletRequest request, HttpServletResponse response) {
            response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-CSRF-TOKEN");
        }
    }
    

    CsrfTokenFilter.java

    @Component("csrfTokenFilter")
    public class CsrfTokenFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            CsrfToken csrf = (CsrfToken)request.getAttribute(CsrfToken.class.getName());
    
            if (csrf != null) {
                Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
    
                String token = csrf.getToken();
    
                if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                    cookie = new Cookie("XSRF-TOKEN", token);
                    cookie.setPath("/");
    
                    response.addCookie(cookie);
                }
            }
    
            filterChain.doFilter(request, response);
        }
    }
    

    web.xml

    ...
    <filter>
        <filter-name>httpHeaderFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>httpHeaderFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
    

    security-context.xml

    ...
    <custom-filter ref="csrfTokenFilter" after="CSRF_FILTER"/>
    ...
    

    app.js

    ...
    .run(['$http', '$cookies', function ($http, $cookies) {
        $http.defaults.transformResponse.unshift(function (data, headers) {
            var csrfToken = $cookies['XSRF-TOKEN'];
    
            if (!!csrfToken) {
                $http.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
            }
    
            return data;
        });
    }]);
    
    0 讨论(0)
  • 2020-12-02 13:04

    Although @rob-winch is right I would suggest to take token from session. If Spring-Security generates new token in SessionManagementFilter using CsrfAuthenticationStrategy it will set it to Session but not on Request. So it is possible you will end up with wrong csrf token.

    public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
    CsrfToken sessionToken = (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);
    
    0 讨论(0)
  • 2020-12-02 13:20

    I use thymeleaf with Spring boot. I had the same problem. I diagnosed problem viewing source of returned html via browser. It should be look like this:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
    <title>Spring Security Example </title>
    </head>
    <body>
    <form method="post" action="/login">
    <div><label> User Name : <input type="text" name="username" /> </label></div>
    <div><label> Password: <input type="password" name="password" /> </label></div>
    <input type="hidden" name="_csrf" value=<!--"aaef0ba0-1c75-4434-b6cf-62c975dcc8ba"--> />
    <div><input type="submit" value="Sign In" /></div>
    </form>
    </body>
    </html>
    

    If you can't see this html code. You may be forgot to put th: tag before name and value. <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

    login.html

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
    <title>Spring Security Example </title>
    </head>
    <body>
    <div th:if="${param.error}"> Invalid username and password. </div>
    <div th:if="${param.logout}"> You have been logged out. </div>
    <form th:action="@{/login}" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <div><input type="submit" value="Sign In"/></div>
    </form>
    </body>
    </html>
    
    0 讨论(0)
  • 2020-12-02 13:22

    You can obtain the CSRF using the request attribute named _csrf as outlined in the reference. To add the CSRF to an HTML page, you will need to use JavaScript to obtain the token that needs to be included in the requests.

    It is safer to return the token as a header than in the body as JSON since JSON in the body could be obtained by external domains. For example your JavaScript could request a URL processed by the following:

    CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
    // Spring Security will allow the Token to be included in this header name
    response.setHeader("X-CSRF-HEADER", token.getHeaderName());
    // Spring Security will allow the token to be included in this parameter name
    response.setHeader("X-CSRF-PARAM", token.getParameterName());
    // this is the value of the token to be included as either a header or an HTTP parameter
    response.setHeader("X-CSRF-TOKEN", token.getToken());
    

    Your JavaScript would then obtain the header name or the parameter name and the token from the response header and add it to the login request.

    0 讨论(0)
提交回复
热议问题