Skip to content
thesarfo

Concept

Spring Academy: Spring Security Fundamentals

Authentication vs authorization, how Spring Security's default filter works, in-memory and JDBC-backed authentication, and role-based authorization with SecurityFilterChain.

views 0

Spring Security is a powerful framework for implementing robust security features in Spring-based applications — tools and mechanisms to address common security challenges.

  1. Authentication — proving a user’s identity to the system, typically via credentials like a username and password. Spring Security handles this by creating authentication sessions using session tokens stored in cookies.
  2. Authorization — happens after authentication, and determines the permissions granted to authenticated users. Spring Security implements role-based access control (RBAC): users are assigned roles that define their permissions, giving granular control over who can do what.
  3. Security policies
    • Same-Origin Policy (SOP) — scripts on a page can only make requests to the same origin as the page itself, preventing unauthorized access to sensitive resources.
    • Cross-Origin Resource Sharing (CORS) — lets servers explicitly define which origins may access resources, relaxing SOP for legitimate cross-origin requests.
  4. Common web exploits
    • CSRF — exploits a user’s authenticated session to perform unauthorized actions. Spring Security has built-in CSRF protection via CSRF tokens.
    • XSS — injects malicious scripts into pages, letting attackers run arbitrary code on the client or server. Proper data validation and escaping mitigate it.
  5. Principal — the person identified through authentication; the currently logged-in user.

How does authorization happen?

The app needs to decide upfront whether to allow a particular access — permissions need to be defined for each user before they access the application. This is called a Granted Authority. A user attempting to do something is only allowed to if they’ve been granted that authority.

  • Role — a group of authorities given to a user.

Implementing Spring Security

Add the spring-boot-starter-security dependency to your pom.xml/build.gradle. As soon as it’s added, Spring Security starts taking effect — you’ll be prompted to sign in on any endpoint.

How does adding a dependency automatically demand sign-in on every endpoint? Via filters — a core concept in servlet technologies. Servlets implement the functionality of your app (a request is handled by a servlet); a filter stands between the user and the servlet, intercepting every request and giving you a chance to act on it. Servlets map to a specific URL, but filters map to all URLs. Spring Security places a filter between the user and the application and uses it to allow or deny requests. By default, just from the dependency, it:

  1. Adds mandatory authentication for URLs
  2. Adds a login form (/login route)
  3. Handles login errors
  4. Creates a user and sets a default password

To find the generated default password, check the console logs on startup:

Terminal window
Using generated security password: 664d498c-d86b-451f-8b34-c1cc44f1071b

The username is user. Providing these credentials in the browser gets you access to protected endpoints.

You can customize the default credentials in application.properties:

spring.security.user.name=example_username
spring.security.user.password=example_password

Configuring authentication

To check against a list of users and log them in with the right credentials, we configure authentication for Spring Security — creating some users in memory to authenticate against.

Configuring authentication means affecting the AuthenticationManager — it sits inside a Spring Security app and does one thing: authenticate (via .authenticate(), throwing an exception if it can’t). We don’t build our own AuthenticationManager — we configure what it does via the builder pattern, using AuthenticationManagerBuilder:

  1. Get hold of the AuthenticationManagerBuilder.
  2. Set configuration on it.

Every interaction happens through that configuration — e.g.:

  1. Builder: “What type of auth do you need?”
  2. You: “In-memory auth, please.”
  3. Builder: “Tell me the username, password, and role.”
  4. You: pass those details to the builder.

In a Spring Security app, define a userDetailsService() method inside a SecurityConfiguration class to configure in-memory authentication. If you don’t override it, Spring Security uses default details. Inside, create user details with User.withDefaultPasswordEncoder(), then initialize an InMemoryUserDetailsManager with them, and return it as a bean:

@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("thesarfo")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

Using a plain-text password here, but it’s essential to hash passwords — normally via a bean:

@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withUsername("thesarfo")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

Configuring authorization

All APIs need authentication, but different parts of the API often need different access requirements:

APIRoles allowed to access it
/All (authenticated)
/userUSER and ADMIN roles
/adminADMIN role

For authentication we used AuthenticationManagerBuilder (now via InMemoryUserDetailsManager, since the former is deprecated); for authorization we’d have used HttpSecurity, but that’s deprecated too, so we use a SecurityFilterChain instead. The example below assumes two users — one USER, one ADMIN:

@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("thesarfo")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("adminpassword")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers("/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}

Spring Security automatically creates a login endpoint, and also a logout endpoint, so you can sign out and sign back in as a different user. Different access levels per endpoint:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").hasAnyRole("USER", "ADMIN") // both admin and users can access the /user endpoint
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}

JDBC authentication

First add the database and JDBC API dependencies — here, H2 and the JDBC API, alongside Spring Security.

Specify a datasource, then configure it with the AuthenticationManagerBuilder. Since we’re using JDBC with H2, configure jdbcAuthentication() and point it at the datasource Spring Security should query for user credentials. Then use a SecurityFilterChain to authorize requests against the users/roles in the database:

@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema()
.withUser(User.withUsername("thesarfo")
.password("password")
.roles("USER")
)
.withUser(User.withUsername("admin")
.password("adminpassword")
.roles("ADMIN")
);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").hasAnyRole("USER", "ADMIN")
.requestMatchers("/").permitAll()
.anyRequest().authenticated())
.formLogin(withDefaults());
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}

Our datasource points at H2, the project default — with a Postgres datasource, Spring Security generates the equivalent Postgres configuration. On startup, Spring Security populates the database with the users we defined, using a default schema — .withDefaultSchema() tells it to create that schema for us.

This is obviously not practical for production, where we don’t want to populate a default schema or hardcode user details — we assume those already exist in the database. The ideal configuration is closer to:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}

This assumes a database, schema, and users already exist — we’re just connecting Spring Security to them. That means something else needs to define the user schema and creation process.

Spring Security publishes its default user schema — copy it into a schema.sql file (Spring Boot’s default mechanism for populating the DB with a schema on startup):

-- schema.sql
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

If you edit this default schema, you become responsible for telling Spring Security how to work with the edited version.

Next, create the users following that schema, via a data.sql file:

-- data.sql
insert into users (username, password, enabled)
values ('thesarfo', 'password', true);
insert into users (username, password, enabled)
values ('admin', 'adminpassword', true);
insert into authorities (username, authority)
values ('thesarfo', 'ROLE_USER');
insert into authorities (username, authority)
values ('admin', 'ROLE_ADMIN');

Now we have a schema and users populated into the database at runtime, and we can access endpoints and be authorized/unauthorized based on role — our schema and users are ours, and we just point Spring Security at the database.

If your users/authorities tables have different (custom) names, tell Spring Security via .usersByUsernameQuery() and .authoritiesByUsernameQuery():

// assuming the user table and authorities table are called my_user and my_authorities
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, enabled "
+ "from my_users "
+ "where username = ?")
.authoritiesByUsernameQuery("select username, authority "
+ "from my_authorities "
+ "where username = ?");
}

Connecting to a real database

For Postgres/Oracle instead of H2/in-memory, set these in application.properties:

spring.datasource.url=
spring.datasource.username=
spring.datasource.password=

JPA authentication with MySQL

For a JPA + MySQL project, you’ll need: Spring Web, Spring Security, Spring JPA, and the MySQL driver. See this article or this repo for a full implementation.