BAEL-1918 Spring StrictHttpFirewall and RequestRejectedException (#11265)

* BAEL-1918: Added code to for RequestRejectedException and StrictHttpFirewall tutorial

* BAEL-1918: Added code to for RequestRejectedException and StrictHttpFirewall tutorial

* BAEL:1918 - Modifed the code to accomodate comments from Michal

* BAEL:1918 - Modifed the code to accomodate comments from Michal
This commit is contained in:
Bhaskara 2021-09-30 03:19:24 +05:30 committed by GitHub
parent 102f09fc6c
commit be966d6d62
13 changed files with 737 additions and 2 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-web-boot-3</artifactId> <artifactId>spring-security-web-boot-3</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
@ -25,6 +25,20 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,15 @@
package com.baeldung.httpfirewall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HttpFirewallApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(HttpFirewallApplication.class);
application.setAdditionalProfiles("httpfirewall");
application.run(args);
}
}

View File

@ -0,0 +1,48 @@
package com.baeldung.httpfirewall;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import java.util.Arrays;
@Configuration
public class HttpFirewallConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//@formatter:off
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/error")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
//@formatter:on
}
@Bean
public HttpFirewall configureFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowedHttpMethods(Arrays.asList("GET", "POST", "DELETE", "OPTIONS")); // Allow only HTTP GET, POST, DELETE and OPTIONS methods
return strictHttpFirewall;
}
/*
Use this bean if you are using Spring Security 5.4 and above
*/
@Bean
public RequestRejectedHandler requestRejectedHandler() {
return new HttpStatusRequestRejectedHandler(); // Default status code is 400. Can be customized
}
}

View File

@ -0,0 +1,59 @@
package com.baeldung.httpfirewall.api;
import com.baeldung.httpfirewall.model.Response;
import com.baeldung.httpfirewall.model.User;
import com.baeldung.httpfirewall.service.UserServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/users")
public class UserApi {
private final UserServiceImpl userServiceImpl;
public UserApi(UserServiceImpl userServiceImpl) {
this.userServiceImpl = userServiceImpl;
}
@PostMapping
public ResponseEntity<Response> createUser(@RequestBody User user) {
if (StringUtils.isBlank(user.getId())) {
user.setId(UUID.randomUUID().toString());
}
userServiceImpl.saveUser(user);
Response response = new Response(HttpStatus.CREATED.value(), "User created successfully", System.currentTimeMillis());
URI location = URI.create("/users/" + user.getId());
return ResponseEntity.created(location).body(response);
}
@GetMapping("/{userId}")
public User getUser(@PathVariable("userId") String userId) {
return userServiceImpl.findById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No user exists with the given Id"));
}
@GetMapping()
public List<User> getAllUsers() {
return userServiceImpl.findAll().orElse(new ArrayList<>());
}
@DeleteMapping("/{userId}")
public ResponseEntity<Response> deleteUser(@PathVariable("userId") String userId) {
userServiceImpl.deleteUser(userId);
return ResponseEntity.ok(new Response(200, "The user has been deleted successfully", System.currentTimeMillis()));
}
}

View File

