This simple tutorial aims to demo the core capabilities of Spring Security and create a seed project for future Authorization/Authentication related tutorials. The latest version of Spring Security at the time is version 6.0.
Features Covered
- Spring Security Configuration
- Basic Auth
- Request filtering
Complexity
Beginner/Intermediate.
Prerequisites
You will need an IDE of your choice and JDK 17 or later installed for this project to run on your machine. JDK 17 was picked because it is the latest Long-Term-Support (LTS) version, but later JDK versions should also run without issues.
Generating a Spring Project
The initial Spring project, with Spring Security and Spring Web as dependencies, is generated via Spring Initializer. The settings in the image are below, or you can pull the seed project from the GitHub repository.
Environment
I will only go through a few details about the development environment setup because it’s the default setup for a Spring application. Still, for more junior readers, I want to highlight that jdk17 must be installed on your machine. To check it on Mac, you can run java –version in the terminal to review; as shown in the screenshot, I have OpenJDK 17.0.5 installed.
I personally like the IntelliJ Community edition for the IDE, but any other will work just fine.
Configure Lombok
Lombok is a java library that automatically plugs into your editor and build tools to remove a burden from you as a developer to write all the boilerplate code. It can be added to the project via Spring Initializer. If you are using the latest version of IntelliJ, the Lombok plugin is bundled as part of it by default, but on older versions or other IDEs, you might need to install it manually. That’s why I wanted to point it out.
Simple CRUD API
To build a use case for Spring Security, we first need to have an API protected by it. Let’s create a simple Spring app that performs CRUD operations on bank accounts.
Project structure
As this is a small sample project, we will aim for a flat structure consisting of three base packages: controllers, models, and services.
Account model
In the models’ package Account model, which describes the account Object, is going to live.
The object will have four properties:
- id – to identify the individual account
- owner – account owner name
- iban – IBAN of the account
- amount – an amount of money in the account
Also, it will be decorated by three Lombok annotations to generate the Constructors and Getters/Setters.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private Long id;
private String owner;
private String iban;
private BigDecimal amount;
}
Account Controller
Once the model is created, APIs used for the CRUD operations can be defined as the next step.
The controller has to be annotated with @RestController annotation for Spring to know it’s a controller, and the path has to be mapped for the APIs. It’s a good practice to state the purpose of the API in the path like “account” and to have the API versioned for future upgrades.
When describing the controller’s constructor, I took one step further and injected the AccountService dependency, as it will be needed in the next step.
Regarding the APIs, I won’t go into the details on what each parameter does/means as it’s the default setup for a basic CRUD controller, which any other Spring app would have.
To summarise, AccountController implements the following APIs:
- Two GET operations to fetch the accounts (“/api/v1/account”), (“/api/v1/account/id”).
- POST to create a new account (“/api/v1/account”).
- PUT to update an existing account (“/api/v1/account”).
- DELETE to remove an account (“/api/v1/account/id”).
@RestController
@RequestMapping("/api/v1/account")
public class AccountController {
private AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping
public List<Account> findAll() {
return accountService.findAll();
}
@GetMapping(value = "/{id}")
public Account findById(@PathVariable("id") Long id) {
return accountService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public String create(@RequestBody Account account) {
return accountService.create(account);
}
@PutMapping
@ResponseStatus(HttpStatus.OK)
public Account update(@RequestBody Account account) {
return accountService.update(account);
}
@DeleteMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public void delete(@PathVariable("id") Long id) {
accountService.deleteById(id);
}
}
Account Service
Code in the Account Service will be a bit hacky for simplicity as we don’t have any Database and are building the Account API to showcase Spring Security capabilities.
In the constructor of the AccountService, let’s add a couple of test accounts to the accounts list so we wouldn’t need to create new ones every time the app is restarted.
- For the findAll method – the accounts list, populated in the constructor, will be returned.
- FindById – will filter the accounts list for a matching id.
- Create – will add a new account to the list.
- Update – will screen the accounts list for a matching id and map the values.
- Delete – similarly to findById, will use a filter to filter out the id in mind instead of looking for it.
@Service
public class AccountService {
private List<Account> accounts;
public AccountService() {
accounts = new ArrayList<>();
accounts.add(new Account(123l, "St. Paddy", "IE64IRCE92050112345678", new BigDecimal(1000)));
accounts.add(new Account(5555l, "Speedy Gonzales", "ES7921000813610123456789", new BigDecimal(5000)));
accounts.add(new Account(7688999l, "Mr Troll", "NO8330001234567", new BigDecimal(200)));
}
public List<Account> findAll() {
return accounts;
}
public Account findById(Long id) {
return accounts.stream()
.filter(account -> account.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Account not found."));
}
public String create(Account account) {
accounts.add(account);
return "Account created for: " + account.getOwner();
}
public Account update(Account newAccount) {
return accounts.stream()
.filter(a -> a.getId().equals(newAccount.getId()))
.map(a -> {
a.setId(newAccount.getId());
a.setIban(newAccount.getIban());
a.setAmount(newAccount.getAmount());
a.setOwner(newAccount.getOwner());
return a;
}).findFirst()
.orElseThrow(() -> new RuntimeException("Account not found."));
}
public void deleteById(Long id) {
accounts = accounts.stream()
.filter(account -> !account.getId().equals(id))
.collect(Collectors.toList());
}
}
Starting the CRUD API
Before the app is started for testing, let’s disable Spring Security, as it is auto-configured by default once dependency is added, and not all of the REST methods are allowed out of the box. To disable the Security SecurityAutoConfiguration, have to be excluded.
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
Also, while testing locally, I like to change the application’s port so it wouldn’t collide with any other app running on the default port locally. That can be done by adding server.port=8666 to the application.properties file.
To start the app gradle bootRun command can be run if starting from the terminal or via IDE as any other Spring app. Start-up logs in below.
:: Spring Boot :: (v3.0.0)
2022-12-29T17:22:48.206+02:00 INFO 84565 — [ main] c.r.security.SecurityApplication : Starting SecurityApplication using Java 17.0.5 with PID 84565
2022-12-29T17:22:48.211+02:00 INFO 84565 — [ main] c.r.security.SecurityApplication : No active profile set, falling back to 1 default profile: “default”
2022-12-29T17:22:49.245+02:00 INFO 84565 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8666 (http)
2022-12-29T17:22:49.260+02:00 INFO 84565 — [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-12-29T17:22:49.260+02:00 INFO 84565 — [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-12-29T17:22:49.363+02:00 INFO 84565 — [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-12-29T17:22:49.366+02:00 INFO 84565 — [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1095 ms
2022-12-29T17:22:49.761+02:00 INFO 84565 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8666 (http) with context path ”
2022-12-29T17:22:49.771+02:00 INFO 84565 — [ main] c.r.security.SecurityApplication : Started SecurityApplication in 2.014 seconds (process running for 2.4)
Testing the CRUD API
To test the application I like to use Postman as an API testing tool, but any other tool or a simple cURL command will work.
Below are listed cURL commands with request and response for each of the four APIs to be tested:
GET /api/v1/account – to fetch all the Accounts
## Request:
curl --request GET 'localhost:8666/api/v1/account'
## Response:
[
{
"id": 123,
"owner": "St. Paddy",
"iban": "IE64IRCE92050112345678",
"amount": 1000
},
{
"id": 5555,
"owner": "Speedy Gonzales",
"iban": "ES7921000813610123456789",
"amount": 5000
},
{
"id": 7688999,
"owner": "Mr Troll",
"iban": "NO8330001234567",
"amount": 200
}
]
GET /api/v1/account/123 – to fetch one of the Accounts
## Request:
curl --request GET 'localhost:8666/api/v1/account/123'
## Response:
{
"id": 123,
"owner": "St. Paddy",
"iban": "IE64IRCE92050112345678",
"amount": 1000
}
DELETE /api/v1/account/123 – to delete an Account
## Request:
curl --request DELETE 'localhost:8666/api/v1/account/123'
## Response Code 200 OK
POST /api/v1/account – to create an Account
## Request:
curl --request POST 'localhost:8666/api/v1/account' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": 123444,
"owner": "test",
"iban": "IE64IRCE92050112345678",
"amount": 500
}'
## Response: “Account created for: test”
PUT /api/v1/account – to update an Account
## Request:
curl --request PUT 'localhost:8666/api/v1/account' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": 123444,
"owner": "test",
"iban": "IE64IRCE92050112345678",
"amount": 449
}'
## Response:
{
"id": 123444,
"owner": "test",
"iban": "IE64IRCE92050112345678",
"amount": 449
}
Spring Security – Basic Authentication
Once Spring Security is pulled in as a dependency into the Spring project, it automatically configures Basic Authorization for us. But from my experience, the default configuration rarely covers intended functionality.
For Spring Security to do auto configuration again, the exclusion of SecurityAutoConfiguration, which we added while testing the CRUD API, must be reverted.
SecurityApplication class after the update:
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
Once the application is started, Spring Security will generate a password for the default “user” and show it in the application logs:
Using generated security password: a106538c-a187-4321-bf26-76d98a307943
This generated password is for development use only your security configuration must be updated before running your application in production.
Note: If needed, it is straightforward to change the default username and password too. You need to add:
spring.security.user.name=newUser
spring.security.user.password=newPassword
To the application.properties file.
Test cURL – with successful Authorization
The main difference between this cURL and the one executed to fetch the accounts while testing the application is the authorisation header in which the username and password pair is encrypted by default while using Postman.
## Request:
curl --location --request GET 'localhost:8666/api/v1/account' \
--header 'Authorization: Basic dXNlcjphMTA2NTM4Yy1hMTg3LTQzMjEtYmYyNi03NmQ5OGEzMDc5NDM='
## Response:
[
{
"id": 123,
"owner": "St. Paddy",
"iban": "IE64IRCE92050112345678",
"amount": 1000
},
{
"id": 5555,
"owner": "Speedy Gonzales",
"iban": "ES7921000813610123456789",
"amount": 5000
},
{
"id": 7688999,
"owner": "Mr Troll",
"iban": "NO8330001234567",
"amount": 200
}
]
Test cURL – with failed Authorization
The default configuration allows only GET operations for the default User, and as we try to execute the DELTE 401 error is thrown as the user doesn’t have sufficient permissions. The same 401 error would be returned if the wrong password or a user name were set in the Authorization header.
## Request:
curl --location --request DELETE 'localhost:8666/api/v1/account/123' \
--header 'Authorization: Basic dXNlcjphMTA2NTM4Yy1hMTg3LTQzMjEtYmYyNi03NmQ5OGEzMDc5NDM='
## Response: 401 Unauthorized
Spring Security Configuration
To demo the basic configuration of Spring Security, we will create a new package for the configuration files: com.releaseweekend.security.config and add a class SecurityConfiguration to the package.
Content of SecurityConfiguration class:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("admin"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers("/api")
.authenticated()
.and()
.httpBasic();
http.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
return encoder;
}
}
- @EnableWebSecurity annotation – have to be added to an @Configuration class to have the Spring Security configuration extended by custom functionality.
- userDetailsService method – userDetailsService will be used to manage the user details as we don’t have an external service from which the application could pull the user data. We will create two users “user” and “admin”.
- filterChain method – this method is used to manage access to the application. To do that will call a simple requestMatchers function with the “/api” as a parameter. This will make sure all the traffic pointed to the URL, which includes /api in its path, will be .authenticated() by any user capable of authorizing with .httpBasic(). Lastly http.csrf().disable() was added to the method because the application is intended to be used as an API service instead of serving content for the web browser. Otherwise, you should ensure to include the CSRF token in the request.
- passwordEncoder method – the purpose of passwordEncoder is self-explanatory. It is used to configure how passwords should be encoded, and there are various options to encode the password, from popular SHA-256 to outdated MD5 algorithms.
Testing The Configuration
After the basic configuration is set up, Accounts API should be available for the authorized users (“user”, “admin”) and for unauthorized ones – return 401 Unauthorized error. A couple of cURL examples are below.
GET /api/v1/account/123 – to fetch one of the Accounts by Admin
## Request:
curl --request GET 'localhost:8666/api/v1/account/123' \
--header 'Authorization: Basic YWRtaW46YWRtaW4='
## Response:
{
"id": 123,
"owner": "St. Paddy",
"iban": "IE64IRCE92050112345678",
"amount": 1000
}
DELETE /api/v1/account/123 – to delete an Account by User
## Request:
curl --request DELETE 'localhost:8666/api/v1/account/123' \
--header 'Authorization: Basic dXNlcjpwYXNzd29yZA=='
## Response Code 200 OK
GET /api/v1/account – to fetch the Accounts by Admin with a wrong password
## Request:
curl --request GET 'localhost:8666/api/v1/account' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ='
## Response:
{
"timestamp": "2023-01-05T10:53:43.519+00:00",
"status": 401,
"error": "Unauthorized",
"path": "/api/v1/account"
}
The source code of the application with basic Spring Security configuration can be found in the GitHub repository.
Conclusion
There are a lot of similar posts on the internet which cover basic Spring Security configuration. Even the official Spring website has a tutorial for Spring/Angular applications. Still, as stated at the beginning of the tutorial, the primary goal of this one is to create a seed project for future Authorization/Authentication related tutorials. Also, this post might be helpful for someone new to the Spring ecosystem as it describes the setup from scratch.
Leave a Reply
You must be logged in to post a comment.