Recently I was getting a refresher on Angular web application development and had to confront with something I have always avoided before - CORS, Cross Origin Resource Sharing. I didn't have to face this because the project I have worked with always places my web page related source code with the back end server work. Once they were together. This way I don't have to deal with CORS. Once I switched from AngularJS to Angular. I find myself at the cross road, either learn how to play with CORS or go back to the old way of doing things. Here is the issue. During development, my Angular code runs in a self contained web server. My back end API service runs in Spring Boot hosted web application container. Because the two are two different web server, I have to have CORS properly configured in my API service so that such set up can work, at least for the sake of development.
Why is this such a big deal? Imaging that my web pages are hosted with the URL: http://localhost:4200/XXXXX. My back end service APIs have a different URL: http://localhost:8080/YYYYY. Even though they are using the same host name "localhost", and the same protocol "http", the difference of port would make them two unique origins instead of the same origin. The CORS check was created to ensure that only the trusted sites can communicate with each other. It is also a complication that we developers must remember to configure/implement for our projects if we need something to be done in a certain way.
It was very easy to configure CORS for Spring Boot base web application. But I didn't know what I was doing. It took a little more effort for me to get it working. The particular problem I had is that the CORS configuration works when the request is unauthenticated. And the requests fails when the back end checks for authentication and authorization. If you have dealt with this before, you might know what is causing the issue. For me at the time, I didn't have the know-how so it took me a while to figure out. I hope this tutorial will provide some useful information for anyone who faces similar problems.
The sample application I have provided has two parts. The first part is the back end service API application, written with Spring Boot. This is the part that needs the CORS configuration. The other part is the front end web application, written in the latest Angular framework. The front end web application has three basic functionalities. The first is that use must login. The next one is that once logged in, user will see a list of data. This is done by an API call to the back end, retrieving the data and displaying them on the page. The last one is the log out functionality. All three requires CORS checks for the two sides to communicate.
I wrote the front end with Angular, and it is very hard to use the normal session based security to work. I decided to use one of my other tutorial as basis. The login security will be controlled by a mock JWT token. On the server side, I have an OncePerRequestFilter
that will check the JWT token, and authenticate/authorize the request. This means that after logging in, every request from user must have the JWT token added, or the request will be rejected. This is the tutorial I have created a while back, which is the basis of this sample application. If you need additional help understanding the back end web application, please check this prior tutorial.
This tutorial has two parts. The first part will be extensive discussion on the front end design, how it communicates with the back end. The second part of the tutorial will be discussing the CORS configuration in the service API end. I will explain in detail how browser works with CORS configuration, what the problem I have faced, and how the solution works.
The client side is a simple application. It has a login page, an index page that display some data, and a log out functionality that kicks the user back to the login page. Since I decided to use Angular instead of the usual AngularJS, all these client end codes are hosted within its own little project. And it is self-hosting. Let me start with the login page.
I break the login page functionality into three different related components, the HTML page mark up, the code logic for the login component, and the code logic for services needed. First, I will show you the easiest of all three, the HTML mark up:
<div class="row login-form justify-content-center">
<div class="col-xs-12 col-sm-8 col-md-6 col-lg-4">
<div class="panel-box">
<form novalidate #loginForm="ngForm" >
<div class="mb-3">
<label for="login_userName" class="form-label">User Name</label>
<input type="text"
class="form-control"
id="login_UserName"
name="userName"
[(ngModel)]="userName"
[ngClass]="{'invalid-input': (usrName.dirty || usrName.touched) && usrName.errors?.['required']}"
required
#usrName="ngModel" />
<span class="badge bg-danger"
*ngIf="(usrName.dirty || usrName.touched) && usrName.errors?.['required']">Required</span>
</div>
<div class="mb-3">
<label for="login_userPass" class="form-label">Password</label>
<input type="password"
class="form-control"
id="login_userPass"
name="userPass"
[(ngModel)]="userPass"
[ngClass]="{'invalid-input': (usrPass.dirty || usrPass.touched) && usrPass.errors?.['required']}"
required
#usrPass="ngModel" />
<span class="badge bg-danger"
*ngIf="(usrPass.dirty || usrPass.touched) && usrPass.errors?.['required']">Required</span>
</div>
<div class="row">
<div class="col">
<button type="submit"
class="btn btn-primary form-control"
(click)="onClickLogin(loginForm)">Login</button>
</div>
<div class="col">
<button type="clear"
class="btn btn-default form-control"
(click)="onClickClear(loginForm)">Clear</button>
</div>
</div>
</form>
</div>
</div>
</div>
Besides the basic Angular stuff in this, I wired the HTML form with some input validations, it deserves its own tutorial. So I will not waste time explain them here. All you need to know is that this page has two input fields, the first one allows user to enter the user name. The other input field is for entering the password. Then there are two buttons, one to log into the secure page. The other clears the input fields.
Next, it would be the login component Angular code:
import { Component, OnInit } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { LoginUser } from '../dataModels/loginUser.type';
import { LoginService } from './login.service';
import { FormsService } from '../common/forms.service';
@Component({
selector: 'app-root',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
private _userName: String = "";
private _userPass: String = "";
private _loginService: LoginService;
private _formsService: FormsService;
private _router: Router;
constructor(loginService: LoginService, formsService: FormsService, router: Router) {
this._loginService = loginService;
this._formsService = formsService;
this._router = router;
}
ngOnInit() {
let userLoggedIn: Boolean
= this._loginService.checkUserLoggedIn();
if (userLoggedIn) {
this._router.navigate(['/index']);
}
}
public get userName() {
return this._userName;
}
public set userName(val: String) {
this._userName = val;
}
public get userPass() {
return this._userPass;
}
public set userPass(val: String) {
this._userPass = val;
}
public onClickClear(loginForm: any): void{
this._userName = "";
this._userPass = "";
this._formsService.makeFormFieldsClean(loginForm);
}
public onClickLogin(loginForm: any): void{
this._formsService.makeFormFieldsDirty(loginForm);
if (loginForm.valid) {
let userToLogin: LoginUser = new LoginUser(this._userName, this._userPass);
let self:any = this;
self._loginService.login(userToLogin)
.subscribe((resp: any) => {
if (resp != null &&
resp.userId != null &&
resp.userId.trim() !== "" &&
resp.tokenValue != null &&
resp.tokenValue.trim() !== "") {
self._loginService.setSessionCurrentUser(resp);
self._router.navigate(['/index']);
}
}, (error: HttpErrorResponse) => {
if (error != null) {
if (error.status === 0) {
console.log("Client error.");
} else if (error.status === 401 || error.status === 403) {
self._userName = "";
self._userPass = "";
self._formsService.makeFormFieldsClean(loginForm);
console.log("You are not authorized.");
} else if (error.status === 500) {
console.log("Server error occurred.");
} else {
console.log("Unknown error: " + error.status);
}
}
});
}
}
}
Again, I am not going to explain the form validation part of this component, not to spoil the fun. The most important part of this component source file is this:
let userToLogin: LoginUser = new LoginUser(this._userName, this._userPass);
let self:any = this;
self._loginService.login(userToLogin)
.subscribe((resp: any) => {
if (resp != null &&
resp.userId != null &&
resp.userId.trim() !== "" &&
resp.tokenValue != null &&
resp.tokenValue.trim() !== "") {
self._loginService.setSessionCurrentUser(resp);
self._router.navigate(['/index']);
}
}, (error: HttpErrorResponse) => {
if (error != null) {
if (error.status === 0) {
console.log("Client error.");
} else if (error.status === 401 || error.status === 403) {
self._userName = "";
self._userPass = "";
self._formsService.makeFormFieldsClean(loginForm);
console.log("You are not authorized.");
} else if (error.status === 500) {
console.log("Server error occurred.");
} else {
console.log("Unknown error: " + error.status);
}
}
});
The code I extracted, uses the service object to call the back end to authenticate the user. If the authentication succeeds, then I need to save the simple JWT token for future use. I save it in the browser session storage. Then the browser will navigate back to the index page.
Here is the service class that calls the back end service API for authentication:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { LoginUser } from '../dataModels/loginUser.type';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class LoginService {
constructor(private http: HttpClient) {
}
public login(userToLogin: LoginUser): Observable<any> {
return this.http.post<any>(environment.apiBaseUrl + "authenticate", userToLogin);
}
public signout(): Observable<any> {
let jwtToken: String = this.getUserSecurityToken(),
headers: HttpHeaders = new HttpHeaders({
"authorization": "bearer " + jwtToken,
}),
options = { headers: headers };
return this.http.post<any>(environment.apiBaseUrl + "signOut", null, options);
}
public setSessionCurrentUser(userToAdd: any): void {
if (userToAdd != null &&
userToAdd.userId &&
userToAdd.userId.trim() !== "" &&
userToAdd.tokenValue &&
userToAdd.tokenValue.trim() !== "") {
if (sessionStorage.getItem("currentUser") != null) {
sessionStorage.removeItem("currentUser");
}
sessionStorage.setItem("currentUser", JSON.stringify(userToAdd));
}
}
public removeSessionCurrentUser(): void {
if (sessionStorage.getItem("currentUser") != null) {
sessionStorage.removeItem("currentUser");
}
}
public checkUserLoggedIn(): Boolean {
let retVal: Boolean = false,
currUserObj: any = null,
currUser: any;
if (sessionStorage.getItem("currentUser") != null) {
currUserObj = sessionStorage.getItem("currentUser");
if (currUserObj != null && currUserObj.toString() != null && currUserObj.toString().trim() !== "") {
currUser = JSON.parse(currUserObj.toString());
if (currUser &&
currUser.userId &&
currUser.userId.trim() !== "") {
retVal = currUser.tokenValue != null && currUser.tokenValue.trim() !== "";
}
}
}
return retVal;
}
public getLoggedinUser(): any | null {
let retVal: any | null = null,
currUser: any | null = null,
currUserObj: any = null;
if (sessionStorage.getItem("currentUser") != null) {
currUserObj = sessionStorage.getItem("currentUser");
if (currUserObj != null && currUserObj.toString() != null && currUserObj.toString().trim() !== "") {
currUser = JSON.parse(currUserObj.toString());
if (currUser &&
currUser.userId &&
currUser.userId.trim() !== "") {
retVal = currUser;
}
}
}
return retVal;
}
public getUserSecurityToken(): String
{
let retVal: String = "",
currUser: any | null = null,
currUserObj: any = null;
if (sessionStorage.getItem("currentUser") != null) {
currUserObj = sessionStorage.getItem("currentUser");
if (currUserObj != null && currUserObj.toString() != null && currUserObj.toString().trim() !== "") {
currUser = JSON.parse(currUserObj.toString());
if (currUser &&
currUser.userId &&
currUser.userId.trim() !== "") {
retVal = currUser.tokenValue;
}
}
}
return retVal;
}
}
This service class has a lot. There are two methods that invokes the the back end APIs. And there are a lot methods that saves the security token into browser session storage, take the security token out of the browser session storage, check the security token, and extract the security token for service calls.
You don't have to worry about the ones that manages the security token. The ones that calls the back end are the one called login()
, and signout()
:
...
public login(userToLogin: LoginUser): Observable<any> {
return this.http.post<any>(environment.apiBaseUrl + "authenticate", userToLogin);
}
public signout(): Observable<any> {
let jwtToken: String = this.getUserSecurityToken(),
headers: HttpHeaders = new HttpHeaders({
"authorization": "bearer " + jwtToken,
}),
options = { headers: headers };
return this.http.post<any>(environment.apiBaseUrl + "signOut", null, options);
}
...
Both methods using HTTP post to interact with the back end. The major difference is that for method signout()
, I had to add my dummy JWT token. Sign out functionality is secure. I have to have the security token in order to access such functionality. The login()
method does not have such a restriction. So for that method, I don't have to add the security token.
The difference of the two is how I hit my issue and begin the adventure. For now you don't have to worry about this. Let me continue with the index page, and the data loading for the index page.
Unlike the log in page, the index page is protected, and is only visible when user successfully logged in. After logging in, the index page will load a list data by calling the back end API service. Here is the component source code for the page:
import { PageSecurityService } from '../common/pageSecurity.service';
import { GameTitlesService } from './gameTitles.service'
import { LoginService } from '../loginPage/login.service';
import { GameTitle } from '../dataModels/gameTitle.type';
@Component({
selector: 'app-root',
templateUrl: './index.component.html',
styleUrls: ['./index.component.css']
})
export class IndexComponent implements OnInit {
private _loginService: LoginService;
private _pageSecurityService: PageSecurityService;
private _gameTitlesService: GameTitlesService;
private _allTitles: Array<GameTitle> = [];
public testArray: Array<String> = [];
constructor(loginService: LoginService,
pageSecurityService: PageSecurityService,
gameTitlesService: GameTitlesService) {
this._loginService = loginService;
this._pageSecurityService = pageSecurityService;
this._gameTitlesService = gameTitlesService;
this.testArray.push("Test1");
this.testArray.push("Test2");
this.testArray.push("Test3");
this.testArray.push("Test4");
this.testArray.push("Test5");
}
public get allTitles(): Array<GameTitle> {
return this._allTitles;
}
public set allTitles(val: Array<GameTitle>) {
this._allTitles = val;
}
ngOnInit(): void {
let userLoggedIn: Boolean
= this._loginService.checkUserLoggedIn();
if (!userLoggedIn) {
this._pageSecurityService.gotoLoginPage();
} else {
this.loadAllGameTitles();
}
}
public onClickLogout(): void {
let self = this;
self._loginService.signout()
.subscribe((resp: any) => {
if (resp != null) {
if (resp.successful) {
alert("Signed out successfully"); /// XXX
self._loginService.removeSessionCurrentUser();
self._pageSecurityService.gotoLoginPage();
} else {
alert("Signed out failed with error. " + resp.detailedMessage); /// XXX
}
} else {
alert("Signed out failed with error. Unknown error."); /// XXX
}
}, (error: HttpErrorResponse) => {
if (error != null) {
if (error.status === 0) {
// XXX
console.log("Client error.");
} else if (error.status === 401 || error.status === 403) {
// XXX
alert("You are not authorized.");
console.log("You are not authorized.");
self._loginService.removeSessionCurrentUser();
self._pageSecurityService.gotoLoginPage();
} else if (error.status === 500) {
console.log("Server error occurred.");
} else {
console.log("Unknown error: " + error.status);
}
}
});
}
private loadAllGameTitles(): void {
let self = this;
self._gameTitlesService.getAllGameTitles()
.subscribe((resp: any) => {
if (resp && resp.length > 0) {
for (var itm of resp) {
if (itm) {
let titleToAdd: GameTitle = new GameTitle(itm.gameTitle, itm.publisher, itm.devStudioName, itm.publishingYear, itm.retailPrice);
self._allTitles.push(titleToAdd);
}
}
}
}, (error: HttpErrorResponse) => {
if (error != null) {
if (error.status === 0) {
// XXX
console.log("Client error.");
} else if (error.status === 401 || error.status === 403) {
// XXX
alert("You are not authorized.");
console.log("You are not authorized.");
self._loginService.removeSessionCurrentUser();
self._pageSecurityService.gotoLoginPage();
} else if (error.status === 500) {
alert("Server error.");
console.log("Server error occurred.");
} else {
alert("Unknown error.");
console.log("Unknown error: " + error.status);
}
}
});
}
}
There is a few pointers I like to discuss about this component class, this class called IndexComponent
. It inherits from the interface called OnInit
. This allows me to override the method called ngOnInit()
. This method will be called to do any initialization. This is the method where I check and see if the user is logged in or not. This is done by checking whether the security token exists in the storage session. If it is not present, it will redirect (via Angular router) to the login page. But if the user is logged in, then it will try to load the list of games and display in a table. Here it is:
...
ngOnInit(): void {
let userLoggedIn: Boolean
= this._loginService.checkUserLoggedIn();
if (!userLoggedIn) {
this._pageSecurityService.gotoLoginPage();
} else {
this.loadAllGameTitles();
}
}
...
This is the method that loads the games from the back end API service:
...
private loadAllGameTitles(): void {
let self = this;
self._gameTitlesService.getAllGameTitles()
.subscribe((resp: any) => {
if (resp && resp.length > 0) {
for (var itm of resp) {
if (itm) {
let titleToAdd: GameTitle = new GameTitle(itm.gameTitle, itm.publisher, itm.devStudioName, itm.publishingYear, itm.retailPrice);
self._allTitles.push(titleToAdd);
}
}
}
}, (error: HttpErrorResponse) => {
if (error != null) {
if (error.status === 0) {
// XXX
console.log("Client error.");
} else if (error.status === 401 || error.status === 403) {
// XXX
alert("You are not authorized.");
console.log("You are not authorized.");
self._loginService.removeSessionCurrentUser();
self._pageSecurityService.gotoLoginPage();
} else if (error.status === 500) {
alert("Server error.");
console.log("Server error occurred.");
} else {
alert("Unknown error.");
console.log("Unknown error: " + error.status);
}
}
});
}
...
All the above code does is call the service object called _gameTitlesService
and invoke a method called getAllGameTitles()
. Once the method call completes successfully, the list of game titles are converted into data model GameTitle
, which will be displayed on the page. These API calls are all plain and simple, looks like the many ones I have done for the past few years with other tutorials. But, in this case, it is different. The reason is that, the API invocation is from the page that was hosted on localhost, and on a different port.
Now that we saw all the API calls, the login authentication, the logout API invocation from secure side, and the secure invocation of the load data on the index page. In the next section, I will explain the issue I found and how it can be resolved. I will discuss my blunder and the hard work that redeemed myself, and all the fun I had which results this tutorial.
I have mentioned, even though the client end API invocation is the same as I have done in the past, this time the situation is different. The client end and the back end are separated in two different servers, hence, CORS must be configured so that I can continue with my development work. Also I need to explain that I had this plan of copying the transformed scripts into the back end server project so that the front end and the back end APIs are integrated together. This would eliminate the need for the CORS configuration. However, during development, the front end and the back end are separated, hence CORS configuration is needed.
There are two different ways to configure CORS in Spring Boot based web application. The first is the global configuration such that the configuration is applied to all the API methods. The other way is annotating the API methods with @CORS(...)
to specify the CORS configuration for API method individually. I choose the approach of configuring CORS globally. This is very simple. All I needed is a bean that provides the configuration. Here is the code:
@Bean
public WebMvcConfigurer corsConfigurer()
{
String[] allowDomains = new String[2];
allowDomains[0] = "http://localhost:4200";
allowDomains[1] = "http://localhost:8080";
System.out.println("CORS configuration....");
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins(allowDomains);
}
};
}
This method returns a bean of type WebMvcConfigurer
. In this method I just defines an anonymous class, sub type of WebMvcConfigurer. And return an object of it. The anonymous class has just one method that defines the CORS mappings. It specifies all APIs are allowed to be invoked from an array of URLs. I have defined the array at the beginning of the method. It has two one is from itself: http://localhost:8080; the other is from: http://localhost:4200 (the nodejs server). Sorry about hard coding this. I should put all these URLs in a config file and read from that. Hard coding is just for demonstration.
With this method, it seemed that my setup is working. I was able to get the authentication working. Here is my API method for authenticating user:
@RequestMapping(value="/authenticate", method = RequestMethod.POST,
consumes=MediaType.APPLICATION_JSON_VALUE, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<AuthUserInfo> login(
@RequestBody
LoginRequest loginReq
)
{
System.out.println("User Name: " + loginReq.getUserName());
System.out.println("User Pass: " + loginReq.getUserPass());
if (StringUtils.hasText(loginReq.getUserName()) && StringUtils.hasText(loginReq.getUserPass()))
{
AuthUserInfo userFound = _authService.authenticateUser(loginReq.getUserName(), loginReq.getUserPass());
if (userFound != null)
{
return ResponseEntity.ok(userFound);
}
else
{
return ResponseEntity.status(403).body((AuthUserInfo)null);
}
}
else
{
return ResponseEntity.status(403).body((AuthUserInfo)null);
}
}
The front end code was able to sent the authentication request to the API method shown above. And as long as the user name and password matches, the user can be authenticated and authorized. A return value of type AuthUserInfo
would be returned to the front end.
Next, I was trying to make the sign out functionality work. This is the time I encountered the problem. Every time the front end is trying to invoke the functionality with security token attached, it will get a CORS error. What I don't understand is that the login functionality is working as expected. But the API method that is protected wouldn't work. Again, I spent quite some time to find an answer. Then I failed to find something that is relatable. The true problem is that I don't know what question I should ask. And I changed my strategy. I ask myself, do I really know how CORS works with browser. The answer was no. The next question I asked is "how CORS works?" The answer led me to the answer of the issue I am facing.
The way Browser works with cross origin requests, that is, the request is from server http://localhost:4200 to http://localhost:8080. Browsers can detect such cross origin requests. And it would first send a HTTP OPTIONS request to the back end server. The return response would tell browser whether the actual cross origin request would work or not. This is done silently and anonymously by the browser, and I cannot even see the HTTP OPTIONS request in the developer mode. No wonder CORS errors are so hard to Since it is sent anonymously, the request can only be processed by unprotected API methods. And the sign out API method looks like this:
@PreAuthorize("isAuthenticated()")
@RequestMapping(value="/signOut", method = RequestMethod.POST)
public ResponseEntity<OpResponse> signOut()
{
ResponseEntity<OpResponse> retVal = null;
OpResponse resp = new OpResponse();
AuthUserInfo currUser = getCurrentUser();
if (currUser != null)
{
String userId = currUser.getUserId();
boolean signoutSuccess = _authService.userSignOut(userId);
if (signoutSuccess)
{
resp.setSuccessful(true);
resp.setStatus("Log out successful");
resp.setDetailMessage("You have successfully log out from this site.");
retVal = new ResponseEntity<OpResponse>(resp, HttpStatus.OK);
}
else
{
resp.setSuccessful(false);
resp.setStatus("Operation Failed");
resp.setDetailMessage("Unable to sin out user. Unknown error.");
retVal = new ResponseEntity<OpResponse>(resp, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
else
{
resp.setSuccessful(false);
resp.setStatus("Operation Failed");
resp.setDetailMessage("You cannot log out if you are not log in first.");
retVal = new ResponseEntity<OpResponse>(resp, HttpStatus.UNAUTHORIZED);
}
System.out.println("sign out called!");
return retVal;
}
Once I realized that the browser is silently sending HTTP OPTIONS to the back end server, and is doing so anonymously. The above method would never be able to process such request. And the actual cross origin request will fail too. The question I should ask is that how do I configure the security for requests so that these HTTP OPTIONS requests can be handled anonymously. Turned out Baeldung had a tutorial that explain how it is done. What is great is that I don't have to read the whole thing to get to the answer. It was at the beginning of the tutorial. Anyways, the answer I seek is to ignore all CORS test requests (those invisible HTTP OPTIONS requests) from the secure API methods. Here is my configuration for secure request handling (the additional changes highlighted in bold):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
System.out.println("Security filter chain initialization...");
http.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/assets/**", "/public/**", "/authenticate", "/app/**").permitAll()
.anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
As you can see, the solution to my problem is pretty easy. All I had to do is adding the code .cors().and()
to the HttpSecurity
object method invocation. This tells Spring Security that anything related to CORS test requests (those HTTP OPTIONS requests) shall be processed by the back end server without any security checks. That is, the request filter that checks whether the request has authorization or not will not be done for these requests. Hence, the CORS test requests and handle by Spring Web automatically via the CORS configuration I have made, then the actual request will be process successfully (as long as the authorization data are included in the request and are valid). See, it was simple as long as I understand the symptom, and the root cause.
Now that all the secrets are revealed, it is time for us to see it in action. This project is a bit more complicated than my previous tutorials. Unlike them, this project has Spring Boot web application, and a nodejs based client end application. Both have to be built and run.
After downloading the sample project zip file, please unzip it to a location at your convenience. Once unzipped, find the folder where the pom.xml is located. That would be the project base folder. Please search into the sub folder webapp/secure-app, there is a .sj file. Please rename it to .js so that nodejs build can proceed without any issues.
To build the Spring Boot web application, use a Terminal and cd into the project's base folder, then run the following command:
mvn clean install
After successfully building the java project, you can run the web application with the following command:
java -jar target/hanbo-angular-secure-app1-1.0.0.jar
For the nodejs project, it needs a little more work. First you need to install all the node modules. Go to the nodejs project folder. It should be the sub folder webapp/secure-app under the project's base folder. CD into that folder. Then run the following command:
npm install
Note that I am using nodejs version v18.11.0. You might be able to use version like 16 or something and the project would still be built. I didn't try so I wouldn't know if it works or not with lower version.
Next, you can build the entire client end application with this command:
ng build
I would advise you not to build this because it will generated the finished scripts and integrate with the java project. That is not what we want to do yet. The next command would be the one you should use:
ng serve
Once both the java application and the nodejs client application are running, it is time to run the web application, use your browser navigate to the following URL:
http://localhost:4200/
If everything works, the login page will show:
Use the credential "testuser1" (user name)/"123test321" (password). When logged in, the user should see the following page:
On the upper right corner, there is the log out link. Click on it and it will log user out. If you comment out all the CORS configuration codes, and re-run the applications, and you will see the CORS related errors. All the CORS configuration codes are in the file WebAppSecurityConfig.java. If you comment out all of them, the log in functionality wouldn't work. If you only comment out the code .cors.and()
, You will still be able to login, but the index page will not be able to load the car models and display in a list. And you wouldn't be able to log out either. You can see the error in the developer's console of the browser.
Here is a screenshot of the developer's console from Chrome showing CORS error, if I comment out the CORS configurations.
It has been a fun tutorial to write about. In this one, I was discussing how CORS works and how to integrate the CORS configuration into a Spring Boot application. As shown, they were easy. One for the global setting for all API methods and one for the security configuration. Even though it is easy, if I don't know what I am doing, it would still be hard. I am very glad I was able to get to the bottom of the issue and get this tutorial out.
Besides the CORS configuration, I have shown extensively the way Angular application works. There are extensive codes on how to invoke the back end API service in a Angular client application; how it takes the JWT token put in the header of the request so that it can get through to the back end API service. There are a lot of other things I have not discussed about the source code, if you review it carefully, you may find them useful.
As always, I find the writing this tutorial a lot of fun. Better yet, the process has helped me solve a problem. I hope this tutorial will provide some help to you if you are facing similar issues. Good luck.
Han is currently looking for exciting and challenging jobs. Han has 19 years of combined software engineering experience. Han has worked on the successful release of numerous software products as a developer, a QA, and developer lead. If you would like to add a CodeProject MVA (2022) to your project team for the next great product, please contact Han at "sunhanbo (at) duck.com". Thank you for being interested.
There is no comments to this post/article.