@ -0,0 +1,65 @@
package com.baeldung.httpfirewall.dao;
import com.baeldung.httpfirewall.model.User;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Repository
public class InMemoryUserDao {
private Map<String, User> map = new HashMap<>();
/**
* Persists the user. The user is store in an In-Memory store (a HashMap)
* The default implementation is an In-Memory persistence
* @param user The user that should be persisted
*/
public void save(User user) {
map.put(user.getId(), user);
}
/**
* Finds the user from the in-memory data store.
* The default implementation is an In-Memory persistence
*
* @param userId The ID of the user that has to be fetched
* @return An optional of the requested user
*/
public Optional<User> findById(String userId) {
return Optional.ofNullable(map.get(userId));
}
/**
* Finds all the users from the in-memory data store
* The default implementation is an In-Memory persistence
*/
public Optional<List<User>> findAll() {
return Optional.of(new ArrayList<>(map.values()));
}
/**
* Delete the user from the data store
* The default implementation is an In-Memory persistence
* @param userId The user that has to be deleted
*/
public void delete(String userId) {
map.remove(userId);
}
/**
* Checks if the user exists
* The default implementation is an In-Memory persistence
* @param userId The user that has to be checked for
*/
public boolean isExists(String userId) {
return map.containsKey(userId);
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.httpfirewall.model;
public class Response {
private int code;
private String message;
private long timestamp;
public Response() {
}
public Response(int code, String message, long timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}

View File

@ -0,0 +1,41 @@
package com.baeldung.httpfirewall.model;
public class User {
private String id;
private String username;
private String email;
public User() {
}
public User(String id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

View File

@ -0,0 +1,51 @@
package com.baeldung.httpfirewall.service;
import com.baeldung.httpfirewall.dao.InMemoryUserDao;
import com.baeldung.httpfirewall.model.User;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserServiceImpl {
private final InMemoryUserDao inMemoryUserDao;
public UserServiceImpl(InMemoryUserDao inMemoryUserDao) {
this.inMemoryUserDao = inMemoryUserDao;
}
/**
* Creates a user. Checks if the user already exists and then persists the user
* @param user The user that is to be persisted into the store
*/
public void saveUser(User user) {
inMemoryUserDao.save(user);
}
/**
* Get a user. Returns a user
*
* @param userId The user that has to be fetched form the repository
*/
public Optional<User> findById(String userId) {
return inMemoryUserDao.findById(userId);
}
/**
* Fetch all the users in the store
* @return A list of all the users
*/
public Optional<List<User>> findAll() {
return inMemoryUserDao.findAll();
}
/**
* Delete the user with a given id
* @param userId The identifier of the user
*/
public void deleteUser(String userId) {
inMemoryUserDao.delete(userId);
}
}

View File

@ -0,0 +1,2 @@
spring.security.user.name=user
spring.security.user.password=password

View File

@ -0,0 +1,114 @@
package com.baeldung.httpfirewall.api;
import com.baeldung.httpfirewall.model.User;
import com.baeldung.httpfirewall.service.UserServiceImpl;
import com.baeldung.httpfirewall.utility.UserTestUtility;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("User API Live Tests")
class UserApiLiveTest {
private final String userId = "1";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setup() throws Exception {
//@formatter:off
mockMvc
.perform(post("/api/v1/users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUserWithId(userId)))
.contentType("application/json"));
//@formatter:on
}
@Test
@WithMockUser
@DisplayName("LiveTest User Creation")
void givenCredentials_whenHttpPost_thenReturn201() throws Exception {
// @formatter:off
MvcResult result = mockMvc
.perform(post("/api/v1/users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUserWithId("200")))
.contentType("application/json"))
.andDo(print())
.andExpect(header().exists("Location")).andReturn();
assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus());
// @formatter:on
mockMvc.perform(delete("/api/v1/users/" + 200).contentType("application/json"));
}
@Test
@WithMockUser
@DisplayName("LiveTest Get User")
void givenCredentials_whenHttpGetById_thenReturnUser() throws Exception {
// @formatter:off
MvcResult result=mockMvc
.perform(get("/api/v1/users/"+userId)
.contentType("application/json")).andReturn();
// @formatter:on
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertEquals(userId, objectMapper.readValue(result.getResponse().getContentAsString(), User.class).getId());
}
@Test
@WithMockUser
@DisplayName("LiveTest Get All Users")
void givenCredentials_whenHttpGet_thenReturnAllUsers() throws Exception {
// @formatter:off
MvcResult result=mockMvc
.perform(get("/api/v1/users/")
.contentType("application/json")).andReturn();
// @formatter:on
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertNotNull(result.getResponse().getContentAsString());
List<User> users = objectMapper.readValue(result.getResponse().getContentAsString(), objectMapper.getTypeFactory().constructCollectionType(List.class, User.class));
assertEquals(1, users.size());
}
@Test
@WithMockUser
@DisplayName("LiveTest Delete User")
void givenCredentials_whenHttpDelete_thenDeleteUser() throws Exception {
// @formatter:off
MvcResult result=mockMvc
.perform(delete("/api/v1/users/"+userId)
.contentType("application/json")).andReturn();
// @formatter:on
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertNotNull(result.getResponse().getContentAsString());
}
}

View File

@ -0,0 +1,161 @@
package com.baeldung.httpfirewall.api;
import com.baeldung.httpfirewall.model.User;
import com.baeldung.httpfirewall.service.UserServiceImpl;
import com.baeldung.httpfirewall.utility.UserTestUtility;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@WebMvcTest
@AutoConfigureMockMvc
@DisplayName("User API Unit Tests")
class UserApiUnitTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserServiceImpl userService;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("Test to Check Authentication")
void whenNoAuthentication_thenThrow401() throws Exception {
// @formatter:off
MvcResult result = mockMvc
.perform(post("/api/v1/users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUser()))
.contentType("application/json"))
.andReturn();
assertEquals(HttpStatus.UNAUTHORIZED.value(), result.getResponse().getStatus());
// @formatter:off
}
@Test
@WithMockUser
@DisplayName("Test Malicious URL")
void givenCredentials_whenMaliciousUrl_thenThrowRequestRejectedException() throws Exception {
// @formatter:off
MvcResult result = mockMvc
.perform(post("/api/v1\\users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUser()))
.contentType("application/json"))
.andDo(print())
.andReturn();
assertEquals(HttpStatus.BAD_REQUEST.value(), result.getResponse().getStatus());
// @formatter:on
}
@Test
@WithMockUser
@DisplayName("Test User Create")
void givenCredentials_whenHttpPost_thenReturn201() throws Exception {
// @formatter:off
doNothing().when(userService).saveUser(new User());
MvcResult result=mockMvc
.perform(post("/api/v1/users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUser()))
.contentType("application/json"))
.andDo(print())
.andExpect(header().exists("Location")).andReturn();
assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus());
// @formatter:on
}
@Test
@WithMockUser
@DisplayName("Test User Create Without ID")
void givenCredentials_whenHttpPostWithId_thenReturn201() throws Exception {
// @formatter:off
doNothing().when(userService).saveUser(new User());
MvcResult result = mockMvc
.perform(post("/api/v1/users")
.content(objectMapper.writeValueAsString(UserTestUtility.createUserWithoutId()))
.contentType("application/json"))
.andDo(print())
.andExpect(header().exists("Location")).andReturn();
assertEquals(HttpStatus.CREATED.value(), result.getResponse().getStatus());
// @formatter:on
}
@Test
@WithMockUser
@DisplayName("Test Get User")
void givenCredentials_whenHttpGetWithId_thenReturnUser() throws Exception {
String userId = "1";
// @formatter:off
when(userService.findById("1")).thenReturn(UserTestUtility.createUserWithId(userId));
MvcResult result = mockMvc
.perform(get("/api/v1/users/"+userId)
.accept("application/json"))
.andDo(print())
.andReturn();
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertEquals("jhondoe",objectMapper.readValue(result.getResponse().getContentAsString(), User.class).getUsername());
// @formatter:on
}
@Test
@WithMockUser
@DisplayName("Test Get All Users")
void givenCredentials_whenHttpGetWithoutId_thenReturnAllUsers() throws Exception {
// @formatter:off
when(userService.findAll()).thenReturn(UserTestUtility.createUsers());
MvcResult result = mockMvc
.perform(get("/api/v1/users/")
.accept("application/json"))
.andDo(print())
.andReturn();
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertTrue(result.getResponse().getContentAsString().contains("jane.doe"));
// @formatter:on
}
@Test
@WithMockUser
@DisplayName("Test Delete a User")
void givenCredentials_whenHttpDelete_thenDeleteUser() throws Exception {
String userId = "1";
doNothing().when(userService).deleteUser(userId);
// @formatter:off
MvcResult result = mockMvc
.perform(delete("/api/v1/users/"+userId)
.accept("application/json"))
.andDo(print())
.andReturn();
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
assertNotNull(result.getResponse());
assertTrue(result.getResponse().getContentAsString().contains("The user has been deleted successfully"));
// @formatter:on
}
}

View File

@ -0,0 +1,92 @@
package com.baeldung.httpfirewall.service;
import com.baeldung.httpfirewall.dao.InMemoryUserDao;
import com.baeldung.httpfirewall.model.User;
import com.baeldung.httpfirewall.utility.UserTestUtility;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("UserService Unit Tests")
class UserServiceUnitTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private InMemoryUserDao userDao;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("Check Create User")
void whenCalledCreateUser_thenVerify() {
User user = UserTestUtility.createUser();
doNothing().when(userDao).save(user);
userService.saveUser(user);
verify(userDao, times(1)).save(user);
}
@Test
@DisplayName("Check Get User")
void givenUserId_whenCalledFindById_thenReturnUser() {
User user = UserTestUtility.createUserWithId("1").orElse(new User("1", "jhondoe", "jhon.doe@gmail.com"));
when(userDao.findById(user.getId())).thenReturn(Optional.of(user));
User actualUser = userService.findById("1").get();
assertNotNull(actualUser);
assertEquals("jhondoe", actualUser.getUsername());
verify(userDao, times(1)).findById(user.getId());
}
@Test
@DisplayName("Check Get All Users")
void whenCalledFindAll_thenReturnAllUsers() {
List<User> users = UserTestUtility.createUsers().orElse(new ArrayList<>());
when(userDao.findAll()).thenReturn(Optional.of(users));
Optional<List<User>> actualUsers = userService.findAll();
assertNotNull(actualUsers);
assertEquals(2, users.size());
verify(userDao, times(1)).findAll();
}
@Test
@DisplayName("Check Delete Users")
void givenId_whenCalledDeleteUser_thenDeleteUser() {
User user = UserTestUtility.createUserWithId("1").orElse(new User());
doNothing().when(userDao).delete(user.getId());
userService.deleteUser(user.getId());
verify(userDao, times(1)).delete(user.getId());
}
}

View File

@ -0,0 +1,33 @@
package com.baeldung.httpfirewall.utility;
import com.baeldung.httpfirewall.model.User;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class UserTestUtility {
public static User createUser() {
return new User(UUID.randomUUID().toString(),"jhondoe", "jhondoe@gmail.com");
}
public static User createUserWithoutId() {
return new User("","jhondoe", "jhondoe@gmail.com");
}
public static Optional<User> createUserWithId(String id) {
// @formatter:off
return Optional.of(new User(id, "jhondoe", "jhon.doe@gmail.com"));
// @formatter:on
}
public static Optional<List<User>> createUsers() {
// @formatter:off
return Optional.of(Arrays.asList(
new User(UUID.randomUUID().toString(), "jhondoe","jhon.doe@gmail.com" ),
new User(UUID.randomUUID().toString(), "janedoe","jane.doe@gmail.com" ))
);
// @formatter:on
}
}