How to Secure REST API using Spring Security and OAuth2 - part 4

268 阅读10分钟
原文链接: sivatechlab.com

This blog post is part of a multi-part series:

Part 1 – Fundamentals of OAuth2, its roles, and Grant types
Part 2 – Setting up a starter Project with REST API endpoints
Part 3 – Adding Spring Security and OAuth2 to protect REST API endpoints
Part 4 – Authenticating user against the credentials stored in the database (this post)
Part 5 – Persisting Client registration and auth tokens in the database

In this post we are going to learn:

  • Persisting user credentials in the database
  • Configuring Spring Security to access the users and roles from the database
  • Defining Method Level Security
  • Obtaining information about the current user
  • Installing H2 database

Persisting user credentials in the database:

We are going to provide an endpoint for the users to sign up for our application so that registered users can access our API endpoints. Let’s start with creating a domain object.

Create a class called User under the package com.stl.crm.domain and add the following code into it.

package com.stl.crm.domain;
 
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
 
@Entity 
@Table(name = "USERS")
public class User {
	@Id 
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id; 
	
	private String username;
	
	private String password;
	
	@OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL) 
	private List<UserRole> roles; 
 
	User() { 
	} 
 
	public User(String username, String password, List<UserRole> roles) { 
		this.username = username; 
		this.password = password; 
		this.roles = roles; 
	} 
 
	public String getUsername() { 
		return username; 
	} 
 
	public void setUsername(String username) { 
		this.username = username; 
	} 
 
	public String getPassword() { 
		return password; 
	} 
 
	public void setPassword(String password) { 
		this.password = password; 
	} 
 
	public List<UserRole> getRoles() { 
		return roles; 
	} 
 
	public void setRoles(List<UserRole> roles) { 
		this.roles = roles; 
	} 
 
}

As you can see, the User entity class has just 3 attributes: username, password, and roles. But it could contain lot other attributes like first name, last name, email, etc. Each user can have multiple roles that are defined as a one-to-many relationship between the User and UserRole classes.

Let’s create another class called UserRole under the package com.stl.crm.domain and add the following code into it.

package com.stl.crm.domain;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
@Entity
@Table(name="USER_ROLES")
public class UserRole {
	@Id 
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
	
	private String name;	
 
	UserRole() { 
	} 
 
	public UserRole(String name) { 
		this.name = name; 
	} 
 
	public String getName() { 
		return name; 
	} 
 
	public void setName(String name) { 
		this.name = name; 
	} 
}

 

Next, create a JPA repository interface.

Create an interface called UserRepository under the package com.stl.crm.repository and add the following code into it.

package com.stl.crm.repository;
 
import org.springframework.data.repository.CrudRepository;
 
import com.stl.crm.domain.User;
 
public interface UserRepository extends CrudRepository<User, Long> {
	
	User findByUsername(String username);
	
}

In the preceding class, the CrudRepository interface is linked to the domain object User and the type of primary key Long. We provide a custom finder method named findByUsername. Spring Data JPA would provide a runtime implementation so that the findByUsername method retrieves a user based on the given username. Let’s wire this repository into the service class.

Create a class called SignupService under the package com.stl.crm.service and add the following code into it.

package com.stl.crm.service;
 
import java.util.Arrays;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import com.stl.crm.domain.User;
import com.stl.crm.domain.UserRole;
import com.stl.crm.repository.UserRepository;
 
@Service
@Transactional
public class SignupService {
	
	@Autowired
	private UserRepository userRepository;
 
	@Autowired
	PasswordEncoder passwordEncoder;
	
	public User addUser(User user) {
		user.setPassword(passwordEncoder.encode(user.getPassword()));
		return userRepository.save(user);
	}
	
	/**
	 * 
	 * set up a default user with two roles USER and ADMIN
	 * 
	 */
	@PostConstruct
	private void setupDefaultUser() {
		//-- just to make sure there is an ADMIN user exist in the database for testing purpose
		if (userRepository.count() == 0) {
			userRepository.save(new User("crmadmin", 
									passwordEncoder.encode("adminpass"), 
									Arrays.asList(new UserRole("USER"), new UserRole("ADMIN"))));
		}		
	}
	
	
}

We should never store passwords in plain text. Spring Security supports BCryptPasswordEncoder which uses the BCrypt encode algorithm. PasswordEncoder is an interface which is implemented by BCryptPasswordEncoder. As you can see in the preceding class, we are auto wiring PasswordEncoder and using that to encrypt the password. We will see in a bit how this BCryptPasswordEncoder bean is created.

Next, let’s provide an endpoint so that the user can signup or register themselves in our application.

Create a class called SignupController under the package com.stl.crm.controller and add the following code into it.

package com.stl.crm.controller;
 
import java.util.Arrays;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import com.stl.crm.domain.User;
import com.stl.crm.domain.UserRole;
import com.stl.crm.service.SignupService;
 
@RestController
public class SignupController {
	
	@Autowired
	private SignupService signupService;
 
    /**
     * 
     * user signup
     * @param user
     * @return
     */
    @RequestMapping(value = "/signup", method = RequestMethod.POST)
    public ResponseEntity<?> signup(@RequestBody User user) {
   		user.setRoles(Arrays.asList(new UserRole("USER")));
    	User newUser = signupService.addUser(user);
    	return new ResponseEntity<>(HttpStatus.CREATED);
    }
    
}

So far, we have created an endpoint(/signup), a service class, a repository interface, and domain classes. If any of the code is not familiar to you, take a look at this blog post.

Now that we have set up the infrastructure needed for creating our application users, let’s continue with Configuring Spring Security.

Configuring Spring Security to access users and roles:

We need to tell Spring Security where and how users and roles are persisted so it can access this data on itself and process authentication. In order to perform an authentication, Spring Security needs to compare the submitted user credentials with an application user stored in the database. Spring Security uses org.springframework.security.core.userdetails.UserDetailsService to retrieve user information from the backend database. We need to provide an implementation for the UserDetailsService interface.

Create a class called CrmUserDetailsService under the package com.stl.crm.security and add the following code into it.

package com.stl.crm.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
import com.stl.crm.domain.User;
import com.stl.crm.repository.UserRepository;
 
@Service
public class CrmUserDetailsService implements UserDetailsService {
	
	@Autowired
	private UserRepository userRepository;
	
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(userName);
        if(user == null){
            throw new UsernameNotFoundException("UserName "+userName+" not found");
        }
        return new CrmUserDetails(user);
    }	
 
}

The CrmUserDetailsService class makes use of UserRepository to retrieve User information from the database.

The Spring Security infrastructure expects the loadUserByUsername method to return an instance of type UserDetails. Think of UserDetails as the adapter between your own database and what Spring Security needs inside the SecurityContextHolder.

UserDetails is an interface in Spring Security which provides core user information. Let’s provide an implementation to this interface.
Create a class called CrmUserDetails class under the package com.stl.crm.security and add the following code into it.

package com.stl.crm.security;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
import com.stl.crm.domain.User;
import com.stl.crm.domain.UserRole;
 
public class CrmUserDetails implements UserDetails {
 
	private static final long serialVersionUID = 1L; 
	private Collection<? extends GrantedAuthority> authorities; 
	private String password; 
	private String username; 
 
	public CrmUserDetails(User user) { 
		this.username = user.getUsername(); 
		this.password = user.getPassword(); 
		this.authorities = translate(user.getRoles()); 
	} 
 
	private Collection<? extends GrantedAuthority> translate(List<UserRole> roles) { 
		List<GrantedAuthority> authorities = new ArrayList<>(); 
		for (UserRole role : roles) { 
			String name = role.getName().toUpperCase(); 
			if (!name.startsWith("ROLE_")) { 
				name = "ROLE_" + name; 
			} 
			authorities.add(new SimpleGrantedAuthority(name)); 
		} 
		return authorities; 
	} 
 
	@Override 
	public Collection<? extends GrantedAuthority> getAuthorities() { 
		return authorities; 
	} 
 
	@Override 
	public String getPassword() { 
		return password; 
	} 
 
	@Override 
	public String getUsername() { 
		return username; 
	} 
 
	@Override 
	public boolean isAccountNonExpired() { 
		return true; 
	} 
 
	@Override 
	public boolean isAccountNonLocked() { 
		return true; 
	} 
 
	@Override 
	public boolean isCredentialsNonExpired() { 
		return true; 
	} 
 
	@Override 
	public boolean isEnabled() { 
		return true; 
	} 
	
}

CrmUserDetails creates a set of GrantedAuthority instances that represent roles that the user has in the system. In Spring Security, GrantedAuthority reflects the permissions granted to the user. SimpleGrantedAuthority is an implementation of GrantedAuthority interface provided by Spring Framework.

We assign the roles as strings like ADMIN, USER, GUEST, etc. Spring Security identifies the roles when the string has a prefix of ‘ROLE_’. We handle these in the translate method.

Once Spring Security successfully verified the submitted credentials with the application user stored in the database,  it creates an Authentication object and then puts it into the SecurityContextHolder.

Customizing Spring Security’s behavior:

Open the SecurityConfig class under the com.stl.crm.security package.
Comment out the globalUserDetails method and add the code as follows. Make sure to import the CrmUserDetailsService class.

	@Autowired
	private CrmUserDetailsService crmUserDetailsService;	
	
    @Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.userDetailsService(crmUserDetailsService)
    		.passwordEncoder(passwordEncoder());
	}
    
	/*
	@Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("crmadmin").password("crmpass").roles("ADMIN","USER").and()
        .withUser("crmuser").password("pass123").roles("USER");
    }
	*/

In the SecurityConfig class, we have added the crmUserDetailsService property, which is annotated with @Autowired. Spring framework takes responsibility of populating this field at runtime.

We override the configure method that takes an AuthenticationManagerBuilder as a parameter. In our method implementation, we use the AuthenticationManagerBuilder to add the CrmUserDetailsService instance. This is how we tell Spring Security where and how to retrieve the application user from the database.

Remember, we encrypted the passwords when saving the user credentials in the database using BCrypt algorithm. We provided an instance of BCryptPasswordEncoder to AuthenticationManagerBuilder, so the Spring Security will use the password encoder to compare the plain string provided by the user with the encrypted hash stored in the database.

Add the following code towards the end of the SecurityConfig class, which creates a BCryptPasswordEncoder bean.

	@Bean
	public PasswordEncoder passwordEncoder() {
	    return new BCryptPasswordEncoder();
	}	

 

We need to update the AuthorizationServerConfig class as well.
Open the AuthorizationServerConfig class under the com.stl.crm.security package.

Declare the crmUserDetailsService attribute as follows.
Add the following code after the authenticationManager attribute in the AuthorizationServerConfig class.

	@Autowired
	private CrmUserDetailsService crmUserDetailsService;		

Replace the configure(AuthorizationServerEndpointsConfigurer endpoints) method with the following code.

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
		.authenticationManager(authenticationManager)
		.userDetailsService(crmUserDetailsService);
	}

 

That’s all with the coding! Save all the changes, make a build, and deploy to the Tomcat Server.

Signing up:

In the Postman app, enter the following HTTP POST URL:
localhost:8080/crm-oauth2/signup

Add the following JSON data as the body.

{
	"username": "testUser",
	"password": "testPass"
}

Your screen should match with the following screen. Click on the Send button and verify the response.

You should get the response similar to the following.

We have successfully saved the user in the database. We will look into the database directly to see how the user data is stored, particularly how the password is stored in the database later in this post.

Getting the Auth token using the newly created user credentials:

Let’s get back to the Postman app and enter the following URL, which is the OAuth2 token endpoint.
http://localhost:8080/crm-oauth2/oauth/token

Select the HTTP method as POST.

Change the Authorization Type to ‘Basic Auth’ and enter the following username & password.
Username: crmClient1
Password: crmSuperSecret

Select the Body tab and click on the x-www-form-urlencoded radio button. Enter the following key-value pairs:
username: testUser
password: testPass
grant_type: password

Click the Send button. You should get the response similar to the following:

Now you can test all the endpoints (add, get, update, and delete) using the new access_token. Also, try using the Refresh token to get a new access token. See the previous articles for more info.

Testing with the wrong password:

Let’s see what happens when we try to get the token using the wrong password. In our example earlier, we saved the username/password as testUser/testPass. If we enter a wrong password, we get the ‘Bad Credentials’ error like you see it below.

Defining Method Level Security:

When the user signs up, we assign the role as ‘USER’, as you can see in the SignupController class.

The signed up user can now add, get, update, and delete Customers. Let’s come up with the security requirement that only allows users with ‘ADMIN’ role to delete a Customer. How do you restrict the deleteCustomer method from the CustomerController class for the user with a ‘USER’ role?

To implement this requirement, we will apply Spring Security’s method level security on the deleteCustomer method. Spring’s method level security can be enabled using EnableGlobalMethodSecurity annotation.

Create a class called MethodSecurityConfig class under the package com.stl.crm.security and add the following code into it.

package com.stl.crm.security;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
 
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

 

Open the CustomerController class under the com.stl.crm.controller package.

