In this article, we will develop a full stack app using Spring Boot as backend provider and Angular 8 as a fronted provider which performs different CRUD operations on a User entity. We will have spring data integrated at the DAO layer that performs different DB operations. We will be using MySQL database to save the user details. The REST APIs will have a token-based authentication integrated with Spring Security. At the client side, Angular routes will be protected and we will have Angular interceptors to add the token in every HTTP request.
You can follow this article for Angular 8 integration with Spring Boot.
Angular 8 Client-Side Architecture
With the release of Angular 8, there are many new features that have been introduced such as Ivy Preview, Web Workers, Lazy Loading, Differential Loading, etc. The new version requires TypeScript 3.4+ and Node 12+. We will be using Angular 8 at the client side.
We will have 4 different routes. The login route will be the welcome page that accepts username and password for login. On submit, the login API will be called and in response of the API, client receives a JWT token which is cached in the browser local cache so that any further HTTP request can be injected with this token in the header. We will have an interceptor implemented to perform this injection.
After login, the list route will be loaded which will again make a REST call to fetch user list and this list will be rendered in the browser. This screen will have options to add new user, edit and delete an existing user.
A similar implementation with other Angular versions can be found here - Angular 6 Implementation and Angular 7 Implementation.
Spring Boot Server-Side Architecture
At the server-side, we will be using Spring Boot 2 to expose our REST endpoints. We will have Spring Security integrated with Spring Data to perform DB operations. All the APIs will be non-reactive APIs. You can visit my previous article to create reactive REST API using Spring Boot 2.
At the server side, we have a security filter defined that is responsible for intercepting all the requests to extract JWT token from the HTTP header and set the security context. You can follow this article to create a full-fledged JWT token-based authentication system using Spring Security.
After this the request will reach the controllers. We have seperate controllers defined for authentication related stuff and for user CRUD related stuff. The authentication related APIs are not secured but all the user related APIs are secured. The controller commands to the service layer to perform the business logic which makes DAO calls if required.
Generating Angular 8 Project from CLI
Angular 8 requires TypeScript 3.4+ and Node 12+ and hence make sure your local system has these software settings present. To start Angular 8 application from scratch, you can follow my previous article here.
Execute below commands to generate an Angular 8 project with CLI.
ng new angular8-demo --enable-ivy cd angular8-demo/ ng serve
Below is our final project structure.
Generating Spring Boot Project
You can download a sample project from start.spring.io with spring-boot-starter-web, spring-boot-starter-data-jpa and spring-boot-starter-security and import it into your favourite IDE. Below is the final project structure.
Spring Boot Maven Dependency
Below is our final pom.xml if you simply want to add dependencies in your existing project.
pom.xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Angular 8 Client Side Implementation
Angular 8 Components
Below are the lists of commands that we will execute to generate our components from CLI.
ng g component login ng g component user/list-user ng g component user/add-user ng g component user/edit-user
Now, let us start implementing these components step by step.
Login Component
As you can see, the login component has a reactive login form that takes username and password input from the user. On the click of Login button, onSubmit()
function is called. This function then calls the service component so that the username/password can be validated from the API. Even this request will be intercepted by our HTTP interceptor but the token will not be added in the header.
<div class="row"> <div class="col-md-6 login-container"> <h2 style="margin: auto">Login </h2> <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label for="username">UserName:</label> <input type="text" class="form-control" formControlName="username" id="username" autocomplete="off"> <div class="error" *ngIf="loginForm.controls['username'].hasError('required') && loginForm.controls['username'].touched">Username is required</div> </div> <div class="form-group"> <label for="pwd">Password:</label> <input type="password" class="form-control" formControlName="password" id="pwd" autocomplete="off"> <div class="error" *ngIf="loginForm.controls['password'].hasError('required') && loginForm.controls['password'].touched">Password is required</div> </div> <button class="btn btn-success" [disabled]="loginForm.invalid">Login</button> <div *ngIf="invalidLogin" class="error"> <div>Invalid credentials.</div> </div> </form> </div> </div>login.component.ts
import { Component, OnInit } from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {Router} from "@angular/router"; import {ApiService} from "../service/api.service"; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm: FormGroup; invalidLogin: boolean = false; constructor(private formBuilder: FormBuilder, private router: Router, private apiService: ApiService) { } onSubmit() { if (this.loginForm.invalid) { return; } const loginPayload = { username: this.loginForm.controls.username.value, password: this.loginForm.controls.password.value } this.apiService.login(loginPayload).subscribe(data => { debugger; if(data.status === 200) { window.localStorage.setItem('token', data.result.token); this.router.navigate(['list-user']); }else { this.invalidLogin = true; alert(data.message); } }); } ngOnInit() { window.localStorage.removeItem('token'); this.loginForm = this.formBuilder.group({ username: ['', Validators.compose([Validators.required])], password: ['', Validators.required] }); } }
List User Component
Once the list view is rendered, an API call will be made to fetch user details and the same can be viewed in a tabular form. We have multiple buttons on this page to add, edit and delete any user.
list-user.component.html<div class="col-md-6 user-container"> <h2 style="margin: auto"> User Details</h2> <button class="btn btn-danger" style="width:100px" (click)="addUser()"> Add User</button> <table class="table table-striped"> <thead> <tr> <th class="hidden">Id</th> <th>FirstName</th> <th>LastName</th> <th>UserName</th> <th>Age</th> <th>Salary</th> </tr> </thead> <tbody> <tr *ngFor="let user of users"> <td class="hidden">{{user.id}}</td> <td>{{user.firstName}}</td> <td>{{user.lastName}}</td> <td>{{user.username}}</td> <td>{{user.age}}</td> <td>{{user.salary}}</td> <td><button class="btn btn-success" (click)="deleteUser(user)"> Delete</button> <button class="btn btn-success" (click)="editUser(user)" style="margin-left: 20px;"> Edit</button></td> </tr> </tbody> </table> </div>
There are many things happening here. All the action items implementation are here. On the click of Add button addUser()
function will be invoked which will just navigate to add-user
route.
On click of Edit button, editUser()
function will be called. This method saves the selected user id in the local storage. Once, the edit component is rendered, this user id will be picked to fetch user details from the API and the add form is rendered in the editable format.
On click of the delete button, the selected rows will be removed from the table and an API call be made to remove this user from the DB too.
list-user.component.tsimport { Component, OnInit , Inject} from '@angular/core'; import {Router} from "@angular/router"; import {User} from "../../model/user.model"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-list-user', templateUrl: './list-user.component.html', styleUrls: ['./list-user.component.css'] }) export class ListUserComponent implements OnInit { users: User[]; constructor(private router: Router, private apiService: ApiService) { } ngOnInit() { if(!window.localStorage.getItem('token')) { this.router.navigate(['login']); return; } this.apiService.getUsers() .subscribe( data => { this.users = data.result; }); } deleteUser(user: User): void { this.apiService.deleteUser(user.id) .subscribe( data => { this.users = this.users.filter(u => u !== user); }) }; editUser(user: User): void { window.localStorage.removeItem("editUserId"); window.localStorage.setItem("editUserId", user.id.toString()); this.router.navigate(['edit-user']); }; addUser(): void { this.router.navigate(['add-user']); }; }
Add User Component
Add user component has a simple form to take input from the user and makes a HTTP call to save the user in the DB. Once, the user is added, user will be again routed to list-user route.
add-user.component.tsimport { Component, OnInit } from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {Router} from "@angular/router"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-add-user', templateUrl: './add-user.component.html', styleUrls: ['./add-user.component.css'] }) export class AddUserComponent implements OnInit { constructor(private formBuilder: FormBuilder,private router: Router, private apiService: ApiService) { } addForm: FormGroup; ngOnInit() { this.addForm = this.formBuilder.group({ id: [], username: ['', Validators.required], password: ['', Validators.required], firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], salary: ['', Validators.required] }); } onSubmit() { this.apiService.createUser(this.addForm.value) .subscribe( data => { this.router.navigate(['list-user']); }); } }add-user.component.html
<div class="col-md-6 user-container"> <h2 class="text-center">Add User</h2> <form [formGroup]="addForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label for="username">User Name:</label> <input type="text" formControlName="username" placeholder="username" name="username" class="form-control" id="username"> </div> <div class="form-group"> <label for="password">Password:</label> <input type="password" formControlName="password" placeholder="password" name="password" class="form-control" id="password"> </div> <div class="form-group"> <label for="firstName">First Name:</label> <input formControlName="firstName" placeholder="First Name" name="firstName" class="form-control" id="firstName"> </div> <div class="form-group"> <label for="lastName">Last Name:</label> <input formControlName="lastName" placeholder="Last name" name="lastName" class="form-control" id="lastName"> </div> <div class="form-group"> <label for="age">Age:</label> <input type="number" formControlName="age" placeholder="age" name="age" class="form-control" id="age"> </div> <div class="form-group"> <label for="salary">Salary:</label> <input type="number" formControlName="salary" placeholder="salary" name="salary" class="form-control" id="salary"> </div> <button class="btn btn-success">Add</button> </form> </div>
Edit User Component
Whenever, edit button is clicked, the selected user id is stored in the local storage and inside ngOnInit()
, an API call is made to fetch the user by user id and the form is auto-populated.
<div class="col-md-6 user-container"> <h2 class="text-center">Edit User</h2> <form [formGroup]="editForm" (ngSubmit)="onSubmit()"> <div class="hidden"> <input type="text" formControlName="id" placeholder="id" name="id" class="form-control" id="id"> </div> <div class="form-group"> <label for="username">User Name:</label> <input type="text" formControlName="username" placeholder="username" name="username" class="form-control" id="username" readonly="true"> </div> <div class="form-group"> <label for="firstName">First Name:</label> <input formControlName="firstName" placeholder="First Name" name="firstName" class="form-control" id="firstName"> </div> <div class="form-group"> <label for="lastName">Last Name:</label> <input formControlName="lastName" placeholder="Last name" name="lastName" class="form-control" id="lastName"> </div> <div class="form-group"> <label for="age">Age:</label> <input type="number" formControlName="age" placeholder="age" name="age" class="form-control" id="age"> </div> <div class="form-group"> <label for="salary">Salary:</label> <input type="number" formControlName="salary" placeholder="salary" name="salary" class="form-control" id="salary"> </div> <button class="btn btn-success">Update</button> </form> </div>edit-user.component.ts
import { Component, OnInit , Inject} from '@angular/core'; import {Router} from "@angular/router"; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {first} from "rxjs/operators"; import {User} from "../../model/user.model"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-edit-user', templateUrl: './edit-user.component.html', styleUrls: ['./edit-user.component.css'] }) export class EditUserComponent implements OnInit { user: User; editForm: FormGroup; constructor(private formBuilder: FormBuilder,private router: Router, private apiService: ApiService) { } ngOnInit() { let userId = window.localStorage.getItem("editUserId"); if(!userId) { alert("Invalid action.") this.router.navigate(['list-user']); return; } this.editForm = this.formBuilder.group({ id: [''], username: ['', Validators.required], firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], salary: ['', Validators.required] }); this.apiService.getUserById(+userId) .subscribe( data => { this.editForm.setValue(data.result); }); } onSubmit() { this.apiService.updateUser(this.editForm.value) .pipe(first()) .subscribe( data => { if(data.status === 200) { alert('User updated successfully.'); this.router.navigate(['list-user']); }else { alert(data.message); } }, error => { alert(error); }); } }
Angular 8 API Service Implementation
Below is the service class that makes HTTP calls to perform the CRUD operations. It uses HttpClient
from @angular/common/http
. As we discussed above, we have 2 different controllers in the server side and hence two different URL group to make HTTP request.
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import {User} from "../model/user.model"; import {Observable} from "rxjs/index"; import {ApiResponse} from "../model/api.response"; @Injectable() export class ApiService { constructor(private http: HttpClient) { } baseUrl: string = 'http://localhost:8080/users/'; login(loginPayload) : Observable<ApiResponse> { return this.http.post<ApiResponse>('http://localhost:8080/' + 'token/generate-token', loginPayload); } getUsers() : Observable<ApiResponse> { return this.http.get<ApiResponse>(this.baseUrl); } getUserById(id: number): Observable<ApiResponse> { return this.http.get<ApiResponse>(this.baseUrl + id); } createUser(user: User): Observable<ApiResponse> { return this.http.post<ApiResponse>(this.baseUrl, user); } updateUser(user: User): Observable<ApiResponse> { return this.http.put<ApiResponse>(this.baseUrl + user.id, user); } deleteUser(id: number): Observable<ApiResponse> { return this.http.delete<ApiResponse>(this.baseUrl + id); } }
Angular 8 Routing Configuration
The routing implementation in Angular 8 is similar to previous versions.
app.routing.tsimport { RouterModule, Routes } from '@angular/router'; import {LoginComponent} from "./login/login.component"; import {AddUserComponent} from "./user/add-user/add-user.component"; import {ListUserComponent} from "./user/list-user/list-user.component"; import {EditUserComponent} from "./user/edit-user/edit-user.component"; const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'add-user', component: AddUserComponent }, { path: 'list-user', component: ListUserComponent }, { path: 'edit-user', component: EditUserComponent }, {path : '', component : LoginComponent} ]; export const routing = RouterModule.forRoot(routes);
Angular 8 HTTP Interceptors
The interceptor implements HttpInterceptor which intercepts all the HTTP request and token in the header for API authentication. At the time of login, there won't be any valid token present in the local cache hence, there is a condition check for the presence of token.
interceptor.tsimport {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; import {Observable} from "rxjs/internal/Observable"; import {Injectable} from "@angular/core"; @Injectable() export class TokenInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable > { let token = window.localStorage.getItem('token'); if (token) { request = request.clone({ setHeaders: { Authorization: 'Bearer ' + token } }); } return next.handle(request); } }
Spring Boot Server-Side Implementation
Spring Boot JWT Security
Following class extends OncePerRequestFilter that ensures a single execution per request dispatch. This class checks for the authorization header and authenticates the JWT token and sets the authentication in the context. Doing so will protect our APIs from those requests which do not have any authorization token. The configuration about which resource to be protected and which not can be configured in WebSecurityConfig.java
JwtAuthenticationFilter.javapublic class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { String header = req.getHeader(HEADER_STRING); String username = null; String authToken = null; if (header != null && header.startsWith(TOKEN_PREFIX)) { authToken = header.replace(TOKEN_PREFIX,""); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } catch(SignatureException e){ logger.error("Authentication Failed. Username or Password not valid."); } } else { logger.warn("couldn't find bearer string, will ignore the header"); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(req, res); } }
Now let us define our usual spring boot security configurations.We have userDetailsService injected to fetch user credentials from database.
Here the annotation @EnableGlobalMethodSecurity
enables method level security and you can annotate your method with annotations such as @Secured to provide role based authentication at method level.
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name = "userService") private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(encoder()); } @Bean public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/token/*").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } @Bean public BCryptPasswordEncoder encoder(){ return new BCryptPasswordEncoder(); } }
Spring Boot Controller Implementation
As discussed above, we have 2 different controllers - one is for authentication and another is to peform CRUD operation on User entity.
Following is the controller that is exposed to create token on user behalf and if you noticed in WebSecurityConfig.java
we have configured this url to have no authentication so that user can generate JWT token with valid credentials.
@RestController @RequestMapping("/token") public class AuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserService userService; @RequestMapping(value = "/generate-token", method = RequestMethod.POST) public ResponseEntity> register(@RequestBody LoginUser loginUser) throws AuthenticationException { final Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginUser.getUsername(), loginUser.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); final User user = userService.findOne(loginUser.getUsername()); final String token = jwtTokenUtil.generateToken(user); return ResponseEntity.ok(new AuthToken(token)); } }
Below is our controller class that has all the API implementation for the CRUD operation. Please let me know if you have any doubt with the code in the comment section.
UserController.javapackage com.devglan.controller; import com.devglan.model.ApiResponse; import com.devglan.model.User; import com.devglan.model.UserDto; import com.devglan.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PostMapping public ApiResponsesaveUser(@RequestBody UserDto user){ return new ApiResponse<>(HttpStatus.OK.value(), "User saved successfully.",userService.save(user)); } @GetMapping public ApiResponse > listUser(){ return new ApiResponse<>(HttpStatus.OK.value(), "User list fetched successfully.",userService.findAll()); } @GetMapping("/{id}") public ApiResponse
getOne(@PathVariable int id){ return new ApiResponse<>(HttpStatus.OK.value(), "User fetched successfully.",userService.findById(id)); } @PutMapping("/{id}") public ApiResponse update(@RequestBody UserDto userDto) { return new ApiResponse<>(HttpStatus.OK.value(), "User updated successfully.",userService.update(userDto)); } @DeleteMapping("/{id}") public ApiResponse delete(@PathVariable int id) { userService.delete(id); return new ApiResponse<>(HttpStatus.OK.value(), "User deleted successfully.", null); } }
Spring Boot Service Implementation
UserServiceImpl.javaThe service class has some business logic and it is the bridge between our controllers and DAOs.
@Transactional @Service(value = "userService") public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserDao userDao; @Autowired private BCryptPasswordEncoder bcryptEncoder; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findByUsername(username); if(user == null){ throw new UsernameNotFoundException("Invalid username or password."); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority()); } private ListgetAuthority() { return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")); } public List findAll() { List list = new ArrayList<>(); userDao.findAll().iterator().forEachRemaining(list::add); return list; } @Override public void delete(int id) { userDao.deleteById(id); } @Override public User findOne(String username) { return userDao.findByUsername(username); } @Override public User findById(int id) { Optional optionalUser = userDao.findById(id); return optionalUser.isPresent() ? optionalUser.get() : null; } @Override public UserDto update(UserDto userDto) { User user = findById(userDto.getId()); if(user != null) { BeanUtils.copyProperties(userDto, user, "password", "username"); userDao.save(user); } return userDto; } @Override public User save(UserDto user) { User newUser = new User(); newUser.setUsername(user.getUsername()); newUser.setFirstName(user.getFirstName()); newUser.setLastName(user.getLastName()); newUser.setPassword(bcryptEncoder.encode(user.getPassword())); newUser.setAge(user.getAge()); newUser.setSalary(user.getSalary()); return userDao.save(newUser); } }
Spring Data Implementation
Let us configure our DB connection parameters first. As we will be using MySQL in this app, below is our configuration properties.
application.propertiesspring.datasource.url=jdbc:mysql://localhost:3306/test spring.datasource.username=root spring.datasource.password=root spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create-drop spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Below is the repo class. It extends CrudRepository that has all the implementations for CRUD operations. For a detailed integration of Spring Data JPA, you can visit my another article here.
UserDao.java@Repository public interface UserDao extends CrudRepository{ User findByUsername(String username); }
Spring Boot Model Class
Below is our User model class the UserDto class.
User.java
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private int id;
@Column
private String firstName;
@Column
private String lastName;
@Column
private String username;
@Column
@JsonIgnore
private String password;
@Column
private long salary;
@Column
private int age;
//setters and getters
UserDto.java
public class UserDto {
private int id;
private String firstName;
private String lastName;
private String username;
private String password;
private int age;
private long salary;
//setters and getters
LoginUser.java
public class LoginUser {
private String username;
private String password;
//setters and getters
AuthToken.java
public class AuthToken {
private String token;
private String username;
//setters and getters
ApiResponse.java
public class ApiResponse{ private int status; private String message; private Object result; public ApiResponse(int status, String message, Object result) { this.status = status; this.message = message; this.result = result; } //setters and getters
Exception Handling at Server Side
There are 2 different configurations for exception handling in the server side. To handle REST exception, we use @ControllerAdvice and @ExceptionHandler in Spring MVC but these handler works if the request is handled by the DispatcherServlet. However, security-related exceptions occur before that as it is thrown by Filters. Hence, it is required to insert a custom filter (RestAccessDeniedHandler and RestAuthenticationEntryPoint) earlier in the chain to catch the exception and return accordingly.
Full explanation of handling exception in spring security can be found here in my previous article.
JwtAuthenticationEntryPoint.java@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }
The exception handling part is out of scope for this article. In the production system, you will have your custom exception defined and you basically write your advice to handle those exceptions. Below implementation is just for a demo to get you started. Please let me know if you need a full-fledged solution to this.
@RestControllerAdvice public class ExceptionAdvice { @ExceptionHandler(RuntimeException.class) public ApiResponse handleNotFoundException(RuntimeException ex) { ApiResponse apiResponse = new ApiResponse(400, "Bad request", null); return apiResponse; } }
CORS Configuration for Spring Boot Angular
Modern browsers does not allow cross browser HTTP request and you will see error stating that Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
To enable CORS request at our server, we can add below filter. It will act as a global filter.
public class CORSFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { System.out.println("Filtering on..........................................................."); HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) {} public void destroy() {} }
Running the Application
We have command line runner integrated in the spring boot main class to add user during application start up.
@SpringBootApplication public class Application { @Autowired private BCryptPasswordEncoder passwordEncoder; public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public CommandLineRunner init(UserDao userDao){ return args -> { User user1 = new User(); user1.setFirstName("Devglan"); user1.setLastName("Devglan"); user1.setSalary(12345); user1.setAge(23); user1.setUsername("devglan"); user1.setPassword(passwordEncoder.encode("devglan")); userDao.save(user1); User user2 = new User(); user2.setFirstName("John"); user2.setLastName("Doe"); user2.setSalary(4567); user2.setAge(34); user2.setUsername("john"); user2.setPassword(passwordEncoder.encode("john")); userDao.save(user2); }; } }
Conclusion
In this article, we developed a full stack app with Angular 8 at the client-side, Spring Boot and Spring Data in the server-side and secured the REST endpoints with Spring security.