BAEL-4792 Stateless REST API and CSRF (#11398)

* Scan package for controller
Migrate deprecated Spring config

* BAEL-4792: enable CSRF + sample REST API request

* Adjust CSRF logs
This commit is contained in:
Benjamin Caure 2021-11-04 21:13:22 +01:00 committed by GitHub
parent 41298ffcf7
commit 502372a1de
8 changed files with 96 additions and 19 deletions

View File

@ -1,18 +1,20 @@
package com.baeldung.spring; package com.baeldung.spring;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView; import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc @EnableWebMvc
@Configuration @Configuration
public class MvcConfig extends WebMvcConfigurerAdapter { @ComponentScan(basePackages = { "com.baeldung.spring" })
public class MvcConfig implements WebMvcConfigurer {
public MvcConfig() { public MvcConfig() {
super(); super();
@ -22,8 +24,6 @@ public class MvcConfig extends WebMvcConfigurerAdapter {
@Override @Override
public void addViewControllers(final ViewControllerRegistry registry) { public void addViewControllers(final ViewControllerRegistry registry) {
super.addViewControllers(registry);
registry.addViewController("/anonymous.html"); registry.addViewController("/anonymous.html");
registry.addViewController("/login.html"); registry.addViewController("/login.html");
@ -35,7 +35,7 @@ public class MvcConfig extends WebMvcConfigurerAdapter {
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/view/react/build/static/"); registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/view/react/build/static/");
registry.addResourceHandler("/*.js").addResourceLocations("/WEB-INF/view/react/build/"); registry.addResourceHandler("/*.js").addResourceLocations("/WEB-INF/view/react/build/");
registry.addResourceHandler("/*.json").addResourceLocations("/WEB-INF/view/react/build/"); registry.addResourceHandler("/*.json").addResourceLocations("/WEB-INF/view/react/build/");
registry.addResourceHandler("/*.ico").addResourceLocations("/WEB-INF/view/react/build/"); registry.addResourceHandler("/*.ico").addResourceLocations("/WEB-INF/view/react/build/");

View File

@ -0,0 +1,31 @@
package com.baeldung.spring;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/rest")
public class RestController {
private static final Logger LOGGER = LoggerFactory.getLogger(RestController.class);
@GetMapping
public ResponseEntity<Void> get(HttpServletRequest request) {
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
LOGGER.info("{}={}", token.getHeaderName(), token.getToken());
return ResponseEntity.ok().build();
}
@PostMapping
public ResponseEntity<Void> post(HttpServletRequest request) {
// Same impl as GET for testing purpose
return this.get(request);
}
}

View File

@ -7,6 +7,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@ -21,11 +22,11 @@ public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(final AuthenticationManagerBuilder auth) throws Exception { protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
// @formatter:off // @formatter:off
auth.inMemoryAuthentication() auth.inMemoryAuthentication()
.withUser("user1").password("user1Pass").roles("USER") .withUser("user1").password("{noop}user1Pass").roles("USER")
.and() .and()
.withUser("user2").password("user2Pass").roles("USER") .withUser("user2").password("{noop}user2Pass").roles("USER")
.and() .and()
.withUser("admin").password("admin0Pass").roles("ADMIN"); .withUser("admin").password("{noop}admin0Pass").roles("ADMIN");
// @formatter:on // @formatter:on
} }
@ -33,11 +34,11 @@ public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(final HttpSecurity http) throws Exception { protected void configure(final HttpSecurity http) throws Exception {
// @formatter:off // @formatter:off
http http
.csrf().disable() .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.authorizeRequests() .authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/anonymous*").anonymous() .antMatchers("/anonymous*").anonymous()
.antMatchers(HttpMethod.GET, "/index*", "/static/**", "/*.js", "/*.json", "/*.ico").permitAll() .antMatchers(HttpMethod.GET, "/index*", "/static/**", "/*.js", "/*.json", "/*.ico", "/rest").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
.and() .and()
.formLogin() .formLogin()

View File

@ -12,7 +12,7 @@
<!-- in order to debug some marshalling issues, this needs to be TRACE --> <!-- in order to debug some marshalling issues, this needs to be TRACE -->
<logger name="org.springframework.web.servlet.mvc" level="INFO" /> <logger name="org.springframework.web.servlet.mvc" level="INFO" />
<logger name="org.springframework.security.web.csrf" level="DEBUG" />
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</root> </root>

View File

@ -1,11 +1,30 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html> <html>
<head></head> <head>
<script src="csrf.js"></script>
</head>
<body> <body>
<h1>This is the body of the sample view</h1> <h1>This is the body of the sample view</h1>
<div>
<h2>CSRF Testing</h2>
<div>
<span>CSRF Token: </span>
<span id="csrf-token"></span>
<input type="checkbox" id="include-csrf" name="include-csrf" checked /><label for="include-csrf">Include token</label>
</div>
<br/>
<div>
<button type="button" onclick="test('GET')">Test GET Request</button>&nbsp;
<button type="button" onclick="test('POST')">Test POST Request</button>
</div>
<br/>
<div><span>Request Result: </span><span id="csrf-result"></span></div><br/>
</div>
<h2>Roles</h2>
<security:authorize access="hasRole('ROLE_USER')"> <security:authorize access="hasRole('ROLE_USER')">
This text is only visible to a user This text is only visible to a user
<br/> <br/> <br/> <br/>
@ -22,5 +41,26 @@
<a href="<c:url value="/perform_logout" />">Logout</a> <a href="<c:url value="/perform_logout" />">Logout</a>
<script language="javascript">
function test(method) {
const includeCsrfCheckbox = document.querySelector('#include-csrf');
const csrfResultDiv = document.querySelector('#csrf-result');
const request = includeCsrfCheckbox.checked === true ? csrfRequest(method) : noCsrfRequest(method);
request
.then((res) => csrfResultDiv.innerText = res.ok ? method + ' Success' : method + ' Failure: ' + res.status)
.catch((err) => csrfResultDiv.innerText = method + ' Failure: ' + err.toString());
}
function csrfRequest(method) {
return fetch('/rest', { headers: { 'X-XSRF-TOKEN': window.getCsrfToken() }, method });
}
function noCsrfRequest(method) {
return fetch('/rest', { method });
}
document.querySelector('#csrf-token').innerText = window.getCsrfToken();
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,3 @@
window.getCsrfToken = () => {
return document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
}

View File

@ -6,6 +6,7 @@
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<script src="%PUBLIC_URL%/csrf.js"></script>
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.

View File

@ -19,7 +19,8 @@ class Form extends Component {
const data = new FormData(this.form) const data = new FormData(this.form)
fetch(this.form.action, { fetch(this.form.action, {
method: this.form.method, method: this.form.method,
body: new URLSearchParams(data) body: new URLSearchParams(data),
headers: { 'X-XSRF-TOKEN': window.getCsrfToken() },
}).then(v => { }).then(v => {
if(v.redirected) window.location = v.url if(v.redirected) window.location = v.url
}) })
@ -61,12 +62,12 @@ class Form extends Component {
) )
) )
const errors = this.renderError() const errors = this.renderError()
return ( return (
<form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} > <form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} >
{inputs} {inputs}
{errors} {errors}
</form> </form>
) )
} }
} }