Add the following PreAuthorize annotation to the above deleteCustomer method.
@PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”)

After adding the PreAuthorize annotation, the deleteCustomer method code looks as follows.

    @RequestMapping(value = "/customers/{customerId}", method = RequestMethod.DELETE)
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public ResponseEntity<?> deleteCustomer(@PathVariable long customerId) {
    	Customer customer = customerService.getCustomer(customerId);
    	customerService.deleteCustomer(customer);
    	return new ResponseEntity<>(HttpStatus.OK);
    }

Adding the PreAuthorize annotation to a method would then limit the access to that method accordingly.

Save all changes, make a build, and deploy to the Tomcat Server.

Delete a Customer:

Let’s try to delete a Customer. To do that, we need a customer Id. Let’s run through the following steps:

#1) Get the Auth token using the user credentials testUser/testPass or any other newly created user credentials
http://localhost:8080/crm-oauth2/oauth/token

#2) Get the Customers
http://localhost:8080/crm-oauth2/api/customers

#3) Delete the customer by using the customer id
http://localhost:8080/crm-oauth2/api/customers/<id>

Did you get a response similar to the following?

This proves that the regular user now can’t delete a Customer. Only the user with the ‘ADMIN’ role should able to delete a Customer. Let’s try to delete the Customer by using the Admin user.

Get the Auth token by using the user credentials crmadmin/adminpass, which is the default ADMIN user in our sample application that is defined in the SignupService class. Delete the Customer by using the new token with the admin user credentials. You should get a successful delete message as follows.

Obtaining information about the current user:

Let’s assume that we have a lot of users signed up and they are doing a lot of operations using our API and we would like to know which user is doing what – for the purpose of logging or auditing.

As we know, signed up users log in once to get the token, which they use to access the API endpoints. So how do we get the username? Let’s find out.

Open the CustomerController class from com.stl.crm.controller package and update the getCustomers() method as follows.

	@RequestMapping(value="/customers", method = RequestMethod.GET)
	public ResponseEntity<?> getCustomers() {
 
		/**
		 * Obtaining information about the current user
		 */
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        CrmUserDetails principal = (CrmUserDetails) authentication.getPrincipal();
        System.out.println("logged in user name:: " + principal.getUsername());
		
		Iterable<Customer> customerList = customerService.getCustomers();
		return new ResponseEntity<>(customerList, HttpStatus.OK);
	}

In the preceding code, we added lines #7 to #9 to the existing method. Make sure CrmUserDetails is imported from the com.stl.crm.security package. Save it, make a build, and deploy it to the Tomcat Server. Get the new token and run the Get Customers. Check the Tomcat server console, where you should see the “logged in user name” printed with the user who accessed the /customers endpoint.

Next, let’s look at how the user credentials are stored in the database.

Installing H2 database:

Download the latest stable version of H2 database from the following link.
www.h2database.com/html/downlo…

After downloading the zip file from the ‘Platform-Independent Zip’ link, extract it into a folder of your choice. Your extracted folder should look similar to the following screenshot.

Open the Command Prompt window and navigate to the bin folder under h2 and run the h2.bat file. The H2 login screen should pop up from your default browser. Update the JDBC URL to jdbc:h2:~/crm-oauth2 as seen in the following screen and click on ‘Connect’.

You should get the following screen.

Issue the following ‘select’ SQL statement and click the Run button.
select * from USERS;

As you can see, the encrypted password is saved in the database.

An important note to remember is that the way we are accessing the H2 database is through single user access. Your application can’t connect while you keep the connection open from the browser, which is launched from the Command Prompt. So as soon as you are done with looking at the data, disconnect it by clicking on the first icon.

After disconnecting it from the database, if your application is throwing any errors, then close the browser tab and press CTRL-C from the Command Prompt.

Conclusion

In this post, we learned how to Persist the user in the database, Configure Spring Security to access the users and roles from the database, define Method Level Security to restrict user with certain roles, Obtain information about the current user, and install the H2 database to see how the encrypted password is stored in the database.

What’s Next in this series?

You may have noticed that every time we deploy or re-start the application server we are getting the new token by providing the user credentials. Even though the refresh token is valid for 30 days in our setup, once the server is stopped and restarted, the refresh token is not valid because we store tokens in memory by using InMemoryTokenStore. In real world applications, however, this is not a realistic scenario. In the next part of this series, we will use a database to store tokens.

The source code used for this post is available on GitHub.

 

Share this post: on Twitter on Facebook on Google+