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:
parent
41298ffcf7
commit
502372a1de
|
@ -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/");
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
<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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
window.getCsrfToken = () => {
|
||||||
|
return document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue