Paghillect
Paghillect

Reputation: 858

How to properly configure sessions in spring boot?

I've been probably trying harder than I should to get this to work, but now I give up. I need a simple HttpSession based authentication on my Spring Boot project which should be rather straightforward based on the docs.

Here is my 'WebSecurityConfigurerAdapter' implementation:

package com.collective.foundation ;

import org.springframework.beans.factory.annotation.Autowired ;
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.EnableWebSecurity ;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter ;
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.config.http.SessionCreationPolicy ;

import com.collective.foundation.usermanagement.UserDetailsServiceImpl ;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService ;

    @Override
    protected UserDetailsService userDetailsService () {
       return this.userDetailsService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
        .antMatchers("/market/**").access("hasRole('CUSTOMER')")
        .antMatchers("/scm/staff/**").access("hasRole('SCM_STAFF')")
        .antMatchers("/scm/manager/**").access("hasRole('SCM_MANAGER')")
        .antMatchers("/warehouse/staff/**").access("hasRole('WAREHOUSE_STAFF')")
        .antMatchers("/warehouse/manager/**").access("hasRole('WAREHOUSE_MANAGER')")
        .antMatchers("/warehouse/supplier/**").access("hasRole('SUPPLIER')")
        .antMatchers("/user/**").permitAll()
        .antMatchers("/**").permitAll()
        // .and()
        //     .formLogin()
        //         .loginPage("/user/login/")
        //         .permitAll()
        .and()
            .logout()
                .permitAll()
        .and()
            .exceptionHandling()
                .accessDeniedPage("/err/403/")
        .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService) ;
    }
}

I have a User model with a role field to encapsulate principal data, so here is my UserDetailsService implementation:

package com.collective.foundation.usermanagement ;

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.UserDetails ;
import org.springframework.security.core.userdetails.UserDetailsService ;
import org.springframework.security.core.userdetails.UsernameNotFoundException ;
import org.springframework.stereotype.Service ;
import org.springframework.transaction.annotation.Transactional ;

import java.util.HashSet ;
import java.util.Set ;

import com.collective.foundation.entities.* ;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository ;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findOne(username) ;

        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole()));

        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
    }
}

I'm relying on my own controller to handle the login page, here is the method:

@PostMapping(value="login/")
public String submitLoginView(@ModelAttribute LoginDto loginDto) {
    //
    System.out.println(Constants.LOG_PREFIX + "submitLoginView") ;
    System.out.println(loginDto.toString()) ;
    //
    User user = userRepository.findOne(loginDto.getUsername()) ;
    if (user != null && user.getPassword().equals(loginDto.getPassword())) {
        System.out.println(Constants.LOG_PREFIX + "successful login") ;
        securityService.autologin(loginDto.getUsername(), loginDto.getPassword()) ;
        return "view_home_page" ;
    } else {
        System.out.println(Constants.LOG_PREFIX + "no matching login found") ;
        return "redirect:/user/login/err/" ;
    }
}

The autoLogin method in the above controller simply puts a USerDetails instance obtained from the userDetailsService into the SecurityContext:

public void autologin(String username, String password) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(username) ;
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()) ;
    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken) ;
}

Yet the isn't authenticated automatically upon their next request, so I'm wondering what is wrong here.

Upvotes: 1

Views: 922

Answers (1)

Bohdan Levchenko
Bohdan Levchenko

Reputation: 3561

The problem is in autologin method.

Please do not use userDetailsService directly. Inject AuthenticationManager instead and pass to it authenticate method UsernamePasswordAuthenticationToken with raw username and password. It will call userDetailsService under the hood, perform all checks and if credentials are valid, account not locked, etc - return to you Authentication which can be set into SecurityContext.

Here is a simplified version of your example:

@SpringBootApplication
public class So44739147Application {

    public static void main(String[] args) {
        SpringApplication.run(So44739147Application.class, args);
    }

    @EnableWebSecurity
    static class Security extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().authorizeRequests()
                    .antMatchers("/login").permitAll()
                    .antMatchers("/protected/*").fullyAuthenticated()
                    .and().logout().permitAll().and().exceptionHandling().accessDeniedPage("/err/403/").and()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        }
    }

    @Service
    public static class SecurityService {
        private final AuthenticationManager authenticationManager;

        @Autowired
        public SecurityService(AuthenticationManager authenticationManager) {
            this.authenticationManager = authenticationManager;
        }

        void autologin(String username, String password) {
            final Authentication usernamePasswordAuthenticationToken = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    public static class CredentialsDTO {
        String username;
        String password;
    }

    @RestController
    @RequestMapping("/login")
    public static class LoginApi {
        private final SecurityService securityService;

        @Autowired
        public LoginApi(SecurityService securityService) {
            this.securityService = securityService;
        }

        @PostMapping
        public String login(@RequestBody CredentialsDTO credentials) {
            securityService.autologin(credentials.username, credentials.password);
            return "ok";
        }
    }

    @RestController
    @RequestMapping("/protected/me")
    public static class MeApi {
        @GetMapping
        public String me(Principal principal) {
            return principal.getName();
        }
    }
}

=>

$ curl -i -XPOST 'localhost:8080/login' -H'Content-Type: application/json' -d'{"username":"user","password":"123"}'
HTTP/1.1 200 
...
Set-Cookie: JSESSIONID=1FECEF3B13065A8938A4B8DA951ED96F; Path=/; HttpOnly
...

ok%
$ curl -XGET 'localhost:8080/protected/me' -H'Cookie: JSESSIONID=1FECEF3B13065A8938A4B8DA951ED96F'                 
user%

Upvotes: 1

Related Questions