Spring Security
Spring Security
Defining Terminology
Before diving into the technical details, I want to explicitly define the
terminology used in the Spring Security context just to be sure that we all
speak the same language.
</dependency>
</dependencies>
Once we have created the project, we can add a simple REST controller to
it as follows:
@RestController @RequestMapping("hello")
@GetMapping("user")
@GetMapping("admin")
After this, if we build and run the project, we can access the following
URLs in the web browser:
User .
</dependency>
</dependencies>
Please remember that the password changes each time we rerun the
application. If we want to change this behavior and make the password
static, we can add the following configuration to
our application.properties file:
spring.security.user.password = Test12345_
The following diagram presents the flow and shows how authentication
requests are processed:
Now, let’s break down this diagram into components and discuss each of
them separately.
Spring Security Filters Chain
For example:
One important detail I want to mention is that Spring Security filters are
registered with the lowest order and are the first filters invoked. For some
use cases, if you want to put your custom filter in front of them, you will
need to add padding to their order. This can be done with the following
configuration:
spring.security.filter.order = 10
AuthenticationManager
You can think of AuthenticationManager as a coordinator where you can
register multiple providers, and based on the request type, it will deliver
an authentication request to the correct provider.
AuthenticationProvider
authentication type.
One important implementation of the interface that we are using in our
sample project is DaoAuthenticationProvider , which retrieves user details
from a UserDetailsService .
UserDetailsService
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder
@Override
protected void configure(HttpSecurity http) throws
Exception {
}
In our sample application, we store user identities in a MongoDB
database, in the users collection. These identities are mapped by
the User entity, and their CRUD operations are defined by
the UserRepo Spring Data repository.
Now, when we accept the authentication request, we need to retrieve the
correct identity from the database using the provided credentials and
then verify it. For this, we need the implementation of
the UserDetailsService interface, which is defined as follows:
throws UsernameNotFoundException;
Here, we can see that it is required to return the object that implements
the UserDetails interface, and our User entity implements it (for
implementation details, please see the sample project’s repository).
Considering the fact that it exposes only the single-function prototype, we
can treat it as a functional interface and provide implementation as a
lambda expression.
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder
.findByUsername(username)
.orElseThrow(
));
WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder
.findByUsername(username)
.orElseThrow(
));
@Bean
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws
Exception {
http = http.cors().and().csrf().disable();
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http = http
.exceptionHandling()
.authenticationEntryPoint(
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
.and();
http.authorizeRequests()
.antMatchers(HttpMethod.POST,
"/api/author/search" ).permitAll()
.antMatchers(HttpMethod.POST,
"/api/book/search" ).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
UrlBasedCorsConfigurationSource source =
CorsConfiguration ();
config.setAllowCredentials( true );
config.addAllowedOrigin( "*" );
config.addAllowedHeader( "*" );
config.addAllowedMethod( "*" );
Please note that we added the JwtTokenFilter before the Spring Security
internal UsernamePasswordAuthenticationFilter . We’re doing this because
we need access to the user identity at this point to perform
authentication/authorization, and its extraction happens inside the JWT
token filter based on the provided JWT token. This is implemented as
follows:
@Component
public class JwtTokenFilter extends
OncePerRequestFilter {
UserRepo userRepo) {
@Override
protected void doFilterInternal(HttpServletRequest
request,
HttpServletResponse response,
FilterChain chain)
request.getHeader(HttpHeaders.AUTHORIZATION);
chain.doFilter(request, response);
return ;
[ 1 ].trim();
if (!jwtTokenUtil.validate(token)) {
chain.doFilter(request, response);
return ;
.findByUsername(jwtTokenUtil.getUsername(token))
.orElse( null );
UsernamePasswordAuthenticationToken
authentication = new
UsernamePasswordAuthenticationToken (
userDetails, null ,
userDetails == null ?
List.of() : userDetails.getAuthorities()
);
authentication.setDetails(
new
WebAuthenticationDetailsSource ().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
Before implementing our login API function, we need to take care of one
more step - we need access to the authentication manager. By default, it’s
not publicly accessible, and we need to explicitly expose it as a bean in
our configuration class.
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Override @Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
@Api(tags = "Authentication")
@RestController @RequestMapping(path = "api/public")
public AuthApi(AuthenticationManager
authenticationManager,
JwtTokenUtil jwtTokenUtil,
UserViewMapper userViewMapper) {
}
@PostMapping("login")
AuthRequest request) {
try {
Authentication authenticate =
authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken (
request.getUsername(), request.getPassword()
);
return ResponseEntity.ok()
.header(
HttpHeaders.AUTHORIZATION,
jwtTokenUtil.generateAccessToken(user)
.body(userViewMapper.toUserView(user));
ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
The Spring Security framework provides us with two options to set up the
authorization schema:
URL-based configuration
Annotation-based configuration
First, let’s see how URL-based configuration works. It can be applied to the
web security configuration as follows:
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws
Exception {
http = http.cors().and().csrf().disable();
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http = http
.exceptionHandling()
.authenticationEntryPoint(
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
.and();
http.authorizeRequests()
.antMatchers(HttpMethod.POST,
"/api/author/search" ).permitAll()
.antMatchers(HttpMethod.GET, "/api/book/**" ).permitAll()
.antMatchers(HttpMethod.POST,
"/api/book/search" ).permitAll()
.antMatchers
( "/api/admin/user/**" ).hasRole(Role.USER_ADMIN)
.anyRequest().authenticated();
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
As you can see, this approach is simple and straightforward, but it has one
downside. The authorization schema in our application can be complex,
and if we define all the rules in a single place, it will become very big,
complex, and hard to read. Because of this, I usually prefer to use
annotation-based configuration.
The Spring Security framework defines the following annotations for web
security:
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
prePostEnabled =
notations.
After enabling them, we can enforce role-based access policies on our API
endpoints like this:
@Api(tags = "UserAdmin")
@RestController @RequestMapping(path = "api/admin/user")
@RolesAllowed(Role.USER_ADMIN)
@Api(tags = "Author")
@RestController @RequestMapping(path = "api/author")
@RolesAllowed(Role.AUTHOR_ADMIN)
@PostMapping
public void create() { }
@RolesAllowed(Role.AUTHOR_ADMIN)
@PutMapping("{id}")
@RolesAllowed(Role.AUTHOR_ADMIN)
@DeleteMapping("{id}")
@GetMapping("{id}")
@GetMapping("{id}/book")
@PostMapping("search")
@Api(tags = "Book")
@RestController @RequestMapping(path = "api/book")
@RolesAllowed(Role.BOOK_ADMIN)
@PostMapping
@RolesAllowed(Role.BOOK_ADMIN)
@PutMapping("{id}")
@RolesAllowed(Role.BOOK_ADMIN)
@DeleteMapping("{id}")
@GetMapping("{id}")
@GetMapping("{id}/author")
@PostMapping("search")
Please note that security annotations can be provided both on the class
level and the method level.
Role : @PreAuthorize(“hasRole(‘BOOK_ADMIN’)”)
To make the difference between these two terms more explicit, the Spring
Security framework adds a ROLE_ prefix to the role name by default. So,
instead of checking for a role named BOOK_ADMIN , it will check
for ROLE_BOOK_ADMIN .
Personally, I find this behavior confusing and prefer to disable it in my
applications. It can be disabled inside the Spring Security configuration as
follows:
@EnableWebSecurity
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
}
Testing Our Spring Security JWT Solution
To test our endpoints with unit or integration tests when using the Spring
Security framework, we need to add spring-security-test dependency
along with the spring-boot-starter-test . Our pom.xml build file will look
like this:
<dependency>
<exclusions>
<exclusion>
</exclusion>
</exclusions>
</dependency>
<dependency>
</dependency>
This approach has a couple of drawbacks, though. First, the mock user
doesn’t exist, and if you run the integration test, which later queries the
user information from the database, the test will fail. Second, the mock
user is the instance of
the org.springframework.security.core.userdetails.User class, which is
the Spring framework’s internal implementation of
the UserDetails interface, and if we have our own implementation, this
can cause conflicts later, during test execution.
If previous drawbacks are blockers for our application, then
the @WithUserDetails annotation is the way to go. It is used when we
have custom UserDetails and UserDetailsService implementations. It
assumes that the user exists, so we have to either create the actual row in
the database or provide the UserDetailsService mock instance before
running tests.
This is how we can use this annotation:
@Test @WithUserDetails("customUsername@example.io")
@Test
public void test1() {
@Test
@Test @WithAnonymousUser
In the end, I would like to mention that the Spring Security framework
probably won’t win any beauty contest and it definitely has a steep
learning curve. I have encountered many situations where it was replaced
with some homegrown solution due to its initial configuration complexity.
But once developers understand its internals and manage to set up the
initial configuration, it becomes relatively straightforward to use.
In this Spring Security tutorial, I tried to demonstrate all the subtle details
of the configuration, and I hope you will find the examples useful