Description:
Setting up a contact database reference application with Java8, Spring Boot, Hibernate to serve the the REST services on the backend. These services are secured with Spring Security.
The frontend consists of a one-page application using AngularJS.
The source for this project is available at: https://github.com/robbertvdzon/reference-implementation-hibernate-spring4-angular
Generating a Spring Boot application:
Spring Boot provides several methods to get you started quickly.
For this sample application I went to http://start.spring.io/ and selected “Web”, “JPA”, and “Jersey”.
After pressing “Generate project” a zip file with the sample maven project can then be downloaded.
Change the database properties:
The sample application will not work directly because it misses the database properties.
In my sample application I use a H2 database, which not only persists its value in memory but also on a file so the data will retain also after an application reboot.
To configure H2, use the following properties in the application.properties file:
spring.datasource.url=jdbc:h2:file:./CONTACTDB;FILE_LOCK=FS spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.datasource.continueOnError=true spring.datasource.initialize=true spring.jpa.hibernate.ddl-auto=none
H2 will now create a database file called CONTACTDB.mv.db in the root of the project.
We also need to provide the system with a database schema and sample data.
The database scheme must be placed in the /schema.sql file and contains the following:
create table contacts ( id bigint generated by default as identity, email varchar(255), name varchar(255), user_id bigint, uuid varchar(255), primary key (id) ); create table users ( id bigint generated by default as identity, passwd varchar(255), username varchar(255), permissions varchar(255), uuid varchar(255), primary key (id) ); alter table contacts add constraint FK_contacts_users foreign key (user_id) references users;
We can also insert sample data in these tables by providing the insert sql statements in the data.sql.
Note that the spring.datasource.initialize property in application.properties must be set to true.
Create the entities:
In our reference application we have two entities: users and contacts:
@Entity @Table(name = "users") public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String uuid; private String username; private String passwd; private String permissions; @OneToMany(mappedBy = "userId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) private List contacts; /* all getters and setters here */ }
@Entity @Table(name = "contacts") public class Contact { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String uuid; private Long userId; private String name; private String email;
/* all getters and setters here */ }
Create CRUD repositories:
For our two entities, we like to have a CRUD Repository. With the Spring JPA CRUD Repository, this can be created by only creating an interface.
You can inject these in your application and Spring will provide an implementation for you.
public interface UserRepository extends CrudRepository { }
public interface ContactRepository extends CrudRepository { }
With these repositories you can create, update, delete and find entities by their id values.
In our application we also want to find users by username, and contacts by their uuid and userid.
These extra methods can easily be added, by adding them and annotate the JPA query on it.
@Transactional public interface UserRepository extends CrudRepository { @Query("SELECT u FROM User u WHERE LOWER(u.username) = LOWER(:username)") User findOne(@Param("username") String username); }
@Transactional public interface ContactRepository extends CrudRepository { @Query("SELECT c FROM Contact c WHERE userId = :userID") List listAll(@Param("userID") long userID); @Modifying @Query("DELETE FROM Contact c WHERE c.uuid = :uuid") void deleteContact(@Param("uuid") String uuid); @Query("SELECT c FROM Contact c WHERE uuid = :uuid") Contact getContact(@Param("uuid") String uuid); }
Adding Spring Security to the application:
By adding spring security, users first need to login before they have access to any secured http calls.
There are multiple ways in implementing this, but I decided to provide my own UserDetailService which provides the security mechanism the user details that it uses to authenticate the user. The advantage of this, is that my custom UserDetails object can later be accessed within rest calls to get more information about the logged in user (in my case, I wanted the userID to be available in the REST services).
@Service("userDetailsService") public class UserDetailsServiceImpl implements org.springframework.security.core.userdetails.UserDetailsService { @Inject UserRepository userService; @Override public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { User user = userService.findOne(username); if (user==null) throw new UsernameNotFoundException("user not found"); Set setAuths = new HashSet(); String permissions = user.getPermissions(); for (String permission:permissions.split(",")){ setAuths.add(new SimpleGrantedAuthority(permission)); } UserDetails userDetails = new AuthUser(setAuths,user.getId(),user.getPasswd(), username); return userDetails; } }
This UserDetailsService can then be used in the WebSecurityConfiguration. This class is used to configure the web security.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsService") UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/**").permitAll() .and() .formLogin() ; } }
Note that I have configured permitAll() on all requests. This is because I only want to secure my REST calls and want to use annotations for that.
You can also decide to secure all requests beginning with e.g. /api or only secure (or permit) a few specific pages.
To secure a REST call, you only need to annotate it with e.g. : @PreAuthorize(“hasAuthority(‘USER’)”)
For me to be able to find the userID from the logged in user from within the REST calls, I have created an AuthenticationService that returns the userID.
@Component public class AuthenticationService { public long getUserId() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof AuthUser) { AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return user.getUserID(); } return -1; } }
Creating the REST calls:
To create a REST call, a class must be created which is annotated with the @RestController annotation.
In the contact REST call which is listed below, I use the AuthenticationService to find the id of the user that is logged in. Since the calls are protected by the @PreAutherize annotation, we know for sure that a user is available.
The contact information that is returned to the client is not the contact entity as stored in the database. I would like to hide the ID field and in the future I might decide to change the model that is send to the client without changing the entity. This is why I return a ContactModel instead of a Contact in all calls.
@RestController @PreAuthorize("hasAuthority('USER')") @RequestMapping("/rest/contacts") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class ContactsResource { @Inject ContactRepository contactService; @Inject AuthenticationService authenticationService; @RequestMapping(value = "/getContacts", method = RequestMethod.GET) public ResponseEntity<List> getContacts() throws Exception { long userId = authenticationService.getUserId(); List contacts = ContactModelMapper.toModel(contactService.listAll(userId)); return new ResponseEntity<List>(contacts, HttpStatus.OK); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public ResponseEntity getContact(@PathParam("id") Long id) { return new ResponseEntity(HttpStatus.NOT_IMPLEMENTED); } @RequestMapping(method = RequestMethod.POST) public ResponseEntity<List> saveContact(ContactModel contactModel) throws Exception { System.out.println("SAVE USer:" + contactModel.getName()); Contact contact = contactService.getContact(contactModel.getUuid()); ContactModelMapper.mergeModel(contactModel, contact); contactService.save(contact); long userId = authenticationService.getUserId(); List contactModels = ContactModelMapper.toModel(contactService.listAll(userId)); return new ResponseEntity<List>(contactModels, HttpStatus.OK); } @RequestMapping(method = RequestMethod.PUT) public ResponseEntity<List> addContact(ContactModel model) throws Exception { long userId = authenticationService.getUserId(); System.out.println("ADD USer:" + model.getName()); Contact contact = new Contact(); ContactModelMapper.mergeModel(model, contact); contact.setUserId(userId); contact.setUuid(UUID.randomUUID().toString()); contactService.save(contact); List contactModels = ContactModelMapper.toModel(contactService.listAll(userId)); return new ResponseEntity<List>(contactModels, HttpStatus.OK); } @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public ResponseEntity<List> deleteContact(@PathVariable("id") String uuid) { long userId = authenticationService.getUserId(); contactService.deleteContact(uuid); List contactModels = ContactModelMapper.toModel(contactService.listAll(userId)); return new ResponseEntity<List>(contactModels, HttpStatus.OK); } }
Static files:
The static files with the angularJS frontend are copied to the srcmainresourcesstatic folder.
Run the application:
To run the application you can either use the generated jar file:
java -jar targetcontactdb-0.0.1-SNAPSHOT.jar
Or you can use maven to run the application:
mvn spring-boot:run
The application is available at http://localhost:8080/
Dynamic reloading in IntelliJ:
When you run the application in debug mode, you can reload the classes or static files from the menu: run – reload changed classes