The Spring Security framework provides declarative security for Spring applications. In this tutorial, we secure a simple Rest API. We begin with a simple example, progress to using a custom UserDetailsService
, and finish by adding method level security.
Spring Security is simple when it works, but can be confusing when it does not. There are differences between Spring and Spring Boot. In this tutorial, we use Spring Boot 2.5 and the spring-boot-starter-parent
, spring-boot-starter-web
and the spring-boot-starter-security
packages. These come pre-packaged with many of the dependencies for developers and frees us from worrying about dependencies in this tutorial. But a word of warning, you will find many different tutorials and many different ways to accomplish the same thing. Be certain you are using the technology discussed in the tutorial and not a variant. For instance, in this tutorial, we use Spring Boot 2.5 with the Spring Boot starter jars.
- Create a new Maven application with
rest-security
as the group id and security
as the artifact id.
- Modify the pom.xml so it appears as follows. Note the addition of the spring-boot dependency and the spring boot starter dependencies (including security).
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tutorial</groupId>
<artifactId>rest-tutorial</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- Create the packages,
com.tutorial.spring.application
and com.tutorial.spring.rest
. - In the
com.tutorial.spring.rest
package, create the Hello
class as follows:
package com.tutorial.spring.rest;
public class Hello {
private String greeting;
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}
- Create the controller class,
HelloController
in the com.tutorial.spring.rest
package. - Add one method named
greeting
and define it as a Rest endpoint.
package com.tutorial.spring.rest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@RequestMapping(value = "/greeting", method = RequestMethod.GET)
public Hello greeting() {
Hello hello = new Hello();
hello.setGreeting("Hello there.");
return hello;
}
}
- Create the Spring Boot entry-point class in
com.tutorial.spring.application
package and name it TutorialApplication
.
package com.tutorial.spring.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan({ "com.tutorial.spring.rest","com.tutorial.spring.application" })
public class TutorialApplication {
public static void main(String[] args) {
SpringApplication.run(TutorialApplication.class, args);
}
}
If you are not familiar with the @SpringBootApplication
or @ComponentScan
annotations, refer to this tutorial, Spring Rest Using Spring Boot. This class is the runner for the application. For more on runners, refer to Spring Boot Runners.
- Create a class named
TutorialSecurityConfiguration
that extends WebSecurityConfigurerAdapter
(Java Doc). Note that there is no @EnableWebSecurity
(Java Doc
) annotation on TutorialSecurityConfiguration
. This annotation is not needed for Spring Boot applications, as it is automatically assumed. But if you are extrapolating this tutorial to a more traditional Spring application, caveat emptor. - Add the
configure
, userDetailsService
, and the passwordEncoder
methods:
package com.tutorial.spring.application;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.
config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class TutorialSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated()
.and().httpBasic().and().csrf().disable();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
String encodedPassword = passwordEncoder().encode("password");
manager.createUser(User.withUsername("james").password(encodedPassword)
.roles("USER").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- Start the application.
- Open Postman and create a request that makes a
GET
request. Attempt to call the endpoint and you get an Unauthorized message. Notice the status is a 401 status code which means unauthorized.
- Modify the Postman request by clicking the Authorization tab, selecting Basic Auth as the authorization type, and then adding the desired Username and Password.
- Call the endpoint and you should receive a valid response:
The TutorialSecurityConfiguration
class extends Spring’s WebSecurityConfigurerAdapter
class. This class is a base class that allows you to customize your security by overriding the configure(WebSecurity)
, configure(HttpSecurity)
, and configure(AuthenticationManagerBuilder)
methods in your own custom class.
Http Configure
In TutorialSecurityConfiguration
, you override the configuration for how Http requests are secured. First, using authorizeRequests
, we tell HttpSecurity
(Java Doc) to allow restricting requests. We then restrict the requests to those matching the ant pattern. In TutorialSecurityConfiguration
, we are telling it to restrict it to all requests starting from the root path. We could have omitted antMatchers
altogether if we wished. Next, we tell HttpSecurity
to use basic http authentication and finally to disable protection from cross-site requests (<a href="https://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html" rel="noopener">more on CSRF</a>
).
http.authorizeRequests().antMatchers("/**").authenticated()
.and().httpBasic().and().csrf().disable();
UserDetailsService
The UserDetailsService
interface loads user-specific data (Java Doc)
. The InMemoryUserDetailsManager
is a memory persistent class useful for testing and demonstration (Java Doc). It creates a map that constitutes an application’s users. By adding it as a bean, Spring security uses it to obtain the user to authenticate. When a user tries to log into the system, it searches for him or her using the user details service. That service can get users from a database, an LDAP server, a flat file, or in memory. See the API for more (implementations of UserDetailsService).
Modify One Endpoint
A Rest API where all endpoints have the same security restrictions is unrealistic. It is more probable that different endpoints are intended for different users. For instance, there might be a /greeting
endpoint for the general public, a /greeting/user
endpoint for users, and a /greeting/admin
endpoint for administrators. Spring security allows adding different security restrictions on each endpoint.
- Modify
HelloController
to have two new Rest endpoints: /greeting/user
and /greeting/admin
implemented by the greetingUser
and greetingAdmin
methods respectively.
package com.tutorial.spring.rest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@RequestMapping(value = "/greeting", method = RequestMethod.GET)
public Hello greeting() {
Hello hello = new Hello();
hello.setGreeting("Hello there.");
return hello;
}
@RequestMapping(value = "/greeting/user", method = RequestMethod.GET)
public Hello greetingUser() {
Hello hello = new Hello();
hello.setGreeting("Hello user.");
return hello;
}
@RequestMapping(value = "/greeting/admin", method = RequestMethod.GET)
public Hello greetingAdmin() {
Hello hello = new Hello();
hello.setGreeting("Hello administrator.");
return hello;
}
}
- Modify
TutorialSecurityConfig
to secure the two newly added endpoints. - Add the newly created user to the
userDetailsService
method.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/hello/greeting").permitAll()
.antMatchers("/hello/greeting/admin").hasRole("ADMIN")
.antMatchers("/hello/greeting/user").hasAnyRole("ADMIN","USER").and()
.httpBasic().and().csrf().disable();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
String encodedPassword = passwordEncoder().encode("password");
manager.createUser(User.withUsername("james").password(encodedPassword)
.roles("USER").build());
manager.createUser(User.withUsername("john").password(encodedPassword)
.roles("ADMIN").build());
return manager;
}
- Run the application. Attempt to access the admin rest endpoint with the john/password credentials and you receive the greeting.
- Now access the user endpoint with john/password as the credentials and you receive the appropriate user greeting.
- Change the credentials to james/password and attempt to access the admin endpoint and you get a 403, Forbidden, status code.
Accessing User Information
After a user logs in, there are many times you might wish to access details about that user. Spring Security offers an easy way to accomplish this through the UserDetails
interface.
The easiest way to obtain a user’s details is through the SecurityContextHolder
class. This class holds the security context, which includes the user’s details, or, to use security appropriate terminology: the principal. A principal is any entity that can be authenticated. For instance, another program could be a principal. A “user” need not be a physical person. Provided you realize user does not equal human, you can use the terms interchangeably.
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
Through the SecurityContextHolder
, you get the context, then obtain the authenticated principal, which in turn allows you to obtain the UserDetails
. The org.springframework.security.core.userdetails.UserDetails
interface is implemented by a org.springframework.security.core.userdetails.User
object, so you can cast the results to the UserDetails
interface or the User
implementation. Of course, you can create your own UserDetails
implementation if you prefer, but that is outside this post’s scope.
User user = (User)SecurityContextHolder.getContext().getAuthentication()
.getPrincipal();
- Modify
HelloController
‘s endpoints so that they append the username to the greetings. In the greetingUser
method, cast the results to a UserDetails
interface. In the greetingAdmin
method, cast the results to the User
class. (UserDetails and User JavaDocs).
package com.tutorial.spring.rest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/hello")
public class HelloController {
@RequestMapping(value = "/greeting", method = RequestMethod.GET)
public Hello greeting() {
Hello hello = new Hello();
hello.setGreeting("Hello there.");
return hello;
}
@RequestMapping(value = "/greeting/user", method = RequestMethod.GET)
public Hello greetingUser() {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
Hello hello = new Hello();
hello.setGreeting("Hello user: " + userDetails.getUsername());
return hello;
}
@RequestMapping(value = "/greeting/admin", method = RequestMethod.GET)
public Hello greetingAdmin() {
User user = (User)SecurityContextHolder.getContext().getAuthentication()
.getPrincipal();
Hello hello = new Hello();
hello.setGreeting("Hello administrator: " + user.getUsername());
return hello;
}
}
- Run the application and when you access the endpoint, you should see the username in the JSON greeting.
Create a Custom UserDetailService
Creating a fully customized UserDetailService
is outside the scope of this tutorial. Several of the Spring supplied implementations of this interface include JdbcDaoImpl
(Java Doc) and LdapUserDetailsService
(Java Doc), which provide ways to obtain user details via a Jdbc database source or an LDAP server, respectively. Here, however, we simply create a simple example for the sake of demonstration.
- Create a new class named
UserDetailsServiceImpl
implements the Spring UserDetailsService
interface. - Implement the
loadByUserByUsername
method so that it creates the user that accessed the endpoint.
package com.tutorial.spring.application;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if(username.equals("james")) {
return User.withUsername("james").password(encoder.encode("password"))
.roles("USER").build();
} else if(username.equals("john")) {
return User.withUsername("john").password(encoder.encode("password"))
.roles("ADMIN").build();
}
else throw new UsernameNotFoundException("user not found");
}
}
- Modify
TutorialSecurityConfiguration
to override the configure
method that takes an AuthenticationMangerBuilder
. Set the builder’s userDetailsService
to a newly created instance of the UserDetailsServiceImpl
class.
package com.tutorial.spring.application;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.
authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.
web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class TutorialSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/hello/greeting").permitAll()
.antMatchers("/hello/greeting/admin").hasRole("ADMIN")
.antMatchers("/hello/greeting/user").hasAnyRole("ADMIN","USER").and()
.httpBasic().and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(new UserDetailsServiceImpl());
}
}
- Build and run the application and use Postman to access the endpoints.
Method Security
Modifying the security configuration’s configure method with every additional endpoint is error prone. Moreover, you cannot add security configuration to specific methods, but only paths. Another way to add security is through global method security.
- Modify
TutorialSecurityConfiguration
by adding the @EnableGlobalSecurity
annotation.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TutorialSecurityConfiguration extends WebSecurityConfigurerAdapter {
- Create a new endpoint with a method named
greetingContractor
in the HelloController
for contractors. - Add the
@PreAuthorize
annotation.
@RequestMapping(value = "/greeting/contractor", method = RequestMethod.GET)
@PreAuthorize("hasRole('CONTRACTOR')")
public Hello greetingContractor() {
User user = (User)SecurityContextHolder.getContext().getAuthentication()
.getPrincipal();
Hello hello = new Hello();
hello.setGreeting("Hello contractor: " + user.getUsername());
return hello;
}
- Modify the
loadUserByUsername
method in UserDetailsServiceImpl
to include a contractor.
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if(username.equals("james")) {
return User.withUsername("james").password(encoder.encode("password"))
.roles("USER").build();
} else if(username.equals("john")) {
return User.withUsername("john").password(encoder.encode("password"))
.roles("ADMIN").build();
} else if(username.equals("lisa")) {
return User.withUsername("lisa").password(encoder.encode("password"))
.roles("CONTRACTOR").build();
} else throw new UsernameNotFoundException("user not found");
}
- Run the application and access the contractor endpoint with the lisa/password credentials.
- Try accessing the contractor endpoint with the james/password credentials and you receive a 403, Forbidden, response code.
- Try accessing the contractor endpoint with the john/password credentials and you also get a 403 status code.
- Modify the
greetingContractor
method in HelloController
so that it uses, hasAnyRole
and includes the ADMIN role.
@RequestMapping(value = "/greeting/contractor", method = RequestMethod.GET)
@PreAuthorize("hasAnyRole('CONTRACTOR','ADMIN')")
public Hello greetingContractor() {
- Run the application and access the contractor endpoint with the john/password credentials and you receive the contractor greeting.
In this tutorial, you created a simple Rest API secured by an in-memory map of users. It was purposely kept simple to illustrate basic Spring Security as it applies to Spring Boot 2.5 and Rest endpoints. Be advised there are many ways to do things in Spring Security. This tutorial showed one way to secure your API. For more information on Spring’s Security architecture, refer to Spring Security Architecture.