• Stars
    star
    258
  • Rank 158,189 (Top 4 %)
  • Language
    Java
  • Created about 8 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Spring boot project with preconfigured JPA, spring security, profiles, swagger and other.. to help you kick start your awesome project

Spring-Boot-Starter

Intro

Spring boot makes it easy to get started but putting together features like security, jpa, spring-data, user authentication on both mysql and embedded database , roles and profiles can sometimes take your time away. Main goal is to help start creating backend application to be used as REST API for multiple client applications, like android, iOS , angular and other clients.

Firebase is included in the project, with simple configuration you can allow client apps to authenticate using google token provided in the header. Filter is created and authentication provider that clearly demonstrate how to authenticate user using google token.

Included spring boot starters

There are several included spring boot starters

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • spring-boot-starter-security
  • spring-boot-starter-actuator
  • spring-boot-devtools

Additional libraries used are

  • mysql-connector-java
  • hsqldb
  • org.modelmapper.modelmapper

Web

As a example one resource is created and named TestResource that demonstrates how to

  • use @RestController
  • inject services
  • map methods to URLs
  • inject configurational properties
  • specify the http status values

Exception Handler annotated with @ControllerAdvice is added to handle exceptions with custom response.

For generating the output for client applications modelmapper is used. It converts for example TestEntity to TestJson. Here is the word or two from official website http://modelmapper.org:

"Why ModelMapper? The goal of ModelMapper is to make object mapping easy, by automatically determining how one object model maps to another, based on conventions, in the same way that a human would - while providing a simple, refactoring-safe API for handling specific use cases."

Using modelMapper is super easy to use, here is how to convert List to List.

	Type listType = new TypeToken<List<TestJson>>() {
		}.getType();

	return modelMapper.map(testService.testRepository(), listType);

REST api is split into 3 big sections

  • /api/open
  • /api/client
  • /api/admin

Each of the sections is secured so that users with correct roles can use them

JPA

All of the data means nothing if it can not be persisted, for this problem JPA is used with JpaRepositories enabled. This allows you to quickly create DAO layer. Initially only one entitiy is created the TestEntity, it is super simple with just generated ID of the entity.

	@Entity
	@Table(name = "PMD_TEST")
	public class TestEntity extends AbstractEntity {
		@GeneratedValue(strategy = GenerationType.AUTO)
		@Id
		private Long id;
	}

Spring JpaRepositories are awesome way for creating DAO layer quick and easy, here is how to create JpaRepository for TestEntity that has CRUD abilities.

	public interface TestRepository extends CrudRepository<TestEntity, Long> {
		Collection<TestEntity> findAll();
	}

To utilize the repository all you need to do is inject it

	@Autowired
	private TestRepository testRepository;

Additionally two more entities are used UserEntity and RoleEntity, so that spring security can be setuped and users are allowed to login.

Transactions are enabled by default so now you can add @Transactional annotations freely

Spring Security

Securing the data is important, for this important task spring security is used. Initially there are 3 roles in the system

  • Admin Role
  • User Role
  • Anonymous Role

REST API is secured so that

  • Administrators can call only /api/admin
  • Users can call only /api/client
  • Anonymous users can call only /api/open

