Securing Your API with Spring Security
This post links to: Building a Spring Boot CRUD App With Postgres from Scratch: The Complete Guide.
This guide follows on from Refactoring Our Application With Lombok.
In this blog post we will discuss adding Spring Security to our API. Even a simple CRUD API like ours needs some sort of security, especially if one day we want to share it with the world.
Why Add Security?
Now the answer to this might sound obvious, we don't want unauthenticated or unauthorised people to access and use our API. Simple. However, there are some more specific points to mention:
- Prevent unauthorised access to data - this is the main reason that we've already mentioned. For our API, the
Userentity (we previously created) could include sensitive fields like: emails, internal IDs, audit metadata, and even data that seems harmless on its own but could become sensitive when combined with other information. This would include information such as job titles, login names, office locations that could be aggregated and used for social engineering attacks. - Stop destructive actions & enable proper permissions - without auth, anyone can accidentally or maliciously create/update/delete resources. For an extra secure API, we would have authentication (who can use the API) and then authorisation (what an authenticated user can do with the API). This is particularly helpful for delegating roles e.g. admins, read only users etc.
- Protect your API in the real world - APIs can get exposed more easily than people expect (misconfigured ingress, public demo environments, copied URLs). Having some sort of security at the application level adds another layer between a malicious actor and your data.
- Build good habits early - once clients depend on an API, adding auth later becomes painful (breaking changes, coordination across consumers). It can also be tricky setting up testing environments and test users for an API that once did not have security. Plus if your API is supporting a frontend application, that's more changes later down the line, rather than tackling the issue early.
Common Types of Web Security
Now we have an understanding of why we need security, the next step is to choose the right security for your application/API.
In this blog we are going to be using HTTP basic auth for our web security. This is mainly due to this being a demo, and I'm not expecting much traffic to my website. However, if I was to make this into something that would have a lot of users and traffic, I'd use a different type of web security.
I'd recommend reading my other page about other common types of web security. This page goes into more detail about what HTTP basic auth is and alternative web security methods.
Implementing HTTP Basic Auth in Spring
Now we've evaluated some web security options and decided to use HTTP Basic Auth, let's dive into how we can implement this in our Spring application.
Setting Up Dependencies
Let's first add the Spring Boot security starter dependency into our build.gradle file:
// build.gradle
dependencies {
//other deps...
implementation 'org.springframework.boot:spring-boot-starter-security'
// other deps...
}
You'll notice that with Spring Boot starter dependencies, we do not need to put the version. This is inherited from the version set by the org.springframework.boot plugin defined at the top of the build.gradle file.
Now we've added this dependency, if you run the application you'll notice a few changes.
Firstly, in the logs you'll see something like this:
2026-05-20T20:49:37.555+01:00 WARN 44326 --- [crud.app] [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 6366a6d5-bc66-4a7e-9f44-6cb6ccbec35b
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2026-05-20T20:49:37.557+01:00 INFO 44326 --- [crud.app] [ main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
Spring Boot security added a generated password for development use only.
Then let's say we wanted to list all our users by running the following in the terminal:
curl -X GET http://localhost:8080/api/users
You'll see that we receive no response, when usually we get an empty array like so: []
We can use the -i flag to get some more info like so:
curl -i -X GET http://localhost:8080/api/users
and we get something like this:
HTTP/1.1 401
// Some other response information
Ahh here's the underlying issue, you can now see that we get a 401 unauthorised response, exactly what we were expecting.
So right out the box, Spring Boot security will secure all your endpoints. But what if we want some endpoints publicly accessible, like Swagger endpoints? Well, we will get to that later.
We can also see a login page now when navigating to http://localhost:8080/api/users in the browser. We can use the username user and the generated password copied from the logs to log in and view the page.
The user and password can also be used in a cURL request like so:
curl -i -u user:<replace_with_generated_password> http://localhost:8080/api/users
So that's it right, we are now fully secure and ready to take on the internet.
Well... not quite.
Like previously mentioned, Basic Auth should only be used for apps that aren't critical, don't have a lot of user traffic and are mainly demos and internal tools behind firewalls and proxies.
Even if we kept Basic Auth, we only have one shared user, with a randomly generated password each time the application is deployed. Also, what if we wanted to add a read-only user for our application. With the current setup, this would be impossible. So we still have a little bit of work to do.
Adding the Basic Auth security config
The next stage is to do what the logs say: "Your security configuration must be updated before running your application in production."
So let's update our security configuration for our application. The way to do that is to define our security configuration using the @Configuration annotation and include 3 important beans: a SecurityFilterChain, a UserDetailsService and a PasswordEncoder, like in the example below:
@Configuration
public class SecurityConfig {
private static final String USER_ROLE = "USER";
private static final String ADMIN_ROLE = "ADMIN";
private static final String SWAGGER_UI_PATH = "/swagger-ui/**";
private static final String V_3_API_DOCS_PATH = "/v3/api-docs/**";
private static final String API_DOCS_PATH = "/docs/**";
private static final String API_DOCS_JSON_PATH = "/docs-json/**";
private static final String API_USERS_PATH = "/api/users/**";
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(SWAGGER_UI_PATH,
V_3_API_DOCS_PATH,
API_DOCS_PATH,
API_DOCS_JSON_PATH).permitAll()
.requestMatchers(HttpMethod.GET, API_USERS_PATH).hasAnyRole(USER_ROLE, ADMIN_ROLE)
.requestMatchers(API_USERS_PATH).hasRole(ADMIN_ROLE)
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public UserDetailsService userDetailsService(final PasswordEncoder passwordEncoder) {
final var user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles(USER_ROLE)
.build();
final var admin = User.withUsername("admin")
.password(passwordEncoder.encode("password"))
.roles(ADMIN_ROLE)
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Security Filter Chain
This bean defines how the incoming HTTP requests should be secured. Spring Security will sit in front of the controllers and intercept any requests before they reach the application. The security filter chain defines:
- Which endpoints require auth.
- Which endpoints are public.
- Which users can access certain routes.
- What auth mechanism is used
Basic Auth,JWT,OAuth2etc. - And additional security behaviour like CSRF, sessions, CORS etc.
In the previous example:
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(SWAGGER_UI_PATH,
V_3_API_DOCS_PATH,
API_DOCS_PATH,
API_DOCS_JSON_PATH).permitAll()
.requestMatchers(HttpMethod.GET, API_USERS_PATH).hasAnyRole(USER_ROLE, ADMIN_ROLE)
.requestMatchers(API_USERS_PATH).hasRole(ADMIN_ROLE)
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}
We are saying:
- Simplify our security config by disabling CSRF since our application is a stateless REST API rather than a browser-based application that uses sessions and forms.
- Filter the following HTTP requests accordingly:
- Swagger docs and API docs are public (no auth required) using the
permitAll()method. GETmethods for theAPI_USERS_PATHare accessible by any users with a user or admin role.- Only an admin role can perform other HTTP methods such as:
POST,PUT,DELETEetc. - Any other request requires authentication.
- Swagger docs and API docs are public (no auth required) using the
- Use HTTP Basic auth with default settings as our security method.
If there was no security filter chain, everything would be locked down by default.
User Details Service
This tells Spring Security how to find and validate users making HTTP requests. If someone tries to access the application with a username user and password password, Spring Security needs to know whether this user exists, whether they're authenticated and what access they have?
Right now for demo purposes we are using the InMemoryUserDetailsManager which hardcodes the users directly in the code. As you can imagine this isn't a great look for the future of our application when we deploy into production. Later in this blog we will instead look at using a database to store and access users.
For now the main takeaway is this defines the user lookup service for Spring Security.
Password Encoder
This bean defines how to handle password hashing and verification. As previously mentioned, passwords should not be stored in plaintext, instead they should be stored as hashes.
Spring Security will use the password encoder to compare the raw password with the stored hash password and verify if they match or not.
These 3 components all work together: the security filter chain filters the request, the user details service finds the user and the password encoder verifies the password of the given user. Credentials are verified and roles are checked. The request is then either allowed or denied.
Testing Our Security
We have now set up our security configuration, it's now time to test it.
Manual Testing
We can do this manually like so:
curl -i -u user:password http://localhost:8080/api/users
And you should receive a HTTP/1.1 200 response with an empty array.
Run the following:
curl -i http://localhost:8080/api/users
And you'll receive a HTTP/1.1 401.
Run this:
curl -i -u admin:password \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"jon","email":"jon@example.com", "age":40}' \
http://localhost:8080/api/users
And you can create a new user.
Run this:
curl -i -u user:password \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"jon","email":"jon@example.com"}' \
http://localhost:8080/api/users
And it'll fail to create a new user as the user role doesn't have permissions to modify users.
Looks like our security is working. However, what if someone makes a change like adding an ops role that can update users, or adding a new API endpoint that needs to be secured. It would take a lot of time to manually test all these new changes. That's where automated testing comes in.
Automated Testing
Instead of manually testing all the endpoints and checking if they are secured correctly, we can write tests to do this for us and run them everytime we make a change to our application.
Firstly, we can add the Spring Security test dependency to our build.gradle file:
// build.gradle
dependencies {
//other deps...
testImplementation 'org.springframework.security:spring-security-test'
// other deps...
}
Next we can define a MockMvc test like the example below:
@SpringBootTest
@AutoConfigureMockMvc
class UserSecurityTest {
private static final String USERS_API_PATH = "/api/users";
private static final String USER_USERNAME = "user";
private static final String USER_PASSWORD = "password";
private static final String ADMIN_USERNAME = "admin";
private static final String ADMIN_PASSWORD = "password";
@Autowired private MockMvc mockMvc;
@Test
void returnsUnauthorisedWhenNoCredentialsProvided() throws Exception {
mockMvc.perform(get(USERS_API_PATH))
.andExpect(status().isUnauthorized());
}
@Test
void allowsAUserRoleToReadUsers() throws Exception {
mockMvc.perform(get(USERS_API_PATH)
.with(httpBasic(USER_USERNAME, USER_PASSWORD)))
.andExpect(status().isOk());
}
@Test
void forbidsTheUserRoleFromCreatingUsers() throws Exception {
final var userCreateRequest = randomUserCreateRequest();
mockMvc.perform(post(USERS_API_PATH)
.with(httpBasic(USER_USERNAME, USER_PASSWORD))
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(userCreateRequest)))
.andExpect(status().isForbidden());
}
@Test
void allowsTheAdminRoleToCreateUsers() throws Exception {
final var userCreateRequest = randomUserCreateRequest();
mockMvc.perform(post(USERS_API_PATH)
.with(httpBasic(ADMIN_USERNAME, ADMIN_PASSWORD))
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(userCreateRequest)))
.andExpect(status().isOk());
}
}
This will start the application and then use MockMvc as an HTTP client to make requests to our user API. Now if we make any updates, our tests will fail, and we will hopefully catch any security flaws.
Implementing Basic Auth With a Database
As mentioned before, we want to make sure we're not storing user details as hardcoded values within our application. Instead, we can use our existing database setup to manage and store our users in a more secure way.
To do this, we first need to update our User model to include a password field and a role field:
@Builder(toBuilder = true)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User {
// Other fields...
@Column(name = "password")
private String password;
@Enumerated(EnumType.STRING)
@Column(name = "role")
private UserRole role;
}
The user roles can be defined as an enum like so:
public enum UserRole {
USER, ADMIN
}
We also need to update our UserRepository to find a user by a username:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByName(final String name);
}
This allows us to remove the UserDetailsService in our security config:
@Bean
public UserDetailsService userDetailsService(final PasswordEncoder passwordEncoder) {
final var user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles(USER_ROLE)
.build();
final var admin = User.withUsername("admin")
.password(passwordEncoder.encode("password"))
.roles(ADMIN_ROLE)
.build();
return new InMemoryUserDetailsManager(user, admin);
}
And add a DatabaseUserDetailsService that implements the UserDetailsService and overrides the loadUserByUsername() method which is used by Spring Security for authentication, this will use our new findByName() method we defined in the repository:
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public DatabaseUserDetailsService(final UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(final String username) {
final var user = userRepository.findByName(username)
.orElseThrow(() -> new UserNotFoundException(username));
return User.withUsername(user.getName())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
Seeding Our First Admin User
Next we need some way to seed our first admin user, and use the admin user to create subsequent users and manage our application.
There are a few ways to do this. One way would be to manually create the user directly in the database or use some sort of script to create the user. We can also do this in our application using the CommandLineRunner, which will run on application startup and create our admin user for us.
However, we want to avoid storing any details in our application or uploading them to GitHub. So we can do this using application properties and environment variables like so:
Create some admin properties:
@Setter
@Getter
@ConfigurationProperties(prefix = "app.security.admin")
public class AdminProperties {
private String password;
private String email;
}
Enable the configuration props:
@Configuration
@EnableConfigurationProperties(AdminProperties.class)
public class SecurityConfig {
// Configuration...
}
Add the following properties to our application.yml file:
# Other props...
app:
security:
admin:
password: ${ADMIN_PASSWORD}
email: ${ADMIN_EMAIL}
Finally, create a user seeder to seed our admin user:
@Configuration
public class UserSeeder {
private static final String ADMIN_USERNAME = "admin";
@Bean
public CommandLineRunner seedAdminUser(final UserRepository userRepository,
final PasswordEncoder passwordEncoder,
final AdminProperties adminProperties) {
return args -> {
if (userRepository.findByName(ADMIN_USERNAME).isEmpty()) {
final var admin = new User();
admin.setName(ADMIN_USERNAME);
admin.setPassword(passwordEncoder.encode(adminProperties.getPassword()));
admin.setEmail(adminProperties.getEmail());
admin.setRole(UserRole.ADMIN);
userRepository.save(admin);
}
};
}
}
Now we can run our application like so to seed our admin user:
ADMIN_PASSWORD=supersecure ADMIN_EMAIL=admin@example.com ./gradlew bootRun
This will create our admin user on the start-up of the application. We would probably only do this once. Then any subsequent runs wouldn't create the admin user as it already exists in the database.
For simplicity this example creates an initial admin account at startup using configuration properties. In production systems you would typically avoid storing default credentials in application files and instead use environment variables, secrets managers, or a dedicated user onboarding flows to create admins and other users.
Update User Creation Logic
The last step would be to update our user creation logic to create users with roles and passwords. We want to make sure only admins can create users and that users are given the USER role when created.
We can start off by adding a password field to our UserCreateRequest DTO:
@Schema(description = "Payload to create a new user")
public record UserCreateRequest(
//... Other fields
@Schema(description = "User password, should be secured over HTTPS", example = "1aB%2cD$", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Password is required")
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!]).{8,}$",
message = "Password must contain at least one digit, one lowercase, one uppercase, and one special character, and be at least 8 characters long.")
String password) {}
For now, we will always default the new user to the USER role. Eventually, we could update the UserCreateRequest to include the desired role for the new user or add a separate endpoint to change the role for a user. We can default the user role like so in our controller method:
@Operation(summary = "Create a new user", description = "Creates and returns the newly created user")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid request data")
})
@PostMapping
public UserResponse createUser(@Valid @RequestBody final UserCreateRequest user) {
final var newUser = userService.createUser(user, UserRole.USER);
log.info("Created new user with ID: {}", newUser.getId());
return userMapper.toResponse(newUser);
}
NOTE: the UserService and UserMapper will also be updated to take the new user role parameter.
So now when we call the POST - /api/users endpoint, we can pass in the password like so:
curl -i -u admin:<WHATEVER_YOUR_ADMIN_PASSWORD_IS> \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"jon","email":"jon@example.com", "age":40, "password": "1aB%2cD$"}' \
http://localhost:8080/api/users
NOTE: also when sending the password over basic auth, you should always enable HTTPS to ensure the traffic is encrypted.
And that's it, we now have some Basic Authentication working with Spring Security backed by a database and the ability to create new users with the user role!
Securing Swagger Docs with Basic Auth
The last thing to do is to update our Swagger doc to allow users with basic authentication to try out our endpoints.
To enable basic auth for swagger, we simply need to add the following OpenAPI bean configuration:
@Configuration
public class OpenApiConfig {
private static final String SECURITY_SCHEME_NAME = "basicAuth";
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(
new Components().addSecuritySchemes(
SECURITY_SCHEME_NAME, new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")
)
);
}
}
If you navigate to http://localhost:8080/docs, you will now see the following "Authorize" button, which we can select and enter our basic auth credentials. Now all the Swagger endpoints will be authorised when trying them out.

Please Read This - Real World Caveats
Before considering basic auth as your web security method, please consider the following:
- Basic Auth should be used over HTTPS (otherwise credentials can be intercepted and viewed).
- Don't ship in-memory users to production, use a real user store (DB/LDAP/IdP).
- Basic Auth is great for internal tools and learning, but for public clients you'll usually want
JWTorOAuth2/OIDC. - Password storage matters, always hash passwords (BCrypt/Argon2), never store plaintext.
- Authorisation matters as much as authentication: it's not just "who are you?" but also "what are you allowed to do?".
What's Next?
So to recap we:
- Learnt why security is important for our API/application
- What Basic Auth is and other types of common web security methods
- How to implement Basic Auth with Spring Security using in-memory and database user storage
- How to manually and automatically test Basic Auth in Spring Security
- How to securely seed our first admin user
- How to secure our Swagger docs with Basic Auth
Stay tuned for the upcoming blog in the series about handling database migration!