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:
| Expression | Actually 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:
httpBasicis configured onhttp, not inside theauthorizeHttpRequestslambda. 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. authorizeHttpRequestsandrequestMatchersreplace the Spring Security 5authorizeRequestsandantMatchers, 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:
- Concatenate as
u5er:passw0rd. - Base64-encode:
dTVlcjpwYXNzdzByZA==. - Send as
Authorization: Basic dTVlcjpwYXNzdzByZA==. BasicAuthenticationFilterextracts and decodes the header.- 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.
Related
- OAuth 2.0 — delegated authorisation concepts and grant types.
- OAuth 2.0 Authorization Server — running one with Spring.
- JWT — token format and validation.