Method security is enabled with @EnableGlobalMethodSecurity(securedEnabled = true), this means that you can protect your service layer by annotating the public methods like this

	@Secured(value = Roles.ROLE_USER)
	public TestEntity create() {

Users are authenticated against the database, to setup this service UserService is created, here is the relevant part of the setup where user details service is set to the AuthenticationManagerBuilder.

	@Order(Ordered.HIGHEST_PRECEDENCE)
	@Configuration
	protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {
		private static Logger logger = Logger.getLogger(AuthenticationSecurity.class);

		@Autowired
		@Qualifier(value="UserService")
		private UserDetailsService userService;

		@Override
		public void init(AuthenticationManagerBuilder auth) throws Exception {
			auth.userDetailsService(userService);
		}
	}

In the dao layer two entities are created as mentioned above UserEntity and RoleEntity

@Entity(name = "user")
@Table(name = "USER")
public class UserEntity implements UserDetails {

	private static final long serialVersionUID = 4815877135015943617L;

	@Id()
	@Column(name = "ID_")
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "USERNAME_", nullable = false, unique = true)
	private String username;

	@Column(name = "PASSWORD_", nullable = false)
	private String password;

	@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
	private List<RoleEntity> authorities;
	...
@Entity(name = "ROLE")
@Table(name = "ROLE")
public class RoleEntity implements GrantedAuthority {

	private static final long serialVersionUID = -8186644851823152209L;

	@Id
	@Column(name = "ID_")
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "AUTHORITY_")
	private String authority;

All that remains is to query the database and load the user and prefill the roles if user has some

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserDetails userDetails = userDao.findByUsername(username);
		if (userDetails == null)
			return null;

		Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
		for (GrantedAuthority role : userDetails.getAuthorities()) {
			grantedAuthorities.add(new SimpleGrantedAuthority(role.getAuthority()));
		}

		return new org.springframework.security.core.userdetails.User(userDetails.getUsername(),
				userDetails.getPassword(), userDetails.getAuthorities());
	}

Spring actuator

Actuator is an interesting part of the app, it allows administrators to use REST api and test if the backend is working and what is the status of it. Here are some examples

GET /health
GET /metrics
GET /mappings
GET /beans
GET /trace
GET /dump
GET /heapdump
GET /configprops 
GET /autoconfig
GET /info

To demonstrate how to create custom health indicator there is PomodoroHealthIndicator as a demonstration.

Spring actuator allows you to monitor the application by creating counts. In the TestService every time when Pomodoro is created counter service is called like this

counterService.increment("rs.pscode.pomodoro.created");

Now when we call /metrics we can see how many pomodors are created.

Devtools

Devtools can help you develop, one feature is currently used directly so that you do not need to stop start the app all the time during development
spring.devtools.restart.enabled=true

Profiles

There are two profiles that are enabled in the system

  • prod
  • dev

For each profile application-{env}.properties file is created. In each file you can add your properties that depend of the profile that is enabled. Enabling profile is done in the applicatin.properties file

spring.profiles.active=dev

application-prod.properties has mysql access specified but application-dev.properties has no mysql access specified, this is a nice way to tell spring to use embedded database for dev environment. Here is the application-prop.properties, this tells spring boot that we want to have datasource that connects to mysql.

env=prod 

spring.datasource.url=jdbc:mysql://localhost:3306/pomodorofire
spring.datasource.username=pomodorofire
spring.datasource.password=pomodorofire1

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop

Swagger 2

Swagger 2 is enabled only in 'dev' profile.

@Configuration
@EnableSwagger2
public class SwaggerConfig {
	@Bean
	@Profile("dev")
	public Docket api() {
		return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
				.paths(PathSelectors.any()).build();
	}
	
	@Bean
	@Profile("prod")
	public Docket apiProd() {
		return new Docket(DocumentationType.SWAGGER_2).enable(false).select().apis(RequestHandlerSelectors.any())
				.paths(PathSelectors.any()).build();
	}
}

For swagger especial configuration is added in spring security section and WebSecurityConfigurerAdapter

		
@Override
   public void configure(WebSecurity web) throws Exception {
	        web.ignoring().antMatchers("/v2/api-docs",
		                           "/configuration/ui", 
					   "/swagger-resources",
					   "/configuration/security",
					   "/swagger-ui.html",
					   "/webjars/**");
	    }

Swagger UI is accessible on http://localhost:8080/swagger-ui.html

##Swagger code-gen

To generate retrofit2 client code you should download swagger.jar and execute it using next command

java -jar swagger.jar generate -i http://localhost:8080/v2/api-docs  -l java --library=retrofit2 -DmodelPackage=rs.pscode.start.model,apiPackage=rs.pscode.start.api -o spring-boot-starter-genereated

Therea are several parameters used

  • -i Indicates the URL to the api-docs
  • -l language, java
  • --library , retrofit2
  • modelPacakge and apiPackage
  • -o , output folder

Firebase integration

Firebase is integrated and it allows clients that are authenticated by firebase to access REST APIs. There are several parts of the integration

  • Maven Dependencies
  • Filter
  • Authentication provider
  • Token parsing
  • Registration

