Spring Boot + Spring Security + Hibernate Configuration Example
This tutorial demonstrates how to configure Spring Security Hibernate and Spring Boot. We secure a simple stateless web service using basic authentication. We configure Spring Security using Spring Java and/or XML Configuration. Finally, we write some JUnit Integration Tests with spring-test, h2 in-memory database and MockMvc.
Project Structure
Let’s start by looking at the project structure.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" 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"> <modelVersion>4.0.0</modelVersion> <groupId>com.memorynotfound.spring.security</groupId> <artifactId>hibernate</artifactId> <version>1.0.0-SNAPSHOT</version> <url>http://memorynotfound.com</url> <name>Spring Security - ${project.artifactId}</name> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Spring Security Configuration
We configure Spring Security using Java Configuration. We configure the DaoAuthenticationProvider by setting the UserDetailsService and the PasswordEncoder. We use the BCryptPasswordEncoder class which uses the BCrypt strong hashing function. Finally, we configure Spring Security to use stateless basic authentication.
package com.memorynotfound.spring.security.config; import com.memorynotfound.spring.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public DaoAuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider auth = new DaoAuthenticationProvider(); auth.setUserDetailsService(userService); auth.setPasswordEncoder(passwordEncoder()); return auth; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider()); } }
Here is the equivalent Spring XML Configuration spring-security-config.xml located in the src/main/resources/ folder.
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <http create-session="stateless"> <intercept-url pattern="/**" access="isAuthenticated()"/> <http-basic/> </http> <mvc:component-scan base-package="com.memorynotfound"/> <beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> <authentication-manager> <authentication-provider user-service-ref="userService"> <password-encoder ref="passwordEncoder"/> </authentication-provider> </authentication-manager> </beans:beans>
Spring Security Hibernate Configuration
We configure JPA with Hiberante using the application.yml file located in the src/main/resources/ folder.
Following properties are configured using the DataSourceProperties class.
- spring.datasource.url – specifies the JDBC url of the database.
- spring.datasource.username – specifies the login user of the database.
- spring.datasource.password – specifies the login password of the database.
Following properties are configured using the JpaProperties class.
- spring.jpa.show-sql – enable logging of SQL statements
- spring.jpa.hibernate.ddl-auto – DDL mode. This is actually a shortcut for the “hibernate.hbm2ddl.auto” property. Default to “create-drop” when using an embedded database, “none” otherwise.
- spring.jpa.database-platform – Name of the target database to operate on, auto-detected by default. Can be alternatively set using the “Database” enum.
- spring.jpa.properties.hibernate.dialect – specifies which SQL dialect to use.
# =============================== # = Hibernate datasource # =============================== spring: datasource: url: jdbc:mysql://localhost:3306/spring_security_hibernate username: root password: # =============================== # = JPA configurations # =============================== jpa: show-sql: true hibernate: ddl-auto: update database-platform: MYSQL properties: hibernate.dialect: org.hibernate.dialect.MySQL5Dialect # =============================== # = Logging configurations # =============================== logging: level: root: WARN com.memorynotfound: DEBUG org.springframework.web: INFO org.springframework.security: INFO
Hibernate POJO Mappings
We are using hibernate annotations to map fields to database columns. Here is the User class.
package com.memorynotfound.spring.security.model; import javax.persistence.*; import java.util.Collection; @Entity @Table(uniqueConstraints = @UniqueConstraint(columnNames = "email")) public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private String password; @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST}) @JoinTable( name = "users_roles", joinColumns = @JoinColumn( name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn( name = "role_id", referencedColumnName = "id")) private Collection<Role> roles; public User() { } public User(String firstName, String lastName, String email, String password, Collection<Role> roles) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.password = password; this.roles = roles; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Collection<Role> getRoles() { return roles; } public void setRoles(Collection<Role> roles) { this.roles = roles; } @Override public String toString() { return "User{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + ", password='" + "*********" + '\'' + ", roles=" + roles + '}'; } }
Here we configured the Role class.
package com.memorynotfound.spring.security.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; public Role() { } public Role(String name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Role{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
Creating User Repository
Create a user repository to lookup the user by email.
package com.memorynotfound.spring.security.repository; import com.memorynotfound.spring.security.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Long> { User findByEmail(String email); }
Implementing Spring UserDetailsService
We need to implement the UserDetailsService to lookup a user from the database.
package com.memorynotfound.spring.security.service; import com.memorynotfound.spring.security.model.Role; import com.memorynotfound.spring.security.model.User; import com.memorynotfound.spring.security.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; 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 java.util.Collection; import java.util.stream.Collectors; @Service public class UserService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if (user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), mapRolesToAuthorities(user.getRoles())); } private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles){ return roles.stream() .map(role -> new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toList()); } }
Simple Rest Service
This rest service maps to the /users URI.
package com.memorynotfound.spring.security.web; import com.memorynotfound.spring.security.model.User; import com.memorynotfound.spring.security.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserRepository userRepository; @GetMapping public ListgetUsers(){ return userRepository.findAll(); } }
Spring Boot
We initialize the application with a default user. We use Spring Boot to start our application.
package com.memorynotfound.spring.security; import com.memorynotfound.spring.security.model.Role; import com.memorynotfound.spring.security.model.User; import com.memorynotfound.spring.security.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ImportResource; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.annotation.PostConstruct; import java.util.Arrays; @SpringBootApplication // uncomment if you want to use Spring XML Configuration // @ImportResource("classpath:spring-security-config.xml") public class Run { @Autowired private BCryptPasswordEncoder passwordEncoder; @Autowired private UserRepository userRepository; @PostConstruct public void init(){ User user = new User( "Memory", "Not Found", "[email protected]", passwordEncoder.encode("password"), Arrays.asList( new Role("ROLE_USER"), new Role("ROLE_ADMIN"))); if (userRepository.findByEmail(user.getEmail()) == null){ userRepository.save(user); } } public static void main(String[] args) { SpringApplication.run(Run.class, args); } }
Spring Security Integration Testing
To verify our configuration we wrote some basic Integration Tests using spring-test, h2 in-memory database, JUnit and MockMvc.
H2 In Memory Database Configuration
For our integration tests, we are using a H2 in memory database. We can configure it in the it-application.yml file, located in the src/test/resources/ folder. We can activate this profile using @ActiveProfiles annotation and Spring’ll automatically load and configure these configurations.
# =============================== # = H2 data source # =============================== spring: datasource: url: jdbc:h2:mem:spring-security-hibernate-test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE platform: h2 username: sa password: # =============================== # = JPA configurations # =============================== jpa: show-sql: true hibernate: ddl-auto: create-drop naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy database-platform: H2
We can initialize our database using the data.sql file located in the src/test/resources/ folder. This’ll initalize our h2 in memory database with data.
INSERT INTO user (id, first_name, last_name, email, password) VALUES (1, 'Memory', 'Not Found', '[email protected]', '$2a$10$RyY4bXtV3LKkDCutlUTYDOKd2AiJYZGp4Y7MPVdLzWzT1RX.JRZyG'); INSERT INTO role (id, name) VALUES (1, 'ROLE_ADMIN'); INSERT INTO role (id, name) VALUES (2, 'ROLE_MANAGER'); INSERT INTO role (id, name) VALUES (3, 'ROLE_USER'); INSERT INTO users_roles (user_id, role_id) VALUES (1, 1); INSERT INTO users_roles (user_id, role_id) VALUES (1, 2);
Spring Security Hibernate H2 Integration Testing
Here is the actual Spring Security Hiberante Integration Test. We use the @ActiveProfiles('it') to enable our special h2 in memory database configuration. Next, we write some tests using MockMvc to demonstrate a successful login and also an invalid login attempt using basic authentication.
package com.memorynotfound.spring.security.test; import org.junit.Test; import org.junit.runner.RunWith; 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.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @ActiveProfiles("it") @AutoConfigureMockMvc @RunWith(SpringJUnit4ClassRunner.class) public class BasicAuthenticationIntegrationTests { @Autowired private MockMvc mockMvc; @Test public void accessWithValidCredentials() throws Exception { this.mockMvc .perform(get("/users").with(httpBasic("[email protected]", "password"))) .andExpect(status().isOk()); } @Test public void accessWithInValidCredentials() throws Exception { this.mockMvc .perform(get("/users").with(httpBasic("[email protected]", "invalidPassword"))) .andExpect(status().is4xxClientError()); } }
Demo
When we access the protected resource, we receive a basic authentication popup.
When the user is authenticated, we can see the following result.
Download
From:一号门
COMMENTS