代码之家  ›  专栏  ›  技术社区  ›  Tadeusz Kleszcz

使用Spring Session Redis时身份验证主体为空

  •  3
  • Tadeusz Kleszcz  · 技术社区  · 9 年前

    我正在使用Spring Boot v1.3.3构建rest API。API由Spring Security保护。我已经实现了自定义用户详细信息服务,以便在身份验证上下文中具有自定义主体。

    我需要与其他Spring应用程序共享API会话,因此我选择使用本教程在我的应用程序中使用Redis服务器实现Spring会话 docs.spring.io/spring-session/docs/current/reference/html/guides/security.html 。不幸的是,它导致身份验证主体停止工作。当我试图通过注释获取当前校长时 @AuthenticationPrincipal CustomUserDetails user 或通过 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 它返回我的自定义用户详细信息,但带有 Id = 0 所有字段都设置为 null ( screen from debugging ). 我甚至无法从中获取用户名 SecurityContextHolder.getContext().getAuthentication().getName() .

    在我注释了Redis代码和maven依赖之后,它就可以工作了( see debug screen ). 如何使其与Spring Session和Redis服务器一起工作?

    以下是应用程序的一些代码:

    检查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);
    }
    

    应用程序和Redis配置:

    @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);
        }
    }
    

    初始化程序(用于Redis会话)

    public class Initializer extends AbstractHttpSessionApplicationInitializer {
    
    }
    

    SecurityInitializer(用于Redis会话)

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

    WebSecurityConfig(Spring安全配置)

    @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>
        <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 回复  |  直到 9 年前
        1
  •  3
  •   Tadeusz Kleszcz    9 年前

    我解决了这个问题。事实证明,Spring Session序列化了Principal对象。我的自定义实现 UserDetails 是Hibernate模型的子类 User 班我通过实施 Serializable 我的自定义界面 用户详细信息 , 使用者 模型和此模型中使用的所有类。

        2
  •  2
  •   yglodt    8 年前

    为了使它在我的案例中起作用,我还必须确保Servlet过滤器的设置顺序正确。

    对我来说:

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

    之后 principal 不再是空的。

        3
  •  1
  •   s1moner3d    6 年前

    正如@yglodt所说,问题在于过滤器在弹簧安全过滤器链中的顺序。

    在Java配置方式中,只需将Redis配置类的优先级设置得更高

    @Configuration
    @EnableRedisHttpSession
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class RedisConfig extends AbstractHttpSessionApplicationInitializer {
    
        @Bean
        public JedisConnectionFactory connectionFactory() {
            return new JedisConnectionFactory();
        }
    
    }
    

    我设置了最高的优先级,但也许更低的优先级就足够了。

    现在应该正确填充主体。

        4
  •  -1
  •   Niklas    2 年前

    HttpSecurity链的顺序很重要:

    无效,并将主体名称保留为空:

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

    工作正确:

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

    编辑:2022此答案已过时,将引发 IllegalStateException 根据@BendaThierry.com