3

I am building rest API using Spring Boot v1.3.3. API is secured by Spring Security. I have implemented custom user details service to have custom principal in authentication context.

I needed to share sessions of API with other Spring app so I choosen to implement Spring Session with Redis server in my app using this tutorial docs.spring.io/spring-session/docs/current/reference/html5/guides/security.html. Unfortunetly it caused Authentication Principal to stop working. When I am trying to get current Principal either by annotation @AuthenticationPrincipal CustomUserDetails user or by SecurityContextHolder.getContext().getAuthentication().getPrincipal() it returns my custom user details but with Id = 0 and all fields set to null (screen from debugging). I can't even get username from SecurityContextHolder.getContext().getAuthentication().getName().

After I commented Redis code and maven dependency it works (see debug screen). How to make it working with Spring Session and Redis server?

Here is some code from the app:

Some example method to check Principal

@RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(@AuthenticationPrincipal CustomUserDetails user) {
    User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if (user != null) {
        String name = user.getUsername();
        return new StatusData(name);
    } else return new StatusData(null);
}

Application and Redis config:

@Configuration
@EnableRedisHttpSession
public class AppConfig {

    @Bean
    public JedisConnectionFactory connectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public ShaPasswordEncoder shaEncoder() {
        return new ShaPasswordEncoder(256);
    }

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

    @Bean(name = "messageSource")
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
        resourceBundleMessageSource.setBasename("messages/messages");
        return resourceBundleMessageSource;
    }

    @Bean
    public Validator basicValidator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(messageSource());
        return validator;
    }

    public AppConfig() {
        DateTimeZone.setDefault(DateTimeZone.UTC);
    }
}

Initializer (used for Redis Session)

public class Initializer extends AbstractHttpSessionApplicationInitializer {

}

SecurityInitializer (used for Redis session)

public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {

    public SecurityInitializer() {
        super(WebSecurityConfig.class, AppConfig.class);
    }
}

WebSecurityConfig (Spring Security config)

@Configuration
@EnableWebSecurity
//@EnableWebMvcSecurity
@ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Autowired
    private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;

    @Autowired
    private AuthSuccessHandler authSuccessHandler;

    @Autowired
    private AuthFailureHandler authFailureHandler;

    @Autowired
    private HttpLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /**
     * Persistent token repository stored in database. Used for remember me feature.
     */
    @Bean
    public PersistentTokenRepository tokenRepository() {
        JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
        db.setDataSource(dataSource);
        return db;
    }

    /**
     * Enable always remember feature.
     */
    @Bean
    public AbstractRememberMeServices rememberMeServices() {
        CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
        rememberMeServices.setAlwaysRemember(true);
        rememberMeServices.setTokenValiditySeconds(1209600);
        return rememberMeServices;
    }

    /**
     * Configure spring security to use in REST API.
     * Set handlers to immediately return HTTP status codes.
     * Enable remember me tokens.
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(httpAuthenticationEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .successHandler(authSuccessHandler)
                .failureHandler(authFailureHandler)
                .and()
                .logout()
                .permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .rememberMe().rememberMeServices(rememberMeServices())
                .and()
                .headers()
                .addHeaderWriter(new HeaderWriter() {
                    /**
                     * Header to allow access from javascript AJAX in chrome extension.
                     */
                    @Override
                    public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
                        String corsUrl = "https://mail.google.com";
                        if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
                            httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
                            httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
                            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
                            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
                        }
                    }
                });
    }

    /**
     * Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }
}

Maven dependencies

<dependencies>
    <dependency>
        <groupId>${project.groupId}</groupId>
        <artifactId>models</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>5.2.3.Final</version>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jadira.usertype</groupId>
        <artifactId>usertype.core</artifactId>
        <version>3.1.0.CR1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.jaxrs</groupId>
        <artifactId>jackson-jaxrs-json-provider</artifactId>
        <version>2.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-joda</artifactId>
    </dependency>
    <dependency>
        <groupId>com.maxmind.geoip2</groupId>
        <artifactId>geoip2</artifactId>
        <version>2.6.0</version>
    </dependency>
    <dependency>
        <groupId>com.ganyo</groupId>
        <artifactId>gcm-server</artifactId>
        <version>1.0.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session</artifactId>
        <version>1.1.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.4.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

4 Answers 4

2

I solved this problem. It turned out that Spring-Session serializes the Principal object. My custom implementation of UserDetails was subclass of Hibernate Model User class. I solved it by implementing Serializable interface in my custom UserDetails, User model and all classes used in this model.

Sign up to request clarification or add additional context in comments.

4 Comments

I also have a custom UserDetails, and I changed it to implement Serializable (public class MyUserDetails extends org.springframework.security.core.userdetails.User implements Serializable). I use Spring Session with JDBC backend, but the column principal_name is NULL. Strange.
I agree with @yglodt, this might not be the good answer. If you check at org.springframework.security.core.userdetails.UserDetails class, it's already extending the java.io.Serializable interface
@BigDong that's right! In fact, that is not the problem! I'm having this issue after upgrading spring-boot...Anyone found a solution?
Found it! I share it for people: the problem is the filter order in the security chain. I solved it by setting an higher precedence to redis configuration @Order(Ordered.HIGHEST_PRECEDENCE) (I set the highest precedence, but maybe something lower is enough). Now my principal is correctly populated.
2

To make it work in my case I had as well to make sure the Servlet filters were set up in the right order.

For me that was:

...
<filter-name>CharacterEncodingFilter</filter-name>
...
<filter-name>springSessionRepositoryFilter</filter-name>
...
<filter-name>springSecurityFilterChain</filter-name>
...
<filter-name>csrfFilter</filter-name>
...

After that, the principal was not empty anymore.

Comments

1

As @yglodt said, the problem is the filter's order in the spring security filter chain.

In Java Config way, just set an higher precedence to Redis configuration class

@Configuration
@EnableRedisHttpSession
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisConfig extends AbstractHttpSessionApplicationInitializer {

    @Bean
    public JedisConnectionFactory connectionFactory() {
        return new JedisConnectionFactory();
    }

}

I set the highest precedence, but maybe something lower is enough.

Now the principal should be correctly populated.

Comments

-1

The order of the HttpSecurity chain is important:

Does not work, and leaves principal name null:

.authorizeRequests()
.antMatchers("/api/register").permitAll()
.anyRequest().authenticated()

Works correct:

.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/register").permitAll()

EDIT: 2022 This answer is outdated and will throw an IllegalStateException according to @BendaThierry.com

2 Comments

Falsy answer, because we can't configure any antMatchers() or mvcMatchers() after anyRequest(). It creates an IllegalStateException if you try, at least with Spring Boot 2.6.6 nowadays... though 4 years later, I think it didn't change since.
Thx for point that out. Didnt double check in years :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.