【问题标题】:Spring Boot + Security + Thymeleaf and CSRF token not injected automaticallySpring Boot + Security + Thymeleaf 和 CSRF 令牌未自动注入
【发布时间】:2015-06-13 02:25:07
【问题描述】:

免责声明:我知道如何使用 thymeleaf 手动将令牌注入表单中:

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />`

这篇文章的目的是提高对平台的了解,并更好地了解 Spring Boot 内部发生的事情

我没有尝试过 Spring Boot,但最近我决定试一试,不得不承认它很棒,但是使用 Spring MVC 上的 Thymeleaf 和 Security,我不需要在表单上注入 CSRF 令牌( POST),因为 Thymeleaf 会自动处理它,但现在在 Spring Boot 中由于某种原因它不会。

Spring Boot Reference,我找到了application.properties文件中常用的属性列表,其中与thymeleaf和security相关的有:

Thymeleaf 属性

spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.excluded-view-names= # comma-separated list of view names that should be excluded from resolution
spring.thymeleaf.view-names= # comma-separated list of view names that can be resolved
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh

安全属性

security.user.name=user # login username
security.user.password= # login password
security.user.role=USER # role assigned to the user
security.require-ssl=false # advanced settings ...
security.enable-csrf=false
security.basic.enabled=true
security.basic.realm=Spring
security.basic.path= # /**
security.basic.authorize-mode= # ROLE, AUTHENTICATED, NONE
security.filter-order=0
security.headers.xss=false
security.headers.cache=false
security.headers.frame=false
security.headers.content-type=false
security.headers.hsts=all # none / domain / all
security.sessions=stateless # always / never / if_required / stateless
security.ignored= # Comma-separated list of paths to exclude from the     default secured paths

但是如果让 Thymeleaf 再次注入令牌的解决方案在那里,我看不到它。

编辑:添加我的配置

该项目是使用上一个 STS 版本(在我看来很棒)中附带的初始化程序创建的,其中包含 Web、Thymeleaf、Security、JPA、MySQL、H2、Mail、Facebook、Twitter、LinkedIn 和 Actuator 项目检查,并在后面添加了一些额外内容

使用 Java 7 和 Tomcat 7,因为我打算在不久的将来在 Openshift 上部署该项目,接下来是我的配置文件:

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.3.RELEASE</version>
    <relativePath/>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <start-class>com.adrisasws.springmvc.WebApplication</start-class>
    <java.version>1.7</java.version>
    <tomcat.version>7.0.59</tomcat.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.spring.platform</groupId>
            <artifactId>platform-bom</artifactId>
            <version>1.1.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-social-facebook</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-social-linkedin</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-social-twitter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-google</artifactId>
        <version>1.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
<profiles>
    <profile>
        <id>openshift</id>
        <build>
            <finalName>webapp</finalName>
            <plugins>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>2.1.1</version>
                    <configuration>
                        <outputDirectory>webapps</outputDirectory>
                        <warName>ROOT</warName>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

安全配置(与我在非启动项目中使用的完全相同的安全文件,其中 CSRF 令牌实际上是自动注入的)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    //////////////////////////////////////////////////////////////////////////
    //                              DEPENDENCIES                            //
    //////////////////////////////////////////////////////////////////////////

    @Autowired private DataSource dataSource;
    @Autowired private UserRepository userRepository;


    //////////////////////////////////////////////////////////////////////////
    //                               PROPERTIES                             //
    //////////////////////////////////////////////////////////////////////////

    @Value("${custom.security.rememberme-secret}")  private String secret;
    @Value("${custom.security.rememberme-create-tables}") private String createTables;

    private final static String[] adminRequests = new String[] { ... some matchers here... };
    private final static String[] userRequests = new String[] { ... some matchers here... };
    private final static String[] publicRequests = new String[] { ...some matchers here... };


    //////////////////////////////////////////////////////////////////////////
    //                              AUTHORIZATION                           //
    //////////////////////////////////////////////////////////////////////////

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/error**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers(adminRequests).access("hasRole('"+Role.ADMIN.toString()+"')")
                .antMatchers(userRequests).access("hasRole('"+Role.USER.toString()+"')")
                .antMatchers(publicRequests).permitAll()
                .anyRequest().authenticated()
                .and()
            .requiresChannel()
                .anyRequest().requiresSecure()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/", false)
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
                .and()
            .rememberMe()
                .rememberMeServices(rememberMeService())
                .and()
            .apply(new SpringSocialConfigurer());
    }


    //////////////////////////////////////////////////////////////////////////
    //                              AUTHENTICATION                          //
    //////////////////////////////////////////////////////////////////////////

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService())
            .passwordEncoder(bCryptPasswordEncoder());
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder(11);
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserRepositoryUserDetailsService(userRepository);
    }

    @Bean
    public SocialUserDetailsService socialUserDetailsService() {
        return new UserRepositorySocialUserDetailsService(userDetailsService());
    }


    //////////////////////////////////////////////////////////////////////////
    //                               REMEMBER ME                            //
    //////////////////////////////////////////////////////////////////////////

    @Bean
    public JdbcTokenRepositoryImpl jdbcTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(Boolean.valueOf(createTables));
        return jdbcTokenRepository; 
    }

    @Bean
    public RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
        return new RememberMeAuthenticationProvider(secret);
    }

    @Bean 
    public PersistentTokenBasedRememberMeServices rememberMeService() {
        PersistentTokenBasedRememberMeServices service = 
                new PersistentTokenBasedRememberMeServices(secret, userDetailsService(), jdbcTokenRepository());
        service.setUseSecureCookie(true);
        service.setParameter("rememberme");
        service.setTokenValiditySeconds(AbstractRememberMeServices.TWO_WEEKS_S);
        return service;
    }

    @Bean
    public RememberMeAuthenticationFilter authenticationFilter() throws Exception {
        return new RememberMeAuthenticationFilter(authenticationManager(), rememberMeService());
    }
}

目前在我的 Spring Boot 配置中与 thymeleaf 相关,用于开发目的

spring.thymeleaf.cache=false

thymeleaf 模板看起来像这样(我的登录页面,为了清楚起见,将只包含相关内容)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security/"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="thymeleaf/layouts/default">
<head>
    ... css and meta tags ...
</head>
<body>
        ... some html ...
        <th:block sec:authorize="isAnonymous()">
        <!-- Bad Credentials -->
        <div th:if="${param.error}" class="alert alert-danger text-center">
            Invalid username and/or password.
        </div>
        <!-- Logout -->
        <div th:if="${param.logout}" class="alert alert-success text-center">
            You have been logged out.
        </div>

        <!-- Login Form -->
        <form id="f" th:action="@{/login}" method="post" role="form" autocomplete="off">
            <!-- Username -->       
            <input type="text" class="form-control text-center" id="username" name="username" th:placeholder="#{form.login.username}" />
            <!-- Password -->
            <input type="password" class="form-control text-center" id="password" name="password" th:placeholder="#{form.login.password}" />
            <!-- Remember me -->
            <input type="checkbox" id="rememberme" name="rememberme" />
            <!-- Submit -->
            <button type="submit" class="btn btn-primary" th:utext="#{form.login.submit}">Login</button>
            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        </form>
        ... more html and javascript ...
</body>
</html>

Edit2 - 在对Faraj Farook 指出的方向进行一些调试后,我发现,在我发布的配置项目中,在 Spring Boot 版本中,在此类 org.thymeleaf.spring4.requestdata.RequestDataValueProcessor4Delegate ,下面的函数返回一个空处理器

public Map<String, String> getExtraHiddenFields(
        final RequestContext requestContext, final HttpServletRequest request) {

    final RequestDataValueProcessor processor = requestContext.getRequestDataValueProcessor();
    if (processor == null) {
        return null;
    }

    return processor.getExtraHiddenFields(request);

}

而非 Spring 引导版本,它返回一个处理器,它是 org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor 的一个实例。

【问题讨论】:

  • 你的 Spring Security 配置是什么样的?你的pom是什么样的?你的 Thymeleaf 文件是什么样的?
  • 添加了 pom、Spring Security 配置和 Thymeleaf 模板示例,抱歉耽搁了

标签: spring-security spring-boot csrf thymeleaf


【解决方案1】:

根据Thymeleaf 开发人员的说法,Thymeleaf 使用RequestDataValueProcessor 接口来查找自动添加到回发表单中的额外隐藏字段。

org/thymeleaf/spring3/processor/attr/SpringActionAttrProcessor.java 中的以下代码显示了这一点。

 final Map<String,String> extraHiddenFields =
                    RequestDataValueProcessorUtils.getExtraHiddenFields(arguments.getConfiguration(), arguments);

对issue进行排序,并自动添加CSRF Token;在您的应用程序中创建一个自定义请求数据值处理器并将其注册到 spring。为此,您可以阅读下面的教程。

Csrf Defense in Spring-MVC

我还建议你在没有 spring boot 的情况下检查你之前的 spring MVC 代码,以确认项目的配置 XML 是否有自定义的RequestDataValueProcessor

【讨论】:

  • 今晚晚些时候有时间我会尽快看看,感谢您的回复
  • 编辑了我最初的帖子,你是对的,谢谢你的指示,很有帮助
【解决方案2】:

使用 Spring Boot + Thymeleaf + Spring Security 可以解决这个问题:

应用程序属性

security.enable-csrf=true

2017 年 3 月 30 日更新

重要的一点是:在表单中使用 th:action,这将告诉 Spring Security 在表单中注入 CSRF,而无需手动插入。

手动插入:

html 模板

<input type="hidden" 
th:name="${_csrf.parameterName}" 
th:value="${_csrf.token}" />

2017 年 1 月 25 日更新

pom.xml

    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        <version>2.1.2.RELEASE</version>
    </dependency>

【讨论】:

    【解决方案3】:

    我遇到了类似的问题。经过一番调查,我发现只有使用 'th:action' 属性(不是普通的 'action')的表单才注入了 csrf 令牌。
    对于登录表单,您似乎需要手动注入 csrf (link)。
    在官方 spring 文档 (link) 中,建议在登录表单提交之前检索 csrf 令牌以防止会话超时。在这种情况下,表单的隐藏输入中将没有 csrf 令牌。

    【讨论】:

    • th:action 解决了困扰我几天的问题。解决了一个问题!
    【解决方案4】:

    你必须做两件事。声明一个 bean

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
     ... other beans ...
    
        @Bean
        public RequestDataValueProcessor requestDataValueProcessor() {
            return new CsrfRequestDataValueProcessor();
        }
    }
    

    确保您的themeleaf 模板中的html 表单使用“th:action”

    <form th:action="@{/youractionurl}"> 
     ... input tags
    </form>
    

    这样会自动插入 _csrf 令牌

    <input type="hidden" name="_csrf" value="4568ad84-b300-48c4-9532-a9dcb58366f3" />
    

    【讨论】:

      猜你喜欢
      • 2018-07-25
      • 2015-08-25
      • 2019-07-20
      • 2017-09-07
      • 1970-01-01
      • 2017-05-02
      • 2020-07-19
      • 2012-03-06
      • 2020-08-31
      相关资源
      最近更新 更多