Spring Security is a powerful framework for implementing robust security features in Spring-based applications — tools and mechanisms to address common security challenges.
- 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.
- 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.
- 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.
- 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.
- 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:
- Adds mandatory authentication for URLs
- Adds a login form (
/loginroute) - Handles login errors
- Creates a user and sets a default password
To find the generated default password, check the console logs on startup:
Using generated security password: 664d498c-d86b-451f-8b34-c1cc44f1071bThe 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_usernamespring.security.user.password=example_passwordConfiguring 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:
- Get hold of the
AuthenticationManagerBuilder. - Set configuration on it.
Every interaction happens through that configuration — e.g.:
- Builder: “What type of auth do you need?”
- You: “In-memory auth, please.”
- Builder: “Tell me the username, password, and role.”
- 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:
@EnableWebSecuritypublic 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:
@EnableWebSecuritypublic 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:
| API | Roles allowed to access it |
|---|---|
/ | All (authenticated) |
/user | USER and ADMIN roles |
/admin | ADMIN 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:
@EnableWebSecuritypublic 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:
@EnableWebSecuritypublic 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.sqlcreate 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.sqlinsert 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.