diff --git a/pom.xml b/pom.xml
index df7a6cb36..1563420ba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -143,6 +143,10 @@
spring-security-test
test
+
+ org.springframework.boot
+ spring-boot-starter-security
+
diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java
new file mode 100644
index 000000000..05fb8fc3d
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java
@@ -0,0 +1,79 @@
+package guru.sfg.brewery.config;
+
+import guru.sfg.brewery.filter.RestHeaderAuthFilter;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+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.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ public RestHeaderAuthFilter restHeaderAuthFilter(AuthenticationManager authenticationManager){
+ RestHeaderAuthFilter restHeaderAuthFilter = new RestHeaderAuthFilter(new AntPathRequestMatcher("/api/**"));
+ restHeaderAuthFilter.setAuthenticationManager(authenticationManager);
+ return restHeaderAuthFilter;
+ }
+ // By default, Spring Security turns on csrf protection.
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.addFilterBefore(restHeaderAuthFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
+ .csrf().disable();
+ http
+ .authorizeRequests(authorize -> {
+ authorize
+ .antMatchers("/h2-console/**").permitAll() // do not use in production
+ .antMatchers("/", "/webjars/**", "/login", "/resources/**").permitAll()
+ .antMatchers("/beers/find", "/beers*").permitAll()
+ .antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll()
+ .mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
+ } )
+ .authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .formLogin().and()
+ .httpBasic();
+
+ // h2 console config
+ http.headers().frameOptions().sameOrigin();
+ }
+
+ // Configuring In Memory Authentication using Authentication Fluent API.
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.inMemoryAuthentication()
+ .withUser("spring")
+ .password("guru")
+ .roles()
+ .and()
+ .withUser("user")
+ .password("{noop}password")
+ .roles("USER");
+
+ }
+
+ // This is kinda an old way of creating InMemory UserDetails Service. Alternatively, we can use
+ // In Memory Authentication Fluent API. See above
+// @Override
+// @Bean
+// protected UserDetailsService userDetailsService() {
+// UserDetails admin = User.withDefaultPasswordEncoder()
+// .username("spring")
+// .password("guru")
+// .roles("ADMIN")
+// .build();
+// UserDetails user = User.withDefaultPasswordEncoder()
+// .username("user")
+// .password("password")
+// .roles("USER")
+// .build();
+// // InMemoryUserDetailsManager implements UserDetailsService thus overriding default UserDetailsService
+// return new InMemoryUserDetailsManager(admin, user);
+// }
+}
diff --git a/src/main/java/guru/sfg/brewery/domain/security/Authority.java b/src/main/java/guru/sfg/brewery/domain/security/Authority.java
new file mode 100644
index 000000000..c817d49d4
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/domain/security/Authority.java
@@ -0,0 +1,22 @@
+package guru.sfg.brewery.domain.security;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.util.Set;
+
+@Entity
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+public class Authority {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private String role;
+
+ @ManyToMany(mappedBy = "authorities")
+ private Set users;
+}
diff --git a/src/main/java/guru/sfg/brewery/domain/security/User.java b/src/main/java/guru/sfg/brewery/domain/security/User.java
new file mode 100644
index 000000000..28f31dc9a
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/domain/security/User.java
@@ -0,0 +1,36 @@
+package guru.sfg.brewery.domain.security;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.util.Set;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Builder
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Integer id;
+ private String username;
+ private String password;
+ @Singular
+ @ManyToMany(cascade = CascadeType.MERGE)
+ @JoinTable(name = "user_authority",
+ joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")},
+ inverseJoinColumns = {@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")})
+ private Set authorities;
+ // @Builder.Default - для того, чтобы в @Builder можно было задать дефолтное значение
+ @Builder.Default
+ private Boolean accountNonExpired = true;
+ @Builder.Default
+ private Boolean accountNonLocked = true;
+ @Builder.Default
+ private Boolean credentialsNonExpired = true;
+ @Builder.Default
+ private Boolean enabled = true;
+}
diff --git a/src/main/java/guru/sfg/brewery/filter/RestHeaderAuthFilter.java b/src/main/java/guru/sfg/brewery/filter/RestHeaderAuthFilter.java
new file mode 100644
index 000000000..8e75ed2e3
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/filter/RestHeaderAuthFilter.java
@@ -0,0 +1,110 @@
+package guru.sfg.brewery.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * AbstractAuthenticationProcessingFilter - abstract processor of browser-based HTTP-based authentication requests.
+ * The filter requires that you set the authenticationManager property. An authenticationManager is required to process
+ * the authentication request tokens creating by implementing classes. This filter will intercept a request and attempt
+ * to perform authentication from that request if the request URL matches the value of the filterProcessesUrl property.
+ * This behaviour can be modified by overriding the method requiresAuthentication. Authentication is performed by the
+ * attemptAuthentication method, which must be implemented by subclasses.
+ *
+ * If authentication is successful, the resulting Authentication object will be placed into the SecurityContext
+ * for the current thread, which is guaranteed to have already been created by an earlier filter.
+ * */
+@Slf4j
+public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter {
+
+ public RestHeaderAuthFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
+ super(requiresAuthenticationRequestMatcher);
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
+
+ String userName = getUsername(httpServletRequest);
+ String password = getPassword(httpServletRequest);
+
+ if (userName == null){
+ userName = "";
+ }
+ if (password == null){
+ password = "";
+ }
+ log.debug("Authenticating user: {}", userName);
+
+ // An Authentication implementation that is designed for simple presentation of a username and a password.
+ // Needed for work with authentication manager.
+ UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
+ // AuthenticationManager is configured as InMemory in fluent api setting
+ if (!StringUtils.isEmpty(userName)){
+ return this.getAuthenticationManager().authenticate(token);
+ }
+ return null;
+ }
+
+ private String getPassword(HttpServletRequest httpServletRequest) {
+ return httpServletRequest.getHeader("Api-Secret");
+ }
+
+ private String getUsername(HttpServletRequest httpServletRequest) {
+ return httpServletRequest.getHeader("Api-Key");
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest)req;
+ HttpServletResponse response = (HttpServletResponse)res;
+ if (!this.requiresAuthentication(request, response)) {
+ chain.doFilter(request, response);
+ } else {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Request is to process authentication");
+ }
+ Authentication authResult = attemptAuthentication(request, response);
+ try {
+ if (authResult != null){
+ this.successfulAuthentication(request, response, chain, authResult);
+ }else {
+ chain.doFilter(request, response);
+ }
+ } catch (AuthenticationException e){
+ log.error("Authentication Failed");
+ unsuccessfulAuthentication(request, response, e);
+ }
+ }
+ }
+
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
+ }
+ SecurityContextHolder.getContext().setAuthentication(authResult);
+ }
+
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ if (this.log.isDebugEnabled()) {
+ log.debug("Authentication request failed: " + failed.toString(), failed);
+ log.debug("Updated SecurityContextHolder to contain null Authentication");
+ }
+ response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
+ }
+}
diff --git a/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java
new file mode 100644
index 000000000..e1b51ac24
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/repositories/security/AuthorityRepository.java
@@ -0,0 +1,9 @@
+package guru.sfg.brewery.repositories.security;
+
+import guru.sfg.brewery.domain.security.Authority;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AuthorityRepository extends JpaRepository {
+
+
+}
diff --git a/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java b/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java
new file mode 100644
index 000000000..5a27e7802
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/repositories/security/UserRepository.java
@@ -0,0 +1,11 @@
+package guru.sfg.brewery.repositories.security;
+
+import guru.sfg.brewery.domain.security.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository {
+
+ Optional findByUsername(String username);
+}
diff --git a/src/main/java/guru/sfg/brewery/services/JpaUserDetailsService.java b/src/main/java/guru/sfg/brewery/services/JpaUserDetailsService.java
new file mode 100644
index 000000000..6e5ecd393
--- /dev/null
+++ b/src/main/java/guru/sfg/brewery/services/JpaUserDetailsService.java
@@ -0,0 +1,49 @@
+package guru.sfg.brewery.services;
+
+import guru.sfg.brewery.domain.security.Authority;
+import guru.sfg.brewery.domain.security.User;
+import guru.sfg.brewery.repositories.security.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+@Service
+public class JpaUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ // We are using @Transactional for executing this method in one Hibernate context so that we do not need
+ // to configure user to load authorities eagerly (we could alternatively do that).
+ @Transactional
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User user = userRepository.findByUsername(username).orElseThrow(()-> {
+ throw new UsernameNotFoundException("User name: " + username + " not found");
+ });
+ // User из пакета org.springframework.security.core.userdetails.User implements UserDetails interface and is
+ // immutable.
+ return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
+ user.getEnabled(), user.getAccountNonExpired(), user.getCredentialsNonExpired(), user.getAccountNonLocked(),
+ convertToSpringAuthorities(user.getAuthorities()));
+ }
+
+ private Collection extends GrantedAuthority> convertToSpringAuthorities(Set authorities) {
+ if (authorities != null && authorities.size() > 0){
+ return authorities.stream().map(Authority::getRole).map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toSet());
+ }else {
+ return new HashSet<>();
+ }
+ }
+}
diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java
new file mode 100644
index 000000000..f0ce21823
--- /dev/null
+++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java
@@ -0,0 +1,78 @@
+package guru.sfg.brewery.web.controllers;
+
+import guru.sfg.brewery.repositories.BeerInventoryRepository;
+import guru.sfg.brewery.repositories.BeerRepository;
+import guru.sfg.brewery.repositories.CustomerRepository;
+import guru.sfg.brewery.services.BeerService;
+import guru.sfg.brewery.services.BreweryService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/**
+ * Created by jt on 6/12/20.
+ */
+@WebMvcTest
+public class BeerControllerIT {
+
+
+ // WebApplicationContext will allow us to leverage filters used by Spring Security in order to test authentication
+ @Autowired
+ WebApplicationContext wac;
+
+ MockMvc mockMvc;
+
+ @MockBean
+ BeerRepository beerRepository;
+
+ @MockBean
+ BeerInventoryRepository beerInventoryRepository;
+
+ @MockBean
+ BreweryService breweryService;
+
+ @MockBean
+ CustomerRepository customerRepository;
+
+ @MockBean
+ BeerService beerService;
+
+ @BeforeEach
+ void setUp() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(wac)
+ // activates spring security filters
+ .apply(springSecurity())
+ .build();
+ }
+ // Здесь просто создаем мокового юзера с username spring.
+ @WithMockUser("spring")
+ @Test
+ void findBeers() throws Exception{
+ mockMvc.perform(get("/beers/find"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("beers/findBeers"))
+ .andExpect(model().attributeExists("beer"));
+ }
+
+ // Здесь с помощью метода with() теперь проверяем базовую аутентификацию.
+ @Test
+ void findBeersWithHttpBasic() throws Exception{
+ mockMvc.perform(get("/beers/find").with(httpBasic("spring", "guru")))
+ .andExpect(status().isOk())
+ .andExpect(view().name("beers/findBeers"))
+ .andExpect(model().attributeExists("beer"));
+ }
+
+}
diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerTest.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerTest.java
index 0efa7484d..196bef31b 100644
--- a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerTest.java
+++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerTest.java
@@ -30,6 +30,7 @@
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
+import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@@ -105,6 +106,7 @@ void showBeer() throws Exception{
.andExpect(model().attribute("beer", hasProperty("id", is(uuid))));
}
+ AuthenticationEntryPoint
@Test
void initCreationForm() throws Exception {
mockMvc.perform(get("/beers/new"))