Firebase can be enabled by setting

rs.pscode.firebase.enabled=true/false

in application.properties. This will register especial FirebaseFilter and authentication provider.

Dependencies

<dependency>
	<groupId>com.google.firebase</groupId>
	<artifactId>firebase-server-sdk</artifactId>
	<version>3.0.1</version>
</dependency>

Firebase filter

Filter checks if header named "X-Authorization-Firebase" exists, if does not this means that request should not be processed by this filter. When token is found it is parsed using firebaseService.parseToken(xAuth) and correct Authentication is created FirebaseAuthenticationToken that is put in the securityContextHolder. Job of the filter is here done and authentication process is being taken over by authentication provider.

public class FirebaseFilter extends OncePerRequestFilter {

	private static String HEADER_NAME = "X-Authorization-Firebase";

	private FirebaseService firebaseService;

	public FirebaseFilter(FirebaseService firebaseService) {
		this.firebaseService = firebaseService;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		String xAuth = request.getHeader(HEADER_NAME);
		if (StringUtil.isBlank(xAuth)) {
			filterChain.doFilter(request, response);
			return;
		} else {
			try {
				FirebaseTokenHolder holder = firebaseService.parseToken(xAuth);

				String userName = holder.getUid();

				Authentication auth = new FirebaseAuthenticationToken(userName, holder);
				SecurityContextHolder.getContext().setAuthentication(auth);

				filterChain.doFilter(request, response);
			} catch (FirebaseTokenInvalidException e) {
				throw new SecurityException(e);
			}
		}
	}

}

Authentication provider

Authentication provider is made so that we map the firebase user to our database. Provider only supports FirebaseAuthenticationToken.

@Component
public class FirebaseAuthenticationProvider implements AuthenticationProvider {

	@Autowired
	@Qualifier(value = UserServiceImpl.NAME)
	private UserDetailsService userService;

	public boolean supports(Class<?> authentication) {
		return (FirebaseAuthenticationToken.class.isAssignableFrom(authentication));
	}

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		if (!supports(authentication.getClass())) {
			return null;
		}

		FirebaseAuthenticationToken authenticationToken = (FirebaseAuthenticationToken) authentication;
		UserDetails details = userService.loadUserByUsername(authenticationToken.getName());
		if (details == null) {
			throw new FirebaseUserNotExistsException();
		}

		authenticationToken = new FirebaseAuthenticationToken(details, authentication.getCredentials(),
				details.getAuthorities());

		return authenticationToken;
	}

}

Firebase Token parser

Firebase parse validates the token and returns instance of especial class FirebaseTokenHolder.

public class FirebaseParser {
	public FirebaseTokenHolder parseToken(String idToken) {
		if (StringUtil.isBlank(idToken)) {
			throw new IllegalArgumentException("FirebaseTokenBlank");
		}
		try {
			Task<FirebaseToken> authTask = FirebaseAuth.getInstance().verifyIdToken(idToken);

			Tasks.await(authTask);

			return new FirebaseTokenHolder(authTask.getResult());
		} catch (Exception e) {
			throw new FirebaseTokenInvalidException(e.getMessage());
		}
	}

One of the key parts of the firebase integration is the application initialization. To complete the initialization you must downlod json from firebase console , check this link https://firebase.google.com/docs/server/setup .

@Configuration
public class FirebaseConfig {

	@Bean
	public DatabaseReference firebaseDatabse() {
		DatabaseReference firebase = FirebaseDatabase.getInstance().getReference();
		return firebase;
	}

	@Value("${rs.pscode.firebase.database.url}")
	private String databaseUrl;

	@Value("${rs.pscode.firebase.config.path}")
	private String configPath;

	@PostConstruct
	public void init() {

		/**
		 * https://firebase.google.com/docs/server/setup
		 * 
		 * Create service account , download json
		 */
		InputStream inputStream = FirebaseConfig.class.getClassLoader().getResourceAsStream(configPath);

		FirebaseOptions options = new FirebaseOptions.Builder().setServiceAccount(inputStream)
				.setDatabaseUrl(databaseUrl).build();
		FirebaseApp.initializeApp(options);
		
	}
}

TODO

Add unit tests