6

I precise that I am a french student in 1st year of Java Developper.

I'm developing a little multi-module app using: Spring Boot, Spring security, Hibernate, Spring Data, Spring MVC and Thymeleaf.

I would like to set the User in the session, or at least the userId, at login. This way I don't have to put it manually in the session or in the model each time I need it.

But as I use the default Spring Security login and authentication configuration, I really don't know how or where to call such a method:

void putUserInHttpSession( HttpSession httpSession ) {
        httpSession.setAttribute( "user" , getManagerFactory().getUserManager().findByUserName( SecurityContextHolder.getContext().getAuthentication().getName()) );
    }

I can do it eahc time I need it but I find it pretty ugly not to just do this at login in!

Here are what I think you might need to help me (that would be AWESOME !!! :)

My WebSecurityConfig class:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private DataSource dataSource;

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

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        // Setting Service to find User in the database.
        // And Setting PassswordEncoder
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

    }


    @Override
    protected void configure( HttpSecurity http ) throws Exception {

        http.csrf().disable();


        // /userInfo page requires login as ROLE_USER or ROLE_ADMIN.
        // If no login, it will redirect to /login page.
        http.authorizeRequests().antMatchers(
                "/user/**")
                .access("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')");

        // For ADMIN only.
        http.authorizeRequests().antMatchers(
                "/admin/**")
                .access("hasRole('ROLE_ADMIN')");

        // When the user has logged in as XX.
        // But access a page that requires role YY,
        // AccessDeniedException will be thrown.
        http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/public/403");

        // Config for Login Form
        http.authorizeRequests().and().formLogin()//
                // Submit URL of login page.
                .loginProcessingUrl("/j_spring_security_check") // Submit URL
                .loginPage("/public/login").defaultSuccessUrl("/public/showAtlas")//
                .failureUrl("/public/login?error=true")//
                .usernameParameter("username")//
                .passwordParameter("password")
                //Config for Logout Page
                .and()
                .logout().logoutUrl("/public/logout").logoutSuccessUrl("/public/logoutSuccessful");

        http.authorizeRequests().antMatchers(
                "/public/**").permitAll();
        // The pages does not require login
    }

}

My UserDetailsServiceImpl class:

@Service
public class UserDetailsServiceImpl implements UserDetailsService{

    @Autowired
    private ManagerFactory managerFactory;

//  private HttpSession httpSession;

