Employee Microservice - Resource Server
Employee Microservice as Resource Server
The idea is to create a simple spring boot application which provides a basic CRUD operation for the employee object. Once the application is up and running, we can transform the application to act as a resource server which would force it to accept requests only with valid Oauth tokens.
To read more about the resource server, refer the following article:
Generate project structure using spring initializer:
Generate a spring boot project using spring initializer:
with the following dependencies:
- Spring Data JPA
- Oauth2 Resource Server
- Spring Web
- Dozer : For mapping between objects
- Spring-Oauth2 : Spring security using Oauth2
- H2 : Serves as our in-memory database.
Final List of dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>net.sf.dozer</groupId> <artifactId>dozer</artifactId> <version>5.5.1</version> </dependency> <dependency> <groupId>io.craftsman</groupId> <artifactId>dozer-jdk8-support</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
Project Structure
Important classes / configuration are as follows:
- Employee Entity
@Entity @DynamicUpdate(true) @JsonIgnoreProperties(ignoreUnknown = true) @Table(name = "Employee") public class Employee extends BaseEntity { /** * */ private static final long serialVersionUID = -8067210052397923732L; @NotNull @Column(name = "first_name") String firstName; @NotNull @Column(name = "last_name") String lastName; @NotNull @Column(name = "dob") @JsonFormat(pattern = "dd.MM.yyyy") LocalDate dateOfBirth; @NotNull @Column(name = "employee_id", unique = true) Long employeeId;
- Employee Service
@Service public class EmployeeService { @Autowired EmployeeRepo employeeRepo; @Autowired @Qualifier("dozerWithMapping") DozerBeanMapper dozerWithMapping; @Transactional(readOnly = true) @PreAuthorize("hasAuthority('READ_EMPLOYEE')") public EmployeeDTO findEmployeeById(Long employeeId) { Employee employee = employeeRepo.findById(employeeId).orElse(null); if (employee == null) { throw new BusinessException("Employee Not Found", "Employee with id: " + employeeId + " not found"); } else { return dozerWithMapping.map(employee, EmployeeDTO.class); } } @Transactional(readOnly = true) @PreAuthorize("hasAuthority('READ_ALL_EMPLOYEE')") public Page<EmployeeDTO> findAllEmployees(Pageable pageable) { Page<Employee> employees = employeeRepo.findAll(pageable); return employees.map(this::convertDtoPage); } private EmployeeDTO convertDtoPage(Employee employee) { return dozerWithMapping.map(employee, EmployeeDTO.class); } @Transactional(propagation = Propagation.REQUIRED) @PreAuthorize("hasAuthority('CREATE_EMPLOYEE')") public void createEmployee(EmployeeDTO employeeDTO) { employeeRepo.save(dozerWithMapping.map(employeeDTO, Employee.class)); } }
- Employee Repo
public interface EmployeeRepo extends JpaRepository{ @Query(value = "select e from Employee e order by e.id desc") Page<Employee> findAll(Pageable pageable); }
- Exception Handling
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler { public static final String APPLICATION_ERROR_JSON = "application/error+json"; @ExceptionHandler(BusinessException.class) public HttpEntity<ErrorDetail> handleBusinessException(BusinessException e, final HttpServletRequest request) { ErrorDetail problem = new ErrorDetail(e.getSummary(), e.getMessage()); problem.setStatus(HttpStatus.NOT_FOUND.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.NOT_FOUND); } @ExceptionHandler(HttpMessageNotReadableException.class) public HttpEntity<ErrorDetail> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, final HttpServletRequest request) { final ErrorDetail problem = new ErrorDetail("Message cannot be converted", String.format("Invalid request body: %s", e.getMessage())); problem.setStatus(HttpStatus.BAD_REQUEST.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.BAD_REQUEST); } @ExceptionHandler(AccessDeniedException.class) public HttpEntity<ErrorDetail> handleAccessDeniedException(AccessDeniedException e, final HttpServletRequest request) { final ErrorDetail problem = new ErrorDetail( "Access Denied - You do not have permission to access the operation", e.getMessage()); problem.setStatus(HttpStatus.FORBIDDEN.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.FORBIDDEN); } @ExceptionHandler(AuthenticationCredentialsNotFoundException.class) public HttpEntity<ErrorDetail> handleCredentialsNotFound(AuthenticationCredentialsNotFoundException e, final HttpServletRequest request) { final ErrorDetail problem = new ErrorDetail( "Access Denied - You do not have permission to access the operation", e.getMessage()); problem.setStatus(HttpStatus.FORBIDDEN.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.FORBIDDEN); } @ExceptionHandler(javax.validation.ConstraintViolationException.class) public HttpEntity<ErrorDetail> handleConstraintViolationsFromJavax(javax.validation.ConstraintViolationException e, final HttpServletRequest request) { final ErrorDetail problem = new ErrorDetail("Constraint Violation", e.getMessage()); problem.setStatus(HttpStatus.CONFLICT.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.CONFLICT); } /** * Handles all unexpected situations * * @param e any exception of type {@link Exception} * @return {@link ResponseEntity} containing standard body in case of errors */ @ExceptionHandler(Exception.class) public HttpEntity<ErrorDetail> handleException(Exception e, final HttpServletRequest request) { ErrorDetail problem = new ErrorDetail("Internal Error", "An unexpected error has occurred"); problem.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); return new ResponseEntity<>(problem, updateContentType(), HttpStatus.INTERNAL_SERVER_ERROR); } private HttpHeaders updateContentType() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Content-Type", APPLICATION_ERROR_JSON); return httpHeaders; } }
- Enabling Resource Server Configuration:
To make the Employee application to behave as a Resource server, the following changes are required :
Create Keystore:
The OAuth2.0 Server signs the tokens using a private key, and the resource server can verify the token using the Server’s public key.
In our example, when we create the authorization server (in the next section), it should contain the keystore and all the resource server should contain the public keys.
For our demo-application, we can create the public and private key by the following steps:
1) Generating key pair:
> keytool -genkeypair -alias microservice -keyalg RSA -keypass microservice -keystore c:\prashant\microservices.jks -storepass microservice
The generated keystore should be saved as this is required by the Oauth Server.
2) Extracting public key: (in bash)
keytool -list -rfc --keystore c:/prashant/microservices.jks | openssl x509 -inform pem -pubkey >> c:/prashant/microservices_public_key.txt
The generated public key file contains public key and certificate. We will open the text file and extract only the public key and save it in the employee apllications application.yml file as follows:
Resource Server & Security Configuration :
@Configuration @EnableResourceServer public class OAuth2ResourceServerConfigJwt extends ResourceServerConfigurerAdapter { @Value("${security.oauth2.resource.jwt.keyValue}") String oauthPublicKey; @Override public void configure(final ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(tokenStore()); } @Bean @Primary public DefaultTokenServices tokenServices() { final DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPublicKeyAsString()); return converter; } private String getPublicKeyAsString() { return oauthPublicKey; } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Enabling Pre-Authorize annotation
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
}
Conclusion
With these changes, we have enabled the Employee microservice application to act as a Resource Server. This would mean that the applciation resources / endpoints can only be accessed with a valid Oauth2 token.
To test this we can create a request in post man as follows:
Note: The in-memory database has some pre-loaded entries. This is achieved by adding records via Dataloader:
@Component
public class DataLoader implements ApplicationRunner
Source Code :
The entire source code can be found in Git Hub
Comments
Post a Comment