Skip to main content

Spring Security

Terminology

  • Subject — any entity requesting access to an object. Logging into an application makes you the subject and the application the object. Someone knocking at your door is the subject; the house is the object.
  • Principal — a subset of subject, represented by an account, role or other unique identifier. Principals are the keys used in access control lists, and may be humans, services, applications or connections.
  • User — a subset of principal, usually a human operator. The distinction blurs because "user" and "account" are used interchangeably, but "user" is the right word when separating interactive operators from the broader class of principals.

In the API, Authentication.getPrincipal() returns the principal — a UserDetails for form login, a Jwt for a resource server. Authorities hang off the Authentication, not off the principal.

Authorities and roles

Spring Security makes no structural distinction between a role and a permission: both are GrantedAuthority strings, and authentication collects all of them into one set.

The ROLE_ prefix is the only difference, and it is a convention enforced at the check site:

ExpressionActually checks for
hasRole("ADMIN")ROLE_ADMIN
hasAuthority("ADMIN")ADMIN
hasAuthority("ROLE_ADMIN")ROLE_ADMIN

So role names may be stored without the prefix in the database, but the prefix must be added when building the GrantedAuthority:

List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());

Omitting it while calling hasRole is the classic cause of a user who has the role and is still rejected — the check looks for ROLE_ADMIN and the set contains ADMIN.

Role hierarchy

RoleHierarchy lets ROLE_ADMIN imply ROLE_USER:

@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("USER")
.build();
}

Historically this applied only to HttpSecurity rules and had no effect on method security (@Secured, @PreAuthorize, @RolesAllowed), and it did not combine with authorizeHttpRequests. Both limitations were addressed in Spring Security 6.3, where publishing a RoleHierarchy bean applies it to web and method security alike.

On older versions the hierarchy has to be wired into the expression handler manually — and on any version, verify with a test rather than assuming, because a hierarchy that is not applied fails open in the direction of denying access, which is easy to misread as a configuration problem elsewhere.

Configuration

Since Spring Security 6, WebSecurityConfigurerAdapter is gone. Configuration is a SecurityFilterChain bean:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
}

Three things that trip people up in this block:

  • httpBasic is configured on http, not inside the authorizeHttpRequests lambda. Chaining it off the authorisation registry does not compile.
  • Rules are evaluated in declaration order, first match wins. anyRequest() must come last; anything after it is unreachable.
  • authorizeHttpRequests and requestMatchers replace the Spring Security 5 authorizeRequests and antMatchers, which are removed in 6.

Lambda DSL

Introduced in Spring Security 5.2, the only supported style from 6.1 onward — the .and()-chained form is deprecated and removed in 7.

Current:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/blog/**").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.loginPage("/login")
.permitAll())
.rememberMe(Customizer.withDefaults())
.build();
}

Removed style, shown only for recognising it in older code:

// Does not compile on Spring Security 6+
http
.authorizeRequests()
.antMatchers("/blog/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.rememberMe();

Customizer.withDefaults() enables a feature with its default settings. It is shorthand for the empty lambda it -> {}.

The lambda parameter must be used inside the lambda body — authorize -> authorize.anyRequest().authenticated(). A body that starts with .anyRequest() has no receiver and will not compile.

Form login

Form login only accepts credentials as application/x-www-form-urlencoded (username and password fields) by default. Posting JSON to /login returns a 401 with no useful explanation — this catches out single-page applications almost every time. Either post form-encoded data or write a custom authentication filter.

Logout is handled by LogoutFilter, which intercepts /logout, delegates to the registered LogoutHandlers and then to LogoutSuccessHandler. With CSRF protection on, logout must be a POST.

HTTP Basic

Credentials are base64-encoded on every request:

  1. Concatenate as u5er:passw0rd.
  2. Base64-encode: dTVlcjpwYXNzdzByZA==.
  3. Send as Authorization: Basic dTVlcjpwYXNzdzByZA==.
  4. BasicAuthenticationFilter extracts and decodes the header.
  5. The resource is served if the credentials verify.

Base64 is an encoding, not encryption — it is trivially reversible. HTTP Basic is only acceptable over TLS, and even then the credentials travel on every single request, so any logging proxy or crash dump captures them. Prefer a token-based scheme for anything long-lived; see JWT.

Dependency

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}

Default password

With the starter on the classpath and no user configured, Spring Boot generates one at startup and logs it:

Using generated security password: 0cc59a43-c2e7-4c21-a38c-0df8d1a6d624

It changes on every restart and exists purely so the application is not open by default. Its presence in the log of a deployed service means no real authentication is configured.

Configured credentials

spring.security.user.name=u5er
spring.security.user.password=passw0rd
spring.security.user.roles=ADMIN

This is for development only — the password sits in plain text in a file that is usually committed. Use a UserDetailsService backed by a datastore, with BCryptPasswordEncoder or better, for anything real.