programing

Spring Boot + Security + Thymeleaf 및 CSRF 토큰이 자동으로 주입되지 않음

i4 2023. 8. 19. 09:45
반응형

Spring Boot + Security + Thymeleaf 및 CSRF 토큰이 자동으로 주입되지 않음

고지 사항:나는 이것으로 백리프가 있는 형태의 토큰을 수동으로 주입하는 방법을 알고 있습니다.

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

이 게시물의 목표는 플랫폼에 대한 지식을 향상시키고 Spring Boot 내부에서 무슨 일이 일어나고 있는지 더 잘 이해하는 것입니다.

저는 스프링 부트를 시도해보지 않았지만, 최근에 시도해 보기로 결정했고, 그것의 훌륭함을 인정해야 했지만, 스프링 MVC에 있는 Thymeleaf와 Security를 사용하면 양식(POST)에 CSRF 토큰을 주입할 필요가 없었습니다. 왜냐하면 Thymeleaf가 자동으로 처리했기 때문입니다. 하지만 이제 Spring Boot에서는 어떤 이유에서인지 그렇지 않습니다.

Spring Boot Reference에서 application.properties 파일에 사용되는 일반적인 속성 목록을 찾았고, 타임리프 및 보안과 관련된 속성은 다음과 같습니다.

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가 토큰을 다시 주입할 수 있는 해결책이 있다면, 저는 그것을 보지 못합니다.

편집: 내 구성 추가

프로젝트는 웹, Thymeleaf, Security, JPA, MySQL, H2, Mail, Facebook, Twitter, LinkedIn 및 Actuator 항목을 확인하고 마지막 STS 버전에 제공된 이니셜라이저를 사용하여 생성되었으며 이후에 일부 추가되었습니다.

가까운 미래에 Openshift에 프로젝트를 배포할 예정이므로 Java 7과 Tomcat 7을 사용하고 있으며 다음은 구성 파일입니다.

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.thymeleaf.cache=false

그리고 타임리프 템플릿은 이렇게 보입니다(현재 제 로그인 페이지에는 명확성을 위해 관련 콘텐츠만 포함됩니다).

<!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>

편집2 - 패러지 패룩이 지적한 방향으로 디버깅을 수행한 후, 제가 게시한 구성이 있는 프로젝트에서 스프링 부트 버전에서 이 클래스에 있다는 것을 알게 되었습니다.org.thymeleaf.spring4.requestdata.RequestDataValueProcessor4Delegate다음 함수는 null 프로세서를 반환합니다.

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);

}

반면 스프링 부트 버전이 아닌 경우, 그것은 인스턴스인 프로세서를 반환합니다.org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor.

저도 비슷한 문제가 있었습니다.조사한 결과, 'th:action' 특성(일반적인 'action'이 아님)을 사용하는 양식에만 csrf 토큰이 주입되었습니다.
로그인 양식의 경우 수동으로 csrf를 주입해야 합니다(링크).
공식 스프링 문서(링크)에는 세션 타임아웃을 방지하기 위해 로그인 양식 제출 직전에 csrf 토큰을 검색하는 제안이 있습니다.이 시나리오에서는 양식의 숨겨진 입력에 csrf 토큰이 없습니다.

Spring Boot + Thymeleaf + Spring Security를 사용하여 다음 작업을 수행했습니다.

응용 프로그램 속성

security.enable-csrf=true

2017년 3월 30일 업데이트:

가지 중요한 것은 양식 내부의 th: 작업을 사용하는 것입니다. 이렇게 하면 스프링 보안부에서 수동으로 삽입할 필요 없이 양식 내부에 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>

『 』에 Thymeleaf 개자발,RequestDataValueProcessor인터페이스는 Thymeleaf에서 폼 포스트백에 자동으로 추가되는 추가 숨김 필드를 찾는 데 사용됩니다.

코드는 에 있습니다.org/thymeleaf/spring3/processor/attr/SpringActionAttrProcessor.java이 표시됩니다.

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

문제를 정렬하고 CSRF 토큰을 자동으로 추가합니다. 응용 프로그램에서 사용자 지정 요청 데이터 값 프로세서를 만들고 스프링에 등록합니다.이 작업을 수행하려면 아래 자습서를 참조하십시오.

스프링-MVC에서의 Csrf 방어

프로젝트의 메이드 XML이 부트 .RequestDataValueProcessor그렇지 않으면.

당신은 두 가지를 해야 할 것입니다.콩 선언

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 ... other beans ...

    @Bean
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CsrfRequestDataValueProcessor();
    }
}

테마 리프 템플릿의 html 양식이 "th:action"을 사용하는지 확인합니다.

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

이렇게 자동으로 _csrf 토큰을 삽입합니다.

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

언급URL : https://stackoverflow.com/questions/29509392/spring-boot-security-thymeleaf-and-csrf-token-not-injected-automatically

반응형