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;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
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.JstlView;
|
||||
|
||||
@EnableWebMvc
|
||||
@Configuration
|
||||
public class MvcConfig extends WebMvcConfigurerAdapter {
|
||||
@ComponentScan(basePackages = { "com.baeldung.spring" })
|
||||
public class MvcConfig implements WebMvcConfigurer {
|
||||
|
||||
public MvcConfig() {
|
||||
super();
|
||||
|
@ -22,8 +24,6 @@ public class MvcConfig extends WebMvcConfigurerAdapter {
|
|||
|
||||
@Override
|
||||
public void addViewControllers(final ViewControllerRegistry registry) {
|
||||
super.addViewControllers(registry);
|
||||
|
||||
registry.addViewController("/anonymous.html");
|
||||
|
||||
registry.addViewController("/login.html");
|
||||
|
@ -35,7 +35,7 @@ public class MvcConfig extends WebMvcConfigurerAdapter {
|
|||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/view/react/build/static/");
|
||||
|
||||
|
||||
registry.addResourceHandler("/*.js").addResourceLocations("/WEB-INF/view/react/build/");
|
||||
registry.addResourceHandler("/*.json").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.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
|
@ -21,11 +22,11 @@ public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
|
||||
// @formatter:off
|
||||
auth.inMemoryAuthentication()
|
||||
.withUser("user1").password("user1Pass").roles("USER")
|
||||
.withUser("user1").password("{noop}user1Pass").roles("USER")
|
||||
.and()
|
||||
.withUser("user2").password("user2Pass").roles("USER")
|
||||
.withUser("user2").password("{noop}user2Pass").roles("USER")
|
||||
.and()
|
||||
.withUser("admin").password("admin0Pass").roles("ADMIN");
|
||||
.withUser("admin").password("{noop}admin0Pass").roles("ADMIN");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
@ -33,11 +34,11 @@ public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
protected void configure(final HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.csrf().disable()
|
||||
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/admin/**").hasRole("ADMIN")
|
||||
.antMatchers("/anonymous*").anonymous()
|
||||
.antMatchers(HttpMethod.GET, "/index*", "/static/**", "/*.js", "/*.json", "/*.ico").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/index*", "/static/**", "/*.js", "/*.json", "/*.ico", "/rest").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<!-- 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.security.web.csrf" level="DEBUG" />
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
|
|
@ -1,11 +1,30 @@
|
|||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
|
||||
<html>
|
||||
<head></head>
|
||||
<head>
|
||||
<script src="csrf.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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')">
|
||||
This text is only visible to a user
|
||||
<br/> <br/>
|
||||
|
@ -22,5 +41,26 @@
|
|||
|
||||
<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>
|
||||
</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">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<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.
|
||||
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)
|
||||
fetch(this.form.action, {
|
||||
method: this.form.method,
|
||||
body: new URLSearchParams(data)
|
||||
body: new URLSearchParams(data),
|
||||
headers: { 'X-XSRF-TOKEN': window.getCsrfToken() },
|
||||
}).then(v => {
|
||||
if(v.redirected) window.location = v.url
|
||||
})
|
||||
|
@ -61,12 +62,12 @@ class Form extends Component {
|
|||
)
|
||||
)
|
||||
const errors = this.renderError()
|
||||
return (
|
||||
<form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} >
|
||||
{inputs}
|
||||
{errors}
|
||||
</form>
|
||||
)
|
||||
return (
|
||||
<form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} >
|
||||
{inputs}
|
||||
{errors}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue