The industry-level architecture of a web-service application developed in spring boot 2.5.2.
- MVC Architecture
- JWT Based Authentication
- Spring Data (JPA)
- Application User Password Encryption
- DB password Encryption.
- SQL Server
- Sl4j
- Swagger For API Doc
- Application Source code.
- SQl script of Data Base along with key data.
- DB.txt file contains the DB config details.
- Postman JSON script to test all web-services.
- Install JDK 14 or latest.
- Clone the Project repository into local.
- Install SQL server 2012.
- Create application DB and user
- Insert the DB key data.
- Add the decoding key of the database password into the system variables. It is present in DB.txt file.
- Sometimes we may need to restart the windows to pick up the updated system variables.
- Run the project source code.
- To call the web-services, import provided postman JSON scripts into the postman client application.
Each Web-services of application will be declared in the controller layer.
@RequestMapping("/api/v1/user")
@RestController
@Validated
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private GeneralServices generalServices;
@Autowired
private UserService userService;
/**
* Web service to create new user
*
* @param httpServletRequest
* @param user
* @return
*/
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> createUser(HttpServletRequest httpServletRequest,
@Valid @RequestBody UserCreateModel user) {
logger.debug("<--- Service to save new user request : received --->");
ApiSuccessResponse apiResponse = userService.createUser(user, generalServices.getApiRequestedUserId(httpServletRequest));
logger.debug("<--- Service to save new user response : given --->");
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
}
- @RequestMapping("/api/v1/user") annotation used to mention the category of web service.
- @RestController annotation will configure the class to receive the rest-full web service call.
- @PostMapping() annotation will decide the HTTP request type.
- consumes & consumes tags will decide the content type of the HTTP request and response.
From this "controller layer" API request will be taken to the service layer. All business logic will be handled here, then it will talk with the database using JPA.
Whenever any exception happened, it will throw from the respective classes and be handled in the "CommonExceptionHandlingController". We have to handle separately for each type of exception. This function is performed with the help of "ControllerAdvice" named annotation.
@ControllerAdvice
public class CommonExceptionHandlingController extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CommonExceptionHandlingController.class);
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException httpRequestMethodNotSupportedException,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiErrorResponse(Constants.WRONG_HTTP_METHOD,
Constants.WRONG_HTTP_METHOD_ERROR_MESSAGE, Calendar.getInstance().getTimeInMillis()));
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException methodArgumentNotValidException,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiErrorResponse(Constants.MANDATORY_FIELDS_ARE_NOT_PRESENT_CODE,
Constants.MANDATORY_FIELDS_ARE_NOT_PRESENT_ERROR_MESSAGE, Calendar.getInstance().getTimeInMillis()));
}
--------
--------
- All interaction of the application with the database will handle by the JPA library.
- JPA will have Entity class and corresponding Repository interface for all logical objects in the application.
@Entity
@Table(name = "tbl_users")
public class Users implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = "bigint")
private Long id;
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_account_id", columnDefinition = "bigint", nullable = false)
private UserAccounts userAccount;
--------
--------
public interface UserRepository extends JpaRepository<Users, Long> {
/**
* To find user object using username
*
* @param username
* @return
*/
Users findByUserAccountUsername(String username);
---------
---------
- Other JPA configurations will be done in application.properties named file.
spring.jpa.show-sql=false
spring.jpa.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect
spring.jpa.hibernate.ddl-auto = update
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.properties.hibernate.use_sql=true
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider
- Database name will be present in the application.properties file.
- And other information like connection URL, user credentials will be mentioned in two different other property files.
application-dev.properties - It will have the configurations which we used for the development.
application-pro.properties - It will have the configurations which we used for the production.
spring.profiles.active=dev
- Above mentioned property configuration will be present in the main "application.properties" file.
- It will decide which sub property file should load to the system(dev or pro).
#DB config
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
#DB config
spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=sample_webservice_db_dev
spring.datasource.username=dbuser
spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)
#spring.datasource.username=dbuser
#spring.datasource.password=dbuserpassword
#DB config
spring.datasource.url=jdbc:sqlserver://192.168.1.119:1433;databaseName=sample_webservice_db
spring.datasource.username=proUser
spring.datasource.password=ENC(proUserPswd)
- Application database password will be encrypted using Jasypt library with the help of a encryption key.
- This encryption key needs to add in the computer system variables of environmental variables under "JASYPT_ENCRYPTOR_PASSWORD" named key.
- We have to mention encrypted database password in the property file as follows. This is how the system will understand the password needs to be decrypted using secret key which is added in the system variables.
spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)
- For the Jasypt decryption we need to mention default encryption configuration in the property file as follows.
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
@SpringBootApplication
@EnableEncryptableProperties
public class SampleWebservice extends SpringBootServletInitializer {
--------
--------
- We also provide @EnableEncryptableProperties annotation in the application main class to let the application know about this database password encryption configuration.
- We implemented JSON Web Token-based authentication with the help of spring security.
- Upon success logged in of a user, we will create two tokens (accessToken && refreshToken) and send back to the client.
- accessToken will be created using a privet key, expiry time (1 hr), user id and role name.
- refreshToken will be created using a privet key , expiry time (24 hr), user id and role name.
- After success login each API request needs to have this accessToken in the header under Authorization key.
- A "bearer" named key should be attached at the starting of the access token like follows.
- "bearer accessToken"
- Access token will keep monitor in every web-service request.
- If the validity of the access token expires we revert the request with 401 HTTP status.
- At that moment web-service user (client) needs to call access token renewal request using the refresh token.
- Then we will check the validity of refresh token, if it is not expired we will give a new access token and refresh token.
- Client can continue using these new tokens.
- If the validity of the refresh token also expired we ask them to re login using username and password.
@Override
public ApiSuccessResponse userLoginService(String username, String password) {
Tokens tokens = null;
Users user = userService.findByUsername(username);
if (user != null) {
if (passwordEncryptingService.matches(password,
user.getUserAccount().getPassword())) {
if (user.getUserAccount().getStatus() == Constants.ACTIVE_STATUS) {
String roleName = user.getUserAccount().getUserRole().getRoleName();
// Creating new tokens
try {
tokens = createTokens(user.getUserAccount().getId().toString(), roleName);
} catch (Exception exception) {
logger.error("Token creation failed : ", exception);
throw new UnknownException();
}
// Validating tokens
if (validationService.validateTokens(tokens)) {
tokens.setUserId(user.getUserAccount().getId());
return new ApiSuccessResponse(tokens);
} else {
throw new UnknownException();
}
} else {
return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USER_ACCOUNT_IS_INACTIVE_ERROR_CODE,
Constants.USER_ACCOUNT_IS_INACTIVE_ERROR_MESSAGE));
}
} else {
return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_CODE,
Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_MESSAGE));
}
} else {
return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_CODE,
Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_MESSAGE));
}
}
@Override
public ApiSuccessResponse createNewAccessTokenUsingRefreshToken(String refreshToken) {
Tokens tokens = null;
UserAccounts userAccount = null;
AppConfigSettings configSettings = appConfigSettingsService.findByConfigKeyAndStatus(Constants.JWT_SECRET_KEY,
Constants.ACTIVE_STATUS);
// Validate Refresh token
userAccount = jwtTokenHandler.validate(configSettings.getConfigValue(), refreshToken);
if (userAccount != null) {
// Creating new tokens if provided refresh token is valid
try {
tokens = createTokens(userAccount.getId().toString(), userAccount.getRole());
} catch (Exception exception) {
logger.error("Token creation failed : ", exception);
throw new UnknownException();
}
if (validationService.validateTokens(tokens)) {
tokens.setUserId(userAccount.getId());
return new ApiSuccessResponse(tokens);
} else {
throw new UnknownException();
}
} else {
return new ApiSuccessResponse(new ApiResponseWithCode(Constants.REFRESH_TOKEN_EXPIRED_ERROR_CODE,
Constants.REFRESH_TOKEN_EXPIRED_ERROR_MESSAGE));
}
}
- In the above code userLoginService named method will check the credentials of the user and providing tokens if it is valid.
- createNewAccessTokenUsingRefreshToken named method will create the new access token and refresh token upon the success refresh token validation.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationProvider authenticationProvider;
@Autowired
private JwtAuthenticationEntryPoint entryPoint;
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(authenticationProvider));
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilter() {
JwtAuthenticationTokenFilter filter = new JwtAuthenticationTokenFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new JwtSuccessHandler());
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling().authenticationEntryPoint(entryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class)
.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.headers().cacheControl();
}
}
- This configuration will enable the spring security module using @EnableWebSecurity AND @EnableGlobalMethodSecurity(prePostEnabled = true) named annotations.
- Here we will inject then JWT filter into the HTTP request of the system.
public class JwtAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private GeneralServices generalServices;
public JwtAuthenticationTokenFilter() {
super("/api/**");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
-------
--------
}
- Here in the above class JwtAuthenticationTokenFilter() named method will filter all incoming web-service requests who have "api" named keyword in the URL.
- All filtered web-service requests will reach attemptAuthentication named method.
- And we can do all our business logic in this method.
- All passwords of the users in this application will be encrypted for security using BCrypt.
public class PasswordEncryptingService {
public String encode(CharSequence rawPassword) {
return BCrypt.hashpw(rawPassword.toString(), BCrypt.gensalt(6));
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
- Here encode named method is used to encrypt the password.
- And matches named method is using to cross-check the provided password and actual password of the user.
- We have one XML file to configure the Log named by logback-spring.xml.
- To log information from each class, we need to inject the respective class to Slf4j.
@Service("UserService")
@Scope("prototype")
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
- Above code snippet shows how we inject the class into the logger.
- Following are the basic methods to log the information.
- logger.error("Error");
- logger.info("Info");
- logger.warn("Warn");
- API doc has an important role in the web-service application.
- Previously we used to create API doc using any static excel documents
- This library will help us to create the API doc using some annotations inside the application.
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.swagger.version}</version>
</dependency>
- These are the libraries we used in the pom file to integrate Swagger.
- We need to do some configurations in the applications to enable the API doc.
@Configuration
@EnableSwagger2
public class SwaggerAPIDocConfig {
public static final Contact DEFAULT_CONTACT = new Contact("Demo", "http://www.demo.ae/",
"[email protected]");
public static final ApiInfo DEFAUL_API_INFO = new ApiInfo("Sample Application",
"Sample Application description.",
"1.0.0",
"http://www.sampleapplication.ae/",
DEFAULT_CONTACT, "Open licence",
"http://www.sampleapplication.ae/#license",
new ArrayList<VendorExtension>());
private static final Set<String> DEFAULT_PRODICERS_AND_CONSUMERS =
new HashSet<>(Arrays.asList("application/json", "application/xml"));
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(DEFAUL_API_INFO)
.produces(DEFAULT_PRODICERS_AND_CONSUMERS)
.consumes(DEFAULT_PRODICERS_AND_CONSUMERS)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}
}
- As we can see in the above class need to add some basic information about our project.
- We need to tell swagger from which class needs to create API docs, and that is configured under .apis(RequestHandlerSelectors.withClassAnnotation,(RestController.class)) named line.
- Swagger API doc will be accessible from http://localhost:8080/sampleWebService/apidoc this link.
- We can find 2 Postman JSON script in the repository. Please import both of them into the Postman client application.
- Execute the login web-service request at first. Then execute the rest of the web-services.
MIT License
Copyright (c) 2020 Vishnu Viswambharan
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.