Build a CRUD App with Angular 9 and Spring Boot 2.2
Heads up... this blog post is old!
For an updated version of this blog post, see The Angular Mini-Book. Its "Integrate Angular with Spring Boot" chapter was inspired by this blog post.
Angular is a web framework for building mobile and desktop applications. Its first version, AngularJS, was one of the first JavaScript MVC frameworks to dominate the web landscape. Developers loved it, and it rose to popularity in the early 2010s. AngularJS 1.0 was released on June 14, 2014. Angular 9 was recently released, giving Angular quite a successful run in the land of web frameworks.
Spring Boot is one of the most popular frameworks for developing Java applications and REST APIs. It also has first-class support for Kotlin!
Spring Boot 1.0 was released on April 1, 2014, and revolutionized the way Java developers write Spring apps. Instead of writing a lot of boilerplate XML or JavaConfig with annotations, Spring Boot introduced the idea of pre-configured starters with smart defaults.
Long story short, they’re a match made in heaven!
- What’s New In Angular 9?
- What’s New in Spring Boot 2.2?
- Create an Angular 9 App
- Add Angular Authentication using OpenID Connect
- Create a Spring Boot 2.2 App
- Add a Notes REST API with Spring Data REST
- Add a Notes CRUD Feature in Angular
- Lock Down Spring Boot with Recommended Security Practices
- Learn More About Angular, Spring Boot, and Kotlin
If you’d rather watch a video, I created a screencast of this tutorial.
What’s New In Angular 9?
Angular 9’s most prominent new feature is Ivy. Ivy is Angular’s new compiler and renderer. The renderer is the engine that takes your components and templates and translates them into instructions that manipulate the DOM. Ivy is an internal component, so you don’t interact with it directly. However, it can have a significant impact on your code, yielding much smaller JavaScript bundles and increasing performance.
Simply put: upgrading to Angular 9 will make your web apps faster!
Other notable features in Angular 9 include:
-
Enhancements to Angular CLI to support internationalization (i18n).
-
Type-safe improvements to
TestBed
for unit testing.
To use Angular CLI’s i18n feature, you simply need to add the i18n
attribute to your text elements, then run ng xi18n
to extract your localizable text into a file.
TestBed.inject(Component)
is the new way to get references to components in tests. TestBed.get(Component)
is deprecated.
What’s New in Spring Boot 2.2?
Spring Boot 2.2 was released in September 2019 and focuses on performance improvements and reduced memory usage. It adds Java 13 support, RSocket support, and the ability to group health indicators. Grouping indicators can be useful if you’re deploying to Kubernetes and want different groups for "liveness" and "readiness" probes.
In this post, I’ll show you how to build a CRUD application with Angular 9 and Spring Boot 2.2. Along the way, I’ll do my best to weave in security tips and how to make your apps more secure.
Prerequisites:
To install Node and Java on a Mac, Linux, or Windows Subsystem for Linux (WSL), you can use Homebrew.
brew install node
brew tap AdoptOpenJDK/openjdk
brew cask install adoptopenjdk11
You can also use SDKMAN! to install Java 11.
sdk install java 11.0.5.hs-adpt
You can refer to the table of contents below for the steps in this tutorial.
Create an Angular 9 App
To create an Angular 9 app, you first need to install Angular CLI.
npm install -g @angular/cli@9.0.2
Then create a directory on your hard-drive called angular-spring-boot
. Open a terminal window and navigate to this directory. Run ng new
to create an Angular application.
ng new notes --routing --style css
This process will take a minute or two to complete, depending on your internet speed and hardware. Once it’s finished, navigate into the directory and run ng serve
.
ng serve
Open your browser to http://localhost:4200
, and you’ll see the default homepage.
Stop the ng serve
process using Ctrl+C in your terminal.
Add Angular Authentication using OpenID Connect
OpenID Connect (also called OIDC) is an identity later based on the OAuth 2.0 specification. It leverages JSON Web Tokens (JWT) to provide an ID token and other features like discoverability and a /userinfo
endpoint.
Okta has Authentication and User Management APIs that reduce development time with instant-on, scalable user infrastructure. Okta’s intuitive API and expert support make it easy for developers to authenticate, manage, and secure users + roles in any application.
Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register
to sign up for a new account. If you already have an account, run okta login
.
Then, run okta apps create
. Select the default app name, or change it as you see fit.
Choose Single-Page App and press Enter.
Use http://localhost:4200/callback
for the Redirect URI and accept the default Logout Redirect URI of http://localhost:4200
.
What does the Okta CLI do?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200
. You will see output like the following when it’s finished:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
NOTE: You can also use the Okta Admin Console to create your app. See Create an Angular App for more information.
Copy your client ID and your issuer URI into the following command.
ng add @oktadev/schematics --issuer=$issuer --clientId=$clientId
This command adds Okta’s Angular SDK and configures OIDC authentication for your app.
It creates a home.component.ts
that has authentication logic, as well as a template that renders login and logout buttons.
import { Component, OnInit } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
isAuthenticated: boolean;
constructor(public oktaAuth: OktaAuthService) {
}
async ngOnInit() {
this.isAuthenticated = await this.oktaAuth.isAuthenticated();
// Subscribe to authentication state changes
this.oktaAuth.$authenticationState.subscribe(
(isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
);
}
}
<div>
<button *ngIf="!isAuthenticated" (click)="oktaAuth.loginRedirect()">Login</button>
<button *ngIf="isAuthenticated" (click)="oktaAuth.logout()">Logout</button>
</div>
There’s also an HttpInterceptor
created to add an access token to outgoing HTTP requests.
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, from } from 'rxjs';
import { OktaAuthService } from '@okta/okta-angular';
import { Injectable } from '@angular/core';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private oktaAuth: OktaAuthService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return from(this.handleAccess(request, next));
}
private async handleAccess(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {
// Only add an access token to whitelisted origins
const allowedOrigins = ['http://localhost'];
if (allowedOrigins.some(url => request.urlWithParams.includes(url))) {
const accessToken = await this.oktaAuth.getAccessToken();
request = request.clone({
setHeaders: {
Authorization: 'Bearer ' + accessToken
}
});
}
return next.handle(request).toPromise();
}
}
You might notice that tokens are only added for http://localhost . You’ll need to modify the allowedOrigins array to include your production URL eventually.
|
Start your app again using ng serve
, open a private/incognito window to http://localhost:4200
, and you’ll see a Login button in the bottom left.
Click on it, and you’ll be redirected to Okta to log in.
Enter valid credentials, and you’ll be redirected back to your app. There will now be a Logout button, indicating that you’ve authenticated successfully.
Now that you’ve created a secure Angular 9 app let’s create a Spring Boot app to serve up data with a REST API.
Create a Spring Boot 2.2 App
The good folks at Pivotal created start.spring.io to help you create Spring Boot apps quickly with minimal fuss. This site is a Spring Boot app that has a REST API you can talk to with HTTPie.
Kotlin is an intriguing language for Spring developers because it reduces boilerplate code and allows succinct, effective code. Kotlin is 100% interoperable with Java, so you can continue to use the Java libraries and frameworks you know and love. Not only that, but Spring has first-class support for Kotlin.
Create a new Spring Boot app that uses Java 11, Kotlin, Gradle, and has the necessary dependencies to create a secure CRUD API.
http https://start.spring.io/starter.zip bootVersion==2.2.4.RELEASE javaVersion==11 language==kotlin \
artifactId==notes-api groupId==com.okta.developer packageName==com.okta.developer.notes \
type==gradle-project dependencies==h2,data-jpa,data-rest,okta,web -d
Run this command in a terminal, and a notes-api.zip
file will be downloaded. Expand it into the angular-spring-boot/notes-api
directory.
unzip notes-api.zip -d angular-spring-boot/notes-api
You can also use start.spring.io in your browser to create this same app.
Secure Spring Boot with Spring Security, OAuth 2.0, and OIDC
Because you selected Okta as a dependency, you’ll need to create an OIDC app for it to authenticate with Okta. You could use the client ID from your Angular app, but if you ever want to allow people to log in to your Spring Boot app, it’ll need its own OIDC app.
In a terminal window, navigate into the angular-spring-boot/notes-api
directory.
Run okta apps create
. Select the default app name, or change it as you see fit.
Choose Web and press Enter.
Select Okta Spring Boot Starter.
Accept the default Redirect URI values provided for you. That is, a Login Redirect of http://localhost:8080/login/oauth2/code/okta
and a Logout Redirect of http://localhost:8080
.
What does the Okta CLI do?
The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:
Okta application configuration has been written to:
/path/to/app/src/main/resources/application.properties
Open src/main/resources/application.properties
to see the issuer and credentials for your app.
okta.oauth2.issuer=https://dev-133337.okta.com/oauth2/default
okta.oauth2.client-id=0oab8eb55Kb9jdMIr5d6
okta.oauth2.client-secret=NEVER-SHOW-SECRETS
NOTE: You can also use the Okta Admin Console to create your app. See Create a Spring Boot App for more information.
Having your app’s client secret in an application.properties
file that might be checked in to source control is a bad idea.
For this example, remove the Okta properties from application.properties
and copy them into a new okta.env
file. Ignore *.env
in your notes-api/.gitignore
file and remove the \
(if there is one) in your issuer’s URL.
export OKTA_OAUTH2_ISSUER=https://{yourOktaDomain}/oauth2/default
export OKTA_OAUTH2_CLIENT_ID={yourClientId}
export OKTA_OAUTH2_CLIENT_SECRET={yourClientSecret}
Run source okta.env
to set your Okta configuration as environment variables. If you’re on Windows, edit this file to change export
to set
. Then rename it to okta.bat
and run it from your terminal.
Start your app using ./gradlew bootRun
. Open http://localhost:8080
in a browser, and you’ll be redirected to Okta to sign in.
If you don’t get prompted, it’s because you’re already logged in. Try it in an incognito window to see the full login flow. |
Spring Boot as an OAuth 2.0 Resource Server
Your Spring Boot API is now secure, but it’s not configured to look for an Authorization
header with an access token in it. You need to write some code to make your Spring Boot API an OAuth 2.0 resource server.
Create a SecurityConfiguration.kt
class in the same directory as DemoApplication.kt
:
package com.okta.developer.notes
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
//@formatter:off
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt()
//@formatter:on
}
}
The oauth2Login() configuration is not necessary for this example to work. It’s only needed if you want to require authentication from a browser.
|
Add a Notes REST API with Spring Data REST
Start by creating a new Note
entity in src/main/kotlin/…/notes/DemoApplication.kt
.
package com.okta.developer.notes
import com.fasterxml.jackson.annotation.JsonIgnore
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
@Entity
data class Note(@Id @GeneratedValue var id: Long? = null,
var title: String? = null,
var text: String? = null,
@JsonIgnore var user: String? = null)
Kotlin’s data classes are built to hold data. By adding the data
keyword, your class will get equals()
, hashCode()
, toString()
, and a copy()
function. The Type? = null
syntax means the arguments are nullable when creating a new instance of the class.
Create a NotesRepository
for persisting the data in your notes. Add the following lines of code just below your Note
entity.
@RepositoryRestResource
interface NotesRepository : JpaRepository<Note, Long>
The extends
syntax differs from Java and is a lot more concise (a colon instead of extends
). If your IDE doesn’t automatically add imports, you’ll need to add the following at the top of the file.
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.rest.core.annotation.RepositoryRestResource
To automatically add the username to a note when it’s created, add a RepositoryEventHandler
that is invoked before creating the record.
@Component
@RepositoryEventHandler(Note::class)
class AddUserToNote {
@HandleBeforeCreate
fun handleCreate(note: Note) {
val username: String = SecurityContextHolder.getContext().getAuthentication().name
println("Creating note: $note with user: $username")
note.user = username
}
}
The imports for this class are:
import org.springframework.data.rest.core.annotation.HandleBeforeCreate
import org.springframework.data.rest.core.annotation.RepositoryEventHandler
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
Create a DataInitializer.kt
class that populates the database with some default data on startup.
package com.okta.developer.notes
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
@Component
class DataInitializer(val repository: NotesRepository) : ApplicationRunner {
@Throws(Exception::class)
override fun run(args: ApplicationArguments) {
listOf("Note 1", "Note 2", "Note 3").forEach {
repository.save(Note(title = it, user = "user"))
}
repository.findAll().forEach { println(it) }
}
}
Restart your Spring Boot app, and you should see the following printed to your console on startup.
Note(id=1, title=Note 1, text=null, user=user)
Note(id=2, title=Note 2, text=null, user=user)
Note(id=3, title=Note 3, text=null, user=user)
Create a UserController.kt
class (in the same directory as DemoApplication.kt
) and use it to filter notes by the currently logged-in user. While you’re at it, add a /user
endpoint that returns the user’s information.
package com.okta.developer.notes
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.security.Principal
@RestController
class UserController(val repository: NotesRepository) {
@GetMapping("/user/notes")
fun notes(principal: Principal): List<Note> {
println("Fetching notes for user: ${principal.name}")
val notes = repository.findAllByUser(principal.name)
if (notes.isEmpty()) {
return listOf()
} else {
return notes
}
}
@GetMapping("/user")
fun user(@AuthenticationPrincipal user: OidcUser): OidcUser {
return user;
}
}
The findAllByUser()
method doesn’t exist on NotesRepository
, so you’ll need to add it. Thanks to Spring Data JPA, all you need to do is add the method definition to the interface, and it will handle generating the finder method in the implementation.
interface NotesRepository : JpaRepository<Note, Long> {
fun findAllByUser(name: String): List<Note>
}
To prevent conflicting paths with the REST endpoints created by @RepositoryRestResource
, set the base path to /api
in application.properties
.
spring.data.rest.base-path=/api
Restart your Spring Boot app, navigate to http://localhost:8080/user
, and you’ll see a whole plethora of details about your account. Opening http://localhost:8080/api/notes
will show the default notes entered by the DataInitializer
component.
Add a CORS Filter for Angular
In order for your Angular app (on port 4200) to communicate with your Spring Boot app (on port 8080), you have to enable CORS (cross-origin resource sharing). You can do this by giving your DemoApplication
a body and defining a corsFilter
bean inside it.
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.core.Ordered
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.filter.CorsFilter
@SpringBootApplication
class DemoApplication {
@Bean
fun simpleCorsFilter(): FilterRegistrationBean<CorsFilter> {
val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()
config.allowCredentials = true
config.allowedOrigins = listOf("http://localhost:4200")
config.allowedMethods = listOf("*");
config.allowedHeaders = listOf("*")
source.registerCorsConfiguration("/**", config)
val bean = FilterRegistrationBean(CorsFilter(source))
bean.order = Ordered.HIGHEST_PRECEDENCE
return bean
}
}
Restart your Spring Boot app after adding this bean.
To see how your final DemoApplication
file should look, you can view the completed version in GitHub.
Now that your API is working, it’s time to develop a UI for it with Angular 9!
Add a Notes CRUD Feature in Angular
Angular Schematics is a workflow tool that allows you to manipulate any project that has a package.json
. Angular CLI is based on Schematics. OktaDev Schematics uses Schematics to update and add new files to projects. There’s even an Angular CRUD schematic!
Angular CRUD allows you to generate CRUD (create, read, update, and delete) screens and associated files from JSON.
In your Angular notes
app, install angular-crud
using npm:
npm i -D angular-crud@1.0.0
Then create a src/app/note
directory.
mkdir -p src/app/note
Then, create a model.json
file in it that defines metadata that’s used when generating files.
{
"title": "Notes",
"entity": "note",
"api": {
"url": "http://localhost:8080/api/notes"
},
"filter": [
"title"
],
"fields": [
{
"name": "id",
"label": "Id",
"isId": true,
"readonly": true,
"type": "number"
},
{
"name": "title",
"type": "string",
"label": "Title"
},
{
"name": "text",
"type": "string",
"label": "Text"
}
]
}
Run the command below to generate CRUD screens.
ng g angular-crud:crud-module note
You will see the following output.
CREATE src/app/note/note-filter.ts (44 bytes)
CREATE src/app/note/note.module.ts (659 bytes)
CREATE src/app/note/note.routes.ts (346 bytes)
CREATE src/app/note/note.service.spec.ts (607 bytes)
CREATE src/app/note/note.service.ts (1744 bytes)
CREATE src/app/note/note.ts (69 bytes)
CREATE src/app/note/note-edit/note-edit.component.html (1097 bytes)
CREATE src/app/note/note-edit/note-edit.component.spec.ts (978 bytes)
CREATE src/app/note/note-edit/note-edit.component.ts (1493 bytes)
CREATE src/app/note/note-list/note-list.component.html (1716 bytes)
CREATE src/app/note/note-list/note-list.component.spec.ts (978 bytes)
CREATE src/app/note/note-list/note-list.component.ts (1091 bytes)
UPDATE src/app/app.module.ts (540 bytes)
This schematic creates a NotesModule
, routes, a service to communicate with the API, and list/edit screens for viewing and editing notes. If you look at the src/app/note/note.routes.ts
file, you’ll see the routes it creates.
import { Routes } from '@angular/router';
import { NoteListComponent } from './note-list/note-list.component';
import { NoteEditComponent } from './note-edit/note-edit.component';
export const NOTE_ROUTES: Routes = [
{
path: 'notes',
component: NoteListComponent
},
{
path: 'notes/:id',
component: NoteEditComponent
}
];
Add a link to the NoteListComponent
in src/app/home/home.component.html
.
<div>
<button *ngIf="!isAuthenticated" (click)="oktaAuth.loginRedirect()">Login</button>
<p><a routerLink="/notes" *ngIf="isAuthenticated">View Notes</a></p>
<button *ngIf="isAuthenticated" (click)="oktaAuth.logout()">Logout</button>
</div>
Change src/app/app.component.html
to be as simple as it can be.
<h1>{{ title }} app is running!</h1>
<router-outlet></router-outlet>
If you want npm test to pass after modifying this template, you’ll need to change app.component.spec.ts to look for querySelector('h1') instead of querySelector('.content span') .
|
Run ng serve
(and make sure your Spring Boot app is running too).
Log in, and you should see a View Notes link.
Click on the link, and you’ll see a list screen like the one below. No notes are displayed because you haven’t created any notes that are tied to your user.
Click on the New link to add a new note.
Add a new note, and you’ll see a message like this in your backend console.
Creating note: Note(id=null, title=1st note, text=Wahoo!, user=null) with user: matt.raible@okta.com
You still won’t see notes in the list. You need to change the NoteService
to call the /user/notes
endpoint to get your notes.
find(filter: NoteFilter): Observable<Note[]> {
const params = {
title: filter.title,
};
const userNotes = 'http://localhost:8080/user/notes';
return this.http.get<Note[]>(userNotes, {params, headers});
}
Now you’ll see your notes listed. Nice work!
You might be wondering how the NoteListComponent
works. It loads the user’s notes from the NoteService
when the component initializes, and also contains select()
and delete()
methods. The reason it’s able to talk to your secured Spring Boot API is that the aforementioned AuthInterceptor
adds an access token to the request.
import { Component, OnInit } from '@angular/core';
import { NoteFilter } from '../note-filter';
import { NoteService } from '../note.service';
import { Note } from '../note';
@Component({
selector: 'app-note',
templateUrl: 'note-list.component.html'
})
export class NoteListComponent implements OnInit {
filter = new NoteFilter();
selectedNote: Note;
feedback: any = {};
get noteList(): Note[] {
return this.noteService.noteList;
}
constructor(private noteService: NoteService) {
}
ngOnInit() {
this.search();
}
search(): void {
this.noteService.load(this.filter);
}
select(selected: Note): void {
this.selectedNote = selected;
}
delete(note: Note): void {
if (confirm('Are you sure?')) {
this.noteService.delete(note).subscribe(() => {
this.feedback = {type: 'success', message: 'Delete was successful!'};
setTimeout(() => {
this.search();
}, 1000);
},
err => {
this.feedback = {type: 'warning', message: 'Error deleting.'};
}
);
}
}
}
The Edit link in this component’s template links to the NoteEditComponent
.
<a [routerLink]="['../notes', item.id ]" class="btn btn-secondary">Edit</a>
The NoteEditComponent
has methods for loading a note, saving a note, and canceling.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NoteService } from '../note.service';
import { Note } from '../note';
import { map, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-note-edit',
templateUrl: './note-edit.component.html'
})
export class NoteEditComponent implements OnInit {
id: string;
note: Note;
feedback: any = {};
constructor(
private route: ActivatedRoute,
private router: Router,
private noteService: NoteService) {
}
ngOnInit() {
this
.route
.params
.pipe(
map(p => p.id),
switchMap(id => {
if (id === 'new') { return of(new Note()); }
return this.noteService.findById(id);
})
)
.subscribe(note => {
this.note = note;
this.feedback = {};
},
err => {
this.feedback = {type: 'warning', message: 'Error loading'};
}
);
}
save() {
this.noteService.save(this.note).subscribe(
note => {
this.note = note;
this.feedback = {type: 'success', message: 'Save was successful!'};
setTimeout(() => {
this.router.navigate(['/notes']);
}, 1000);
},
err => {
this.feedback = {type: 'warning', message: 'Error saving'};
}
);
}
cancel() {
this.router.navigate(['/notes']);
}
}
Fix the Note Edit Feature
One of the problems with the NoteEditComponent
is it assumes the API returns an ID. Since Spring Data REST uses HATEOS by default, it returns links instead of IDs. You can change this default to return IDs by creating a RestConfiguration
class in your Spring Boot app. You might notice you can also configure the base path in this class, instead of in application.properties
.
package com.okta.developer.notes
import org.springframework.context.annotation.Configuration
import org.springframework.data.rest.core.config.RepositoryRestConfiguration
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer
@Configuration
class RestConfiguration : RepositoryRestConfigurer {
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration?) {
config?.exposeIdsFor(Note::class.java)
config?.setBasePath("/api")
}
}
Another option is to modify the Angular side of things. Since the ID is passed into the NoteEditComponent
, you can set it as a local variable, then set it on the note after it’s returned. Here’s a diff of what changes need to be made in notes/src/app/note/note-edit/note-edit.component.ts
.
--- a/notes/src/app/note/note-edit/note-edit.component.ts
+++ b/notes/src/app/note/note-edit/note-edit.component.ts
@@ -30,11 +29,13 @@ export class NoteEditComponent implements OnInit {
map(p => p.id),
switchMap(id => {
if (id === 'new') { return of(new Note()); }
+ this.id = id;
return this.noteService.findById(id);
})
)
.subscribe(note => {
this.note = note;
+ this.note.id = +note.id;
this.feedback = {};
},
err => {
@@ -47,6 +48,7 @@ export class NoteEditComponent implements OnInit {
this.noteService.save(this.note).subscribe(
note => {
this.note = note;
+ this.note.id = +this.id;
this.feedback = {type: 'success', message: 'Save was successful!'};
setTimeout(() => {
this.router.navigate(['/notes']);
In the final example for this post, I opted to return IDs from my Spring Boot API.
Lock Down Spring Boot with Recommended Security Practices
In 10 Excellent Ways to Secure Your Spring Boot Application, I recommended a few Spring Boot-specific items:
-
Use HTTPS in Production
-
Enable Cross-Site Request Forgery (CSRF) Protection
-
Use a Content Security Policy (CSP) to Prevent XSS Attacks
-
Use OpenID Connect for Authentication
You’ve already implemented #4 with Okta, but what about the others?
You can use mkcert to generate local, valid TLS certificates. To force HTTPS, you just need to configure Spring Security. I like just to do it in production, so I don’t need to install certificates in development.
CSRF protection and a CSP can be configured with Spring Security.
Modify your SecurityConfiguration
class with these security enhancements.
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
//@formatter:off
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt()
http.requiresChannel()
.requestMatchers(RequestMatcher {
r -> r.getHeader("X-Forwarded-Proto") != null
}).requiresSecure() (1)
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); (2)
http.headers()
.contentSecurityPolicy("script-src 'self'; report-to /csp-report-endpoint/"); (3)
//@formatter:on
}
}
1 | Force HTTPS in production |
2 | Configure the CSRF Cookie so it can be read by JavaScript |
3 | Configure a CSP that only allows local scripts |
Angular’s HttpClient
has built-in support for the client-side half of the CSRF protection. It’ll read the cookie sent by Spring Boot and return it in an X-XSRF-TOKEN
header. You can read more about this in Angular’s Security docs.
In this particular example, the CSP won’t be used since Angular is a separate app. However, if you were to include the Angular app in your Spring Boot artifact, it’d come in handy.
Once you’ve deployed your Spring Boot app to a public URL, you can test your CSP headers are working with securityheaders.com. |
Learn More About Angular, Spring Boot, and Kotlin
In this tutorial, I showed you how to create an Angular 9 app, a Spring Boot 2.2 app, and how to secure communication between them with OAuth 2.0 and OIDC. You used Kotlin on the backend; a language loved by many. You used Angular Schematics to generate code for authentication and CRUD, improving your efficiency as a developer.
This tutorial did not show you how to make your Angular app look good, add validation, or how to deploy it to a public server. I’ll tackle those topics in an upcoming post. In the meantime, you can see a previous tutorial I wrote to see how this app might look with Angular Material.
You can find the source code for the completed application at oktadeveloper/okta-spring-boot-2-angular-9-example.
Our blog has a bunch of Angular, Spring Boot, and Kotlin tutorials. Here are some I recommend:
To read more about the Angular 9 and Spring Boot 2.2 releases, I recommend these articles:
-
Spring Boot 2.2.0 from the Spring Team’s Blog
For Angular 8, 7, and 5 versions of this tutorial, see my past posts.
-
Angular 8 + Spring Boot 2.2: Build a CRUD App Today! - May 2019
-
Build a Basic CRUD App with Angular 7.0 and Spring Boot 2.1 - August 2018
-
Build a Basic CRUD App with Angular 5.0 and Spring Boot 2.0 - December 2017
If you like this blog post and want to see more like it, follow @oktadev on Twitter, subscribe to our YouTube channel, or follow us on LinkedIn. As always, please leave a comment below if you have any questions.
Update:
This blog post sparked a 4-part series on Angular and Spring Boot. Check out the other posts in the series to learn more! 😃
Changelog:
- Nov 21, 2021: Added announcement for The Angular Mini-Book. Its "Integrate Angular with Spring Boot" chapter was inspired by this blog post.
- Apr 6, 2021: Changed Okta OIDC app creation instructions to use the Okta CLI. See changes in okta-blog#673.
- Jun 18, 2020: Added links to other blog posts in series.
- May 18, 2020:
Updated to remove
/implicit
from Login redirect URL for Angular. See the code changes in the example app on GitHub. Changes to this article can be viewed in oktadeveloper/okta-blog#296. - Feb 19, 2020: Updated to use Angular CLI 9.0.2 (with Angular 9.0.1). See the code changes in the example app on GitHub. Changes to this article can be viewed in oktadeveloper/okta-blog#197.
- Jan 30, 2020: Updated to use Spring Boot 2.2.4 and Angular CLI 9.0 RC11 (with Angular 9.0 RC11). See the code changes in the example app on GitHub. Changes to this article can be viewed in oktadeveloper/okta-blog#175.
Okta Developer Blog Comment Policy
We welcome relevant and respectful comments. Off-topic comments may be removed.