如何将 Spring Security 6.4.0 密钥与 RedisHttpSessionRepository 一起使用?

问题描述 投票:0回答:1

如何配置 Spring Boot 3.4.0 将 PublicKeyCredentialRequestOptions.java 存储在 RedisSessionRepository 中?

我正在使用 Spring Boot 3.4.0、Spring Security 6.4.0 和 @EnableRedisHttpSession。当默认 /login 启动身份验证仪式时,我在服务器日志中收到此异常(注意最后的完整堆栈跟踪):

o.e.jetty.ee10.servlet.ServletChannel    : /webauthn/authenticate/options

org.springframework.data.redis.serializer.SerializationException: Cannot serialize
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:97) ~[spring-data-redis-3.4.0.jar:3.4.0]

在默认生成的/登录页面上:

  1. 输入用户名/密码点击
    Sign in
    成功。用户会话保存在 Redis 中。
  2. 单击
    Sign in with a Passkey
    失败并返回 HTTP 500。Redis 无法序列化
    PublicKeyCredentialRequestOptions

我对WebAuthn注册或身份验证的理解是它们执行两个请求/响应到达。

  1. 服务器生成包含挑战的
    PublicKeyCredentialRequestOptions
    。返回前已保留。
  2. 服务器收到
    PublicKeyCredential
    ,并查找持久化的
    PublicKeyCredentialRequestOptions
    以验证
    PublicKeyCredential

这是我在默认/登录页面上单击

Sign in with a Passkey
时获得的完整堆栈跟踪。我的配置低于此。

2024-12-14T19:42:31.835-05:00  WARN 2484 --- [springs-server-authentication] [tp1933073727-77] [                                                 ] o.e.jetty.ee10.servlet.ServletChannel    : /webauthn/authenticate/options

org.springframework.data.redis.serializer.SerializationException: Cannot serialize
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:97) ~[spring-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.data.redis.core.AbstractOperations.rawHashValue(AbstractOperations.java:206) ~[spring-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.data.redis.core.DefaultHashOperations.putAll(DefaultHashOperations.java:161) ~[spring-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.session.data.redis.RedisSessionRepository$RedisSession.saveDelta(RedisSessionRepository.java:328) ~[spring-session-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.session.data.redis.RedisSessionRepository$RedisSession.save(RedisSessionRepository.java:306) ~[spring-session-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.session.data.redis.RedisSessionRepository.save(RedisSessionRepository.java:132) ~[spring-session-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.session.data.redis.RedisSessionRepository.save(RedisSessionRepository.java:45) ~[spring-session-data-redis-3.4.0.jar:3.4.0]
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:229) ~[spring-session-core-3.4.0.jar:3.4.0]
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:145) ~[spring-session-core-3.4.0.jar:3.4.0]
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82) ~[spring-session-core-3.4.0.jar:3.4.0]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) ~[spring-web-6.2.0.jar:6.2.0]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) ~[spring-web-6.2.0.jar:6.2.0]
    at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:114) ~[spring-web-6.2.0.jar:6.2.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
    at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.0.jar:6.2.0]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.0.jar:6.2.0]
    at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1547) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:819) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:436) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:464) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:575) ~[jetty-security-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:717) ~[jetty-ee10-servlet-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.handler.EventsHandler.handle(EventsHandler.java:81) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.Server.handle(Server.java:182) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:662) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:418) ~[jetty-server-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.ssl.SslConnection$SslEndPoint.onFillable(SslConnection.java:575) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:390) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:150) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53) ~[jetty-io-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:478) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:441) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:201) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209) ~[jetty-util-12.0.15.jar:12.0.15]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164) ~[jetty-util-12.0.15.jar:12.0.15]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer
    at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:64) ~[spring-core-6.2.0.jar:6.2.0]
    at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:33) ~[spring-core-6.2.0.jar:6.2.0]
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:95) ~[spring-data-redis-3.4.0.jar:3.4.0]
    ... 49 common frames omitted
Caused by: java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions]
    at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:43) ~[spring-core-6.2.0.jar:6.2.0]
    at org.springframework.core.serializer.Serializer.serializeToByteArray(Serializer.java:56) ~[spring-core-6.2.0.jar:6.2.0]
    at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:60) ~[spring-core-6.2.0.jar:6.2.0]
    ... 51 common frames omitted

Redis Http 会话存储库配置

@Configuration
@Import(RedisConfiguration.ExtraConfiguration.class)
@EnableRedisHttpSession
@Slf4j
public class RedisConfiguration {
    @Autowired
    private RedisProperties redisProperties;

