In Spring Security, we've witnessed significant changes, notably the deprecation of the trusted WebSecurityConfigurerAdapter
is in favor of a more streamlined and powerful approach to securing your web applications. This article serves as a comprehensive resource for developers seeking to grasp the latest techniques and best practices in securing their Spring-based web applications.
Here you will find all examples with code: https://github.com/BykaWF/SpringSecurityGuide
Basic Authentication
Code source:https://github.com/BykaWF/SpringSecurityGuide/tree/
When you send a request to a secure server and receive a 401 (Unauthorized)
response, it's the server's way of asking for your username and password. It checks these credentials to determine if you're authorized to access the requested resource. If your credentials are valid, you'll be granted access to the resource.
If you just add Spring Security
dependency we will get this page and generated password in the console:
If you want to add Basic Auth to your application:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* @Explanation: anyRequest() should be authenticated() and for getting pop up Basic Auth you need add httpBasic()
*
* @Note: You can't log out after you have authenticated
*/
@Bean
public SecurityFilterChain formLogin(HttpSecurity http) throws Exception{
http.
authorizeHttpRequests(authorize -> authorize
.anyRequest()
.authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
And magic happened:
ParmitAll()
If you want to add an endpoint where every single user gets access without authorization.
To show how it works we need to add HomeController(unsecured) and UserController(secured):
@RestController
public class HomeController {
@GetMapping("/")
public String homeGreeting(){
return "Hello, I'm not secured";
}
}
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/greeting")
public String userGreeting(){
return "I'm secured";
}
And a little bit edit our SecurityConfig:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* @Explanation: if your request is matching ("/") to give permit it without authentication. If request is not matched it should be authenticated by Basic Auth
* @Note: You can't log out after you have authenticated
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/").permitAll();
auth.anyRequest().authenticated();
}
)
.httpBasic(withDefaults())
.build();
}
}
Without permitAll()
we should be authenticated for http:8080/
or http:8080/api/user/greeting
:
With permitAll() we can get access to http:8080/
without permission, but http:8080/API/user/greeting
still secured:
Multiple Security Configuration
Do you want to use different types of authentication? We have two controllers: HomeController
and UserController
. Let's consider a situation where you want to have HTTP basic() auth
for HomeContorller and formLogin()
for UserContorller.
There is SecurityMatcher()
which will help us to achieve it. We will make some changes in HomeController that help to be more clear:
@RestController
@RequestMapping("api/home/")
public class HomeController {
@GetMapping("/greeting")
public String homeGreeting(){
return "Hello, I'm secured with httpBasic()";
}
}
And of course, we should make some changes in SecurityConfig
:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/user/**")
.authorizeHttpRequests(auth -> {
auth.anyRequest().authenticated();
}
)
.httpBasic(withDefaults())
.build();
}
@Bean
SecurityFilterChain homeFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/").permitAll();
auth.requestMatchers("/error").permitAll();
auth.anyRequest().authenticated();
}
)
.formLogin(withDefaults())
.build();
}
}
As we can see for UserController we want to use pop-up auth and for HomeController login form.
Users' Roles and Authorities
Code source:https://github.com/BykaWF/SpringSecurityGuide/tree/Role_Auth
Previously to get access to any resource we should use a generated password in our console and in the field username pass user. But if you want to create your User with username and password, role and so forth. You can store your User in memory or your database.
InMemoryUserDetailsManager
We can create our user InMemory by UserDetailsService
- a core interface that loads user-specific data.
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("Bogdan")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
And we need to provide PasswordEncoder
with technology BCryptPasswordEncoder.
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
Let's test it, could we get access to our resources with a new user:
Because our user with username: "Bogdan" and password: "password" exists we can reach the resource. But you have access to any other resource.
Roles and Permission (Authority)
Let's consider a basic situation when you are building a Library Management System and you have :
Seller (library stuff)
Customer
In this case, our Seller has the role of Admin
and permission to write and read a book. Our Customer has the role of User
and permission only read a book. We need APIs that are accessible only for admin
or only users
. And of course, you can assign multiple roles to your user.
Role-based Authentication
Let's add a new User with a new role in our SecurityConfig and create enums for roles and permissions.
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails bogdan = User.builder()
.username("Bogdan")
.password(passwordEncoder.encode("password"))
.roles(USER.name())
.build();
UserDetails admin = User.builder()
.username("John")
.password(passwordEncoder.encode("password"))
.roles(ADMIN.name())
.build();
return new InMemoryUserDetailsManager(bogdan,admin);
}
@RequiredArgsConstructor
@Getter
public enum Role {
ADMIN(
Set.of(
ADMIN_UPDATE,
ADMIN_DELETE,
ADMIN_CREATE,
ADMIN_READ,
USER_UPDATE,
USER_DELETE,
USER_CREATE,
USER_READ
)
),
USER(
Set.of(
USER_UPDATE,
USER_DELETE,
USER_CREATE,
USER_READ
)
),
GEUST(Collections.emptySet());
//Roles will have Set of permissions, because we don't want to have duplicates
private final Set<Permission> permissions;
}
package Guide.SecuritySpring.model;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"),
ADMIN_DELETE("admin:delete"),
USER_READ("user:read"),
USER_UPDATE("user:update"),
USER_CREATE("user:create"),
USER_DELETE("user:delete");
private final String permission;
}
We have created roles and enums. Let's add some new future to our Security Config.
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/user/**") // <--- it is not necessary
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/").permitAll();
auth.requestMatchers("/api/**").hasRole(USER.name());
auth.anyRequest().authenticated();
}
)
.httpBasic(withDefaults())
.build();
}
We have added auth.requestMatchers("/api/**").hasRole(USER.name());
that tells us to get access to our user endpoints can only who hasRole(User.name())
Permission-Based Authentication
Code source: https://github.com/BykaWF/SpringSecurityGuide/tree/Permission_Based_Authentication
For this section, I rebuilt the project, I think It will help get a better experience for testing any type of permission.
Added two models: Customer
and Address
@Entity
@Transactional
@Data
@NoArgsConstructor
@Table(name = "address")
@AllArgsConstructor
@Builder
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "add_id")
private Long addressId;
private String city;
private String addressType;
}
@Entity
@Transactional
@Data
@NoArgsConstructor
@Table(name = "customer_details")
@AllArgsConstructor
@Builder
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "csr_id")
private Long customerId;
private String username;
private String email;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "fk_add_id")
private Address address;
}
Also provided Repositories
for both entity and CustomerService
. Role
and Permission
were edited:
@RequiredArgsConstructor
@Getter
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"),
ADMIN_DELETE("admin:delete"),
USER_READ("user:read"),
MANAGER_READ("manager:read");
private final String permission;
}
To our Role
we added method getGrantedAuthorities()
. GrantedAuthority
- represents an authority granted (permissions) to an Authentication object(Our user). Here we want to map our permission to Set<SimpleGrantedAuthority>
. SimpleGrantedAuthority
is a basic concrete implementation of a GrantedAuthority.
@RequiredArgsConstructor
@Getter
public enum Role {
ADMIN(
Set.of(
ADMIN_UPDATE,
ADMIN_DELETE,
ADMIN_CREATE,
ADMIN_READ
)
),
USER(
Set.of(
USER_READ
)
),
MANAGER_TRAINEE(
Set.of(
MANAGER_READ
)
);
//Roles will have Set of permissions, because we don't want to have duplicates
private final Set<Permission> permissions;
public Set<SimpleGrantedAuthority> getGrantedAuthorities(){
Set<SimpleGrantedAuthority> permissions = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.name()))
.collect(Collectors.toSet());
permissions.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return permissions;
}
And we want to use this method in our SecurityConfig
. Let's look at our UserDetailsService.
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails customer = User.builder()
.username("Bogdan")
.password(passwordEncoder.encode("password"))
// .roles(USER.name())
.build();
UserDetails admin = User.builder()
.username("John")
.password(passwordEncoder.encode("password"))
// .roles(ADMIN.name())
.authorities(ADMIN.getGrantedAuthorities())
.build();
UserDetails manager = User.builder()
.username("Yaroslav")
.password(passwordEncoder.encode("password123"))
// .roles(MANAGER_TRAINEE.name())
.authorities(MANAGER_TRAINEE.getGrantedAuthorities())
.build();
return new InMemoryUserDetailsManager(customer, admin, manager);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
Let's comment roles()
in our user's details and use .authorities(ROLE.getGrantedAuthorities())
. Here is we implemented method that we built in our Role
that will load the permissions. If just add a debug pointer to the line where we are returning InMemoryUserDetailsManager(customer, admin, manager);
we get:
As we can see our permissions were loaded from our method and also was added ROLE_
.
In apiSecurityFilterChain
we should add a few requestMatchers for each method and set authorities for each one.
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers(GET, "management/api/**").hasAnyRole(ADMIN.name(), MANAGER_TRAINEE.name());
auth.requestMatchers(POST, "management/api/**").hasAuthority(ADMIN_CREATE.name());
auth.requestMatchers(DELETE, "management/api/**").hasAuthority(ADMIN_DELETE.name());
auth.requestMatchers(PUT, "management/api/**").hasAuthority(ADMIN_UPDATE.name());
auth.anyRequest().authenticated();
}
)
.httpBasic(withDefaults())
.build();
}
After all, let's consider our CustomerContoroller
. As I mentioned we put a "lock" for anyone except the admin to send a POST method.
@RestController
@RequestMapping("management/api/")
public class CustomerController {
private final CustomerService customerService;
@Autowired
public CustomerController(CustomerService customerService, CustomerRepository customerRepository) {
this.customerService = customerService;
}
@PostMapping("/new")
public ResponseEntity<Customer> createCustomer(@RequestBody CreateCustomerRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Authentication: " + authentication.getName());
System.out.println("Authorities: " + authentication.getAuthorities());
return ResponseEntity.ok(customerService.createCustomer(request.toCustomer()));
}
@GetMapping("/getCustomers")
public List<Customer> getAllUsers() {
return customerService.getCustomers();
}
}
Let's try to send a POST request to our server and we will start with our manager Yaroslav as we can see below he can't send a request. Because he doesn't have the authority to do it. (screenshot above with authorities all users).
-> However, our admin John can do it:
@PreAuthorize
Annotation for specifying a method access-control expression which will be evaluated to decide whether a method invocation is allowed or not.
The first thing you need to add to SecurityConfig new annotation - @EnableMethodSecurity
@Configuration
@EnableWebSecurity
@EnableMethodSecurity <-------
public class SecurityConfig {
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
// your code
}
Then go to CustomerController
and let's consider the POST request. We take 'ADMIN_CREATE'
from our Permission.
@PostMapping("/new")
@PreAuthorize("hasAuthority('ADMIN_CREATE')") <------
public ResponseEntity<Customer> createCustomer(@RequestBody CreateCustomerRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Authentication: " + authentication.getName());
System.out.println("Authorities: " + authentication.getAuthorities());
return ResponseEntity.ok(customerService.createCustomer(request.toCustomer()));
}
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"), <-------
ADMIN_DELETE("admin:delete"),
..........
}
Also, you can use roles:
@GetMapping("/getCustomers")
@PreAuthorize("hasAnyRole('ADMIN','MANAGER_TRAINEE')")<---
public List<Customer> getAllUsers() {
return customerService.getCustomers();
}
Conclusion
To wrap it up, in Spring Security, we've explored various ways to make our web applications secure. Here's a simplified summary:
Basic Authentication: When you visit a secure website, it might ask for your username and password. If they're correct, you can access the website.
permitAll(): Sometimes, we want to allow access to a part of our website without needing a username and password. Spring Security helps us do this for specific parts of our site.
Multiple Security: Imagine having different locks for different doors in your house. Spring Security allows us to set up different security rules for different parts of our website.
Users' Roles and Authorities: We can assign different roles to users. Some users might have more access (like administrators), while others have limited access (like regular users).
InMemoryUserDetailsManager: This is like having a list of users and their credentials stored in memory. Spring Security helps us manage this list.
Role-based Authentication: Think of it as giving different people different levels of access. Some have more privileges, while others have fewer.
Permission-Based Authentication: We can specify who can access specific parts of the website based on their permissions.
@PreAuthorize: It's a way to set rules for who can use certain features on the website. Think of it like a bouncer at a club checking if you're allowed to enter.
In the next parts, we will delve deeper into additional important security concepts, including:
CSRF (Cross-Site Request Forgery): We'll learn how to protect our web applications from malicious requests that can trick users into performing actions without their consent.
Form-Based Authentication: We'll explore how to create custom login forms and authenticate users using their credentials.
Database Authentication: We'll dive into storing user information and credentials in a database, allowing for more flexibility in managing users.
JSON Web Tokens (JWT): We'll discover how to implement authentication and authorization using JWTs, which are becoming increasingly popular for securing modern web applications.