Tuesday, January 24, 2017

Spring Security takes long time to login

Problem


In our project we've implemented a login process based on spring security. After that we faced one problem -  why does it take so long? Our login request took about 6-7 seconds. There was a quite long CPU peak during this operation. We asked ourselves, what might be the reason. There were a few points where the problem might have existed - the application was contained in docker, it contained hystrix, api gateway, service discovery, 2 databases - postgres and redis. We tried to run this application without all that extra stuff - no progress, still 7 seconds. After that we knew, the problem was inside the spring library, not within our code.




Implementation

You can find a simple example project on GitHub here:


https://github.com/bartoszkomin/spring-login-long-demo




Please take a look at class SecurityConfig extended from WebSecurityConfigurerAdapter:

 package com.blogspot.bartoszkomin.spring_login.config;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.boot.autoconfigure.security.SecurityProperties;  
 import org.springframework.context.annotation.Bean;  
 import org.springframework.context.annotation.Configuration;  
 import org.springframework.core.annotation.Order;  
 import org.springframework.http.HttpMethod;  
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;  
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;  
 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;  
 import com.blogspot.bartoszkomin.spring_login.service.CustomUserDetailsService;  
 /**  
  * @author Bartosz Komin  
  *  
  */  
 @Configuration  
 @EnableGlobalMethodSecurity(prePostEnabled = true)  
 public class SecurityConfig extends WebSecurityConfigurerAdapter {  
     /**  
      * Init custom user detail service  
      */  
     @Autowired  
     private CustomUserDetailsService userDetailsService;  
     /* (non-Javadoc)  
      * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)  
      */  
     @Override  
     protected void configure(HttpSecurity http) throws Exception {  
         http.csrf().disable();  
         http.httpBasic().and()  
             .authorizeRequests()  
                 .antMatchers(HttpMethod.OPTIONS).permitAll()  
                 .antMatchers(HttpMethod.POST,"/login").permitAll()  
                 .antMatchers(HttpMethod.GET, "/insert").permitAll()  
                 .anyRequest().authenticated();  
     }  
     /* (non-Javadoc)  
      * @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#configure(org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder)  
      */  
     @Override  
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {  
         auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());  
     }  
     @Bean  
     public PasswordEncoder getPasswordEncoder() {  
         BCryptPasswordEncoder bCryptPAsswordEncoder = new BCryptPasswordEncoder(16);      
         return bCryptPAsswordEncoder;  
     }  
 }  

We override the configure method to use our own user entity object to get the password from our database and define the password encoder. In our case the password is encoded with BCrypt. BCryptPasswordEncoder obtains the strength of the password as a parameter. In our case it is 16.

CustomUserDetailsService looks like this:

 package com.blogspot.bartoszkomin.spring_login.service;  
 import java.util.ArrayList;  
 import java.util.List;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.security.core.GrantedAuthority;  
 import org.springframework.security.core.authority.SimpleGrantedAuthority;  
 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.stereotype.Service;  
 import com.blogspot.bartoszkomin.spring_login.repository.UserRepository;  
 @Service  
 public class CustomUserDetailsService implements UserDetailsService {  
     @Autowired  
     private UserRepository repository;  
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
         User user = null;  
         if (username == null || username.isEmpty()) {  
             throw new UsernameNotFoundException("User not found");  
         }  
         com.blogspot.bartoszkomin.spring_login.model.User u = repository.findByName(username);  
         if (u == null) {  
             throw new UsernameNotFoundException("User " + username + " not found");  
         }  
         boolean enabled = true;  
         boolean accountNonExpired = true;  
         boolean credentialsNonExpired = true;  
         boolean accountNonLocked = true;  
         user = new User(u.getName(), u.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(null));  
         return user;  
     }  
     private List<SimpleGrantedAuthority> getAuthorities(String role) {  
     List<SimpleGrantedAuthority> authList = new ArrayList<SimpleGrantedAuthority>();  
     authList.add(new SimpleGrantedAuthority("ROLE_USER"));  
     return authList;  
   }  
 }  

And User entity:

 package com.blogspot.bartoszkomin.spring_login.model;  
   
 import javax.persistence.Entity;  
 import javax.persistence.GeneratedValue;  
 import javax.persistence.GenerationType;  
 import javax.persistence.Id;  
 import javax.persistence.Table;  
   
 /**  
  * @author Bartosz Komin  
  * User  
  */  
 @Entity  
 @Table(name = "users")  
 public class User {  
       
     @Id  
     @GeneratedValue(strategy = GenerationType.IDENTITY)  
     private Integer id;  
   
     private String name;  
   
     private String password;  
   
   
     public String getName() {  
         return name;  
     }  
   
     public void setName(String name) {  
         this.name = name;  
     }  
   
     public String getPassword() {  
         return password;  
     }  
   
     public void setPassword(String password) {  
         this.password = password;  
     }  
   
     public Integer getId() {  
         return id;  
     }  
   
 }  


In our example project we have 3 REST endpoints:
  • POST /login - to display token.
  • GET /insert - you don't need to be authenticated to use this endpoint, it inserts user login and password to database, the login is "user" and the password is "password". Data is needed to login to the system.
  • GET /secret - if you are not authenticated, you will get a 401 Unauthorized HTTP response. To login you need to use Basic authentication with base64 login/pass encoded. In our case it will be: Authorization: Basic dXNlcjpwYXNzd29yZA==
Example response to GET /secret without authentication:


After executing GET/insert (where we put user and password record to database), GET /secret with basic auth response looks like this:


Let's execute the same endpoint, but with measuring the exact execution time:


On i7 processor the login execution time was 4.6 seconds. Quite long, isnt't it?


Solution

To check what was wrong, we used VisualVM.
Let's execute the project jar like this:

 java -jar spring-login-1.0.0.jar  

and then run VisualVM. Insert login/pass with GET/insert, and then try to login. This is what we see on VisualVM for this operation:


Got you! 96% of CPU time was used by org.springframework.security.crypto.bcrypt.BCrypt.key() and it was 4699 ms. And this is our answer - everything works fine, but our password encryption level is so high, it has to take time.

In this example, strength of the password for BCrypt was set to 16.
Let's check the time one more time, for password strength = 12.
The results are:


This time the encrypt operation was ready in 299 ms.


No comments:

Post a Comment