    @Bean(initMethod="start",destroyMethod="stop")
    public RedisServer redisServerEmbedded(final Environment environment) throws IOException {
        final String host = this.redisProperties.getHost();
        final Integer port = this.redisProperties.getPort();
        final RedisServer redisServer = new RedisServer(port);
        if (!redisServer.isActive()) {
            redisServer.start();
            log.info("Started embedded redis server, host: {}, port: {}", host, port);
        }
        return redisServer;
    }

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        final String host = this.redisProperties.getHost();
        final Integer port = this.redisProperties.getPort();
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(final LettuceConnectionFactory redisConnectionFactory) {
        final RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Configuration
    public static class ExtraConfiguration {
        @Autowired
        private RedisSessionRepository redisSessionRepository;

        @PostConstruct
        public void postConstruct() {
            log.info("Add CustomSessionIdGenerator to sessionRepository: {}", this.redisSessionRepository.getClass().getCanonicalName());
            this.redisSessionRepository.setSessionIdGenerator(new CustomSessionIdGenerator());
        }
    }
}

安全过滤链配置

@Configuration
@EnableAutoConfiguration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityFilterChainConfiguration {
    @Primary
    @Bean
    public UserDetailsService webauthnUserDetailsService() {
        return new InMemoryUserDetailsManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChainUserUi(HttpSecurity http) throws Exception {
        http.securityMatcher("/login", "/logout", "/default-ui.css", "/login/webauthn.js", "/login/webauthn", "/webauthn/**", "/secure/**")
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login", "/logout", "/default-ui.css", "/login/webauthn.js", "/login/webauthn", "/webauthn/**").permitAll()
                .requestMatchers("/secure/**").authenticated()
            )
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .httpBasic(AbstractHttpConfigurer::disable)
            .formLogin(form -> form
                .permitAll()
                .defaultSuccessUrl("/secure/home", true)
            )
            .webAuthn((webAuthn) -> webAuthn
                .rpName("Passkeys Relying Party")
                .rpId(this.serverAddress)
                .allowedOrigins("https://" + this.serverAddress)
            )
            .logout(logout -> logout
                .permitAll()
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
            )
            .sessionManagement(session -> session
                  .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                  .maximumSessions(3)
                  .expiredUrl("/login?expired")
            );
        return http.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChainResources(HttpSecurity http) throws Exception {
        http.securityMatcher("/helloworld", "/static/**", "/public/**", "/templates/**", "/META-INF/resources/**")
            .authorizeHttpRequests(authz -> authz
                 .requestMatchers("/helloworld", "/static/**", "/public/**", "/templates/**", "/META-INF/resources/**").permitAll()
            )
            .csrf(AbstractHttpConfigurer::disable) // Typically disabled for stateless APIs
            .sessionManagement(management -> management
                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
        return http.build();
    }
}
spring spring-boot spring-security webauthn passkey
1个回答
0
投票

回答我自己的问题,因为我让 Jackson JSON 序列化适用于注册和身份验证对象。

我必须向我的 RedisSerializer bean 使用的 ObjectMapper 添加 3 个自定义项:

  1. registerModules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader()))
  2. registerModule(new WebauthnJackson2Module())
    ,因为1没有调用它
  3. addMixin
    ,因为 2 缺少两个请求对象内部部分所需的 12 个 mixins

我最初实现了 22 个我自己的 mixin,并且序列化/反序列化工作正常。那是在我发现步骤 1 处理了 1 个所需的 mixin,而步骤 2 处理了 9 个所需的 mixin 之前。之后,我仍然需要原来 22 个 mixins 中的 12 个。

解决这个问题需要进行大量的调试,并深入源代码来找到各个部分。我没有在任何文档中找到它,所以也许这可以让其他人了解如何使其工作。

@Configuration
public class WebauthnJacksonMixinConfiguration {
    @Qualifier("springSessionDefaultObjectMapper")
    @Autowired
    private ObjectMapper springSessionDefaultObjectMapper;

    @PostConstruct
    public void postConstruct() {
        updateObjectMapper(this.springSessionDefaultObjectMapper);
    }

    public static void updateObjectMapper(final ObjectMapper objectMapper) {
        objectMapper.registerModule(new WebauthnJackson2Module());
//        objectMapper.addMixIn(Bytes.class, WebauthnBytesMixIn.class);

        objectMapper.addMixIn(PublicKeyCredentialCreationOptions.class, WebauthnPublicKeyCredentialCreationOptionsMixIn.class);
        objectMapper.addMixIn(ImmutablePublicKeyCredentialUserEntity.class, PublicKeyCredentialUserEntityMixIn.class);
//        objectMapper.addMixIn(PublicKeyCredentialUserEntity.class, PublicKeyCredentialUserEntityMixIn.class);
        objectMapper.addMixIn(PublicKeyCredentialRpEntity.class, PublicKeyCredentialRpEntityMixIn.class);
        objectMapper.addMixIn(PublicKeyCredentialParameters.class, PublicKeyCredentialParametersMixIn.class);
//        objectMapper.addMixIn(PublicKeyCredentialType.class, PublicKeyCredentialTypeMixIn.class);
//        objectMapper.addMixIn(COSEAlgorithmIdentifier.class, COSEAlgorithmIdentifierMixIn.class);
        objectMapper.addMixIn(AuthenticatorSelectionCriteria.class, AuthenticatorSelectionCriteriaMixIn.class);
//        objectMapper.addMixIn(AttestationConveyancePreference.class, AttestationConveyancePreferenceMixIn.class);
//        objectMapper.addMixIn(AuthenticatorAttachment.class, AuthenticatorAttachmentMixIn.class);
//        objectMapper.addMixIn(ResidentKeyRequirement.class, ResidentKeyRequirementMixIn.class);
//        objectMapper.addMixIn(UserVerificationRequirement.class, UserVerificationRequirementMixIn.class);
//
        objectMapper.addMixIn(PublicKeyCredentialRequestOptions.class, WebauthnPublicKeyCredentialRequestOptionsMixIn.class);
        objectMapper.addMixIn(ImmutableAuthenticationExtensionsClientInputs.class, AuthenticationExtensionsClientInputsMixIn.class);
        objectMapper.addMixIn(AuthenticationExtensionsClientInputs.class, AuthenticationExtensionsClientInputsMixIn.class);
        objectMapper.addMixIn(AuthenticationExtensionsClientInput.class, AuthenticationExtensionsClientInputMixIn.class);
        objectMapper.addMixIn(PublicKeyCredentialDescriptor.class, PublicKeyCredentialDescriptorMixIn.class);
//        objectMapper.addMixIn(AuthenticatorTransport.class, AuthenticatorTransportMixIn.class);
        objectMapper.addMixIn(CredProtectAuthenticationExtensionsClientInput.class, CredProtectAuthenticationExtensionsClientInputMixIn.class);
        objectMapper.addMixIn(CredProtect.class, CredProtectMixIn.class);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.