    /**
     * The authentication method uses the user email, since it is easier to remember for most users
     * @param input
     * @return a UserDetails object
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername( String input) throws UsernameNotFoundException {

        User user = new User();

        if( input.contains( "@" )){
            user =  this.managerFactory.getUserManager().findByEmail( input );
        }
        else {
            user =  this.managerFactory.getUserManager().findByUserName( input );
        }


        if (user == null) {
            throw new UsernameNotFoundException( "User with email " + input + " was not found in the database" );
        }

        // [ROLE_USER, ROLE_ADMIN,..]
        List<String> roleNames = this.managerFactory.getRoleManager().findRoleByUserName(user.getUserName());

        List<GrantedAuthority> grantList = new ArrayList<GrantedAuthority>();
        if (roleNames != null) {
            for (String role : roleNames) {
                // ROLE_USER, ROLE_ADMIN,..
                GrantedAuthority authority = new SimpleGrantedAuthority(role);
                grantList.add(authority);
            }
        }

        return (UserDetails) new org.springframework.security.core.userdetails.User(user.getUserName(),
                user.getPassword(), grantList);
    }
}

My simple LoginController:

@Controller
public class LoginController{

    @GetMapping("/public/login")
    public String login(Model model ){


        return "view/login";
    }

    @GetMapping("/public/logoutSuccessful")
    public String logout(Model model) {

        return "view/logoutSuccessful";

    }

So, is there a simple way to put the user or userId in the httpSession at login?

Thank you very much guys!!!

THE SOLUTION

Create a CustomAuthenticationSuccessHandler

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ManagerFactory managerFactory;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {

        String userName = "";
        HttpSession session = request.getSession();
        Collection< GrantedAuthority > authorities = null;
        if(authentication.getPrincipal() instanceof Principal ) {
            userName = ((Principal)authentication.getPrincipal()).getName();
            session.setAttribute("role", "none");
        }else {
            User userSpringSecu = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            session.setAttribute("role", String.valueOf( userSpringSecu.getAuthorities()));
            session.setAttribute( "connectedUser" , managerFactory.getUserManager().findByUserName( userSpringSecu.getUsername() ) );
        }
        response.sendRedirect("/public/showAtlas" );
    }
}

Then Autowired this class and add it in the WebSecurityConfigurerAdapter

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private DataSource dataSource;


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

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        // Setting Service to find User in the database.
        // And Setting PassswordEncoder
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

    }


    @Override
    protected void configure( HttpSecurity http ) throws Exception {

        http.csrf().disable();


        // /userInfo page requires login as ROLE_USER or ROLE_ADMIN.
        // If no login, it will redirect to /login page.
        http.authorizeRequests().antMatchers(
                "/user/**")
                .access("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')");

        // For ADMIN only.
        http.authorizeRequests().antMatchers(
                "/admin/**")
                .access("hasRole('ROLE_ADMIN')");
//      http.exceptionHandling().accessDeniedPage( "/error/403" );

        // When the user has logged in as XX.
        // But access a page that requires role YY,
        // AccessDeniedException will be thrown.
        http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/public/403");

        // Config for Login Form
        http.authorizeRequests().and().formLogin()//
                // Submit URL of login page.
                .loginProcessingUrl("/j_spring_security_check") // Submit URL
                .loginPage("/public/login")
                .defaultSuccessUrl("/public/showAtlas")//
                .successHandler( customAuthenticationSuccessHandler )
                .failureUrl("/public/login?error=true")//
                .usernameParameter("username")//
                .passwordParameter("password")
                //Config for Logout Page
                .and()
                .logout().logoutUrl("/public/logout").logoutSuccessUrl("/public/logoutSuccessful");

        http.authorizeRequests().antMatchers(
                "/public/**").permitAll();
        // The pages does not require login
    }

}
John Student
  • 298
  • 1
  • 6
  • 18
  • You can autowire `HttpSession` in the controller class. is your question about how to get `user`? – Shiva Nov 16 '18 at 15:45
  • I tried to call my "putUserInHttpSession" method in my LoginController, in "login(Model model)" method. But this method is called by Spring Mvc BEFORE the Spring Security authentication process. So the user isn't yet in the SecurityContextHolder at that time. Doesn't work then. And what I want is the user or user.id being put in session each time a user is connected (I mean with its ID and PWD). I hope i'm clear enough to get understood... ^^ – John Student Nov 16 '18 at 15:58
  • 1
    You can register `successHandler` in your `WebSecurityConfig` class and there you will get `Authentication` object from which you can obtain the userId. – Shiva Nov 17 '18 at 05:33
  • Sounds interesting guys. I don't want to abuse but could you tell me how to "register successHandler"? I understand I must create a custom implementation of AuthenticationSuccessHandler: `@Component public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {` But what should I put in? `@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {` to keep my login feature working as it currently does? – John Student Nov 17 '18 at 15:56

3 Answers3

12

Assuming you wanted to add user to session on seccessful login, You can create the AuthenticationSuccessHandler like below and register using successHandler(new AuthenticationSuccessHandlerImpl())

Update: If we create the object AuthenticationSuccessHandlerImpl, it will not be spring mananged and hence autowire into your Securityconfig and use it like shown below.

Here autowire the AuthenticationSuccessHandler in your WebSecurityConfig

@Autowired
AuthenticationSuccessHandler authenticationSuccessHandler;

and use it WebSecurityConfig.java

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
                .antMatchers("/resources/**", "/registration").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll().successHandler(authenticationSuccessHandler) // See here
                .and()
            .logout()
                .permitAll();
}

The AuthenticationSuccessHandlerImpl.java

import java.io.IOException;
import java.security.Principal;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.techdisqus.auth.repository.UserRepository;

@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler{

    @Autowired HttpSession session; //autowiring session

    @Autowired UserRepository repository; //autowire the user repo


    private static final Logger logger = LoggerFactory.getLogger(AuthenticationSuccessHandlerImpl.class);
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        // TODO Auto-generated method stub
        String userName = "";
        if(authentication.getPrincipal() instanceof Principal) {
             userName = ((Principal)authentication.getPrincipal()).getName();

        }else {
            userName = ((User)authentication.getPrincipal()).getUsername();
        }
        logger.info("userName: " + userName);
        //HttpSession session = request.getSession();
        session.setAttribute("userId", userName);

    }

}

Hope this helps.

Shiva
  • 1,962
  • 2
  • 13
  • 31
  • Thanks a lot! I think I get the mecanism! I will try to implement that this evening. Hope I will manage to make it work... If not, then I guess I will bother you a bit again ;) – John Student Nov 17 '18 at 16:56
  • Well this helped a lot! I can now have the userId in session! :) But I'm a bit greedy and I would like to put the whole User in the session, still at login. The new pb is that I don't have access to my JpaRepositories in `AuthenticationSuccessHandlerImpl`. I have acces to them in my Constollers but not here. Do you know how cn I fix it? The `@Autowired` doesn't work... – John Student Nov 18 '18 at 01:05
  • do you need a different `User` object than from the one you are getting userId? – Shiva Nov 18 '18 at 04:16
  • You have created the object manually and hence it is not spring managed bean instead `autowire` in your config and use it. updating code to illustrate the same. – Shiva Nov 18 '18 at 05:28
  • Yup I do need a User object from my domain module. In order to have access to its properties in my templates. Nevertheless I'm not sure if it's a good practice or not..? I did try to @Autowired the UserRepository, but it stays null... – John Student Nov 18 '18 at 12:51
  • You should autowire `AuthenticationSuccessHandler` inside `securityConfig` class – Shiva Nov 18 '18 at 13:00
  • It is now FULLY WORKING :) Thanks a lot guys!!! My mistake was to do a `new CustomAuthenticationSuccessHandler` in `WebSecurityConfg`, which has the effect to go out from the Spring Context. That's why I was getting a `null` from my `repository`. – John Student Nov 18 '18 at 13:40
  • Thansk, I just did :) – John Student Nov 18 '18 at 13:58
0

Let me supplement above two solutions. My experience showed the following statement initiated below exception:

session.setAttribute("userId", userName);

Exception:

java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread?

And I was able to remove it after studying Using a request scoped bean outside of an actual web request. That is, I've overridden onStartup method in the class which extends AbstractAnnotationConfigDispatcherServletInitializer class.

@Override
public void onStartup(ServletContext servletContext) 
        throws ServletException {
    super.onStartup(servletContext);
    servletContext.addListener(new RequestContextListener());
}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
Park JongBum
  • 1,245
  • 1
  • 16
  • 27
0

Another approach: Registered a bean listening for Spring Security's InteractiveAuthenticationSuccessEvent and SessionDestroyedEvent events. These events fire without any explicit configuration in a Spring Boot app.

See https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.security:

The basic features you get by default in a web application are:

  • . . .
  • A DefaultAuthenticationEventPublisher for publishing authentication events.

Handling these events, you can add username as a session attribute immediately after a user logons and remove that attribute when the security session (security context) is destroyed:

@Component
public class SessionStoreUsernameAuthEventHandler {

  @EventListener
  public void audit(InteractiveAuthenticationSuccessEvent e) {
    getSession().ifPresent(s -> s.setAttribute("username", e.getAuthentication().getName()));
  }

  @EventListener
  public void audit(SessionDestroyedEvent e) {
    getSession().ifPresent(s -> s.removeAttribute("username"));
  }
  
  private static Optional<HttpServletRequest> getCurrentRequest() {
    return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
      .filter(ServletRequestAttributes.class::isInstance)
      .map(ServletRequestAttributes.class::cast)
      .map(ServletRequestAttributes::getRequest);
  }

  private static Optional<HttpSession> getSession() {
    return getCurrentRequest().map(HttpServletRequest::getSession);
  }
}
Brice Roncace
  • 10,110
  • 9
  • 60
  • 69