Spring Boot,OAuth2 认证在请求之间丢失

6

编辑:

来自org.springframework.security的日志:

2022-01-17 12:31:03.495 IST
2022-01-17 10:31:03.495 DEBUG [080-exec-5] o.s.s.w.s.SessionManagementFilter - Request requested invalid session id D5F8BA31A3D7466AK3K3C8EA26A4F037
Default

2022-01-17 12:31:03.495 IST
2022-01-17 10:31:03.495 DEBUG [080-exec-5] o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
Debug

2022-01-17 12:31:03.495 IST
"Request requested invalid session id D5F8BA31A3D7466AK3K3C8EA26A4F037"
Debug

2022-01-17 12:31:03.495 IST
"Set SecurityContextHolder to anonymous SecurityContext"
Default

2022-01-17 12:31:03.494 IST
2022-01-17 10:31:03.494 DEBUG [080-exec-5] o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
Debug

2022-01-17 12:31:03.494 IST
"Set SecurityContextHolder to empty SecurityContext"
Default

2022-01-17 12:31:03.493 IST
2022-01-17 10:31:03.493 DEBUG [080-exec-5] o.s.security.web.FilterChainProxy - Securing GET /logo192.png
Debug

2022-01-17 12:31:03.493 IST
"Securing GET /logo192.png"

***但是,如果我查看日志,有些请求之后我可以获得有效的认证:

调试 2022-01-17 12:31:03.945 IST “将SecurityContextHolder设置为SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]” 调试

2022-01-17 12:31:03.945 IST “检索到SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]” 调试

2022-01-17 12:31:03.945 IST “检索到SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=com..security.oauth.CustomOAuth2User@, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=***, SessionId=9438C880A19C93AADJI206B9B8B3386], Granted Authorities=[ROLE_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]]]” 默认

2022-01-17 12:31:03.944 IST 2022-01-17 10:31:03.944 DEBUG [080-exec-8] o.s.security.web.FilterChainProxy - 保护GET /auth/api/getBasicInfo

看起来会话ID不一致


我使用Spring Security内置的OAuth2社交登录选项, 我实现了一个OAuth2LoginSuccess类,并在其中实现了onAuthenticationSuccess方法。在该方法中,我获取与从OAuth中获得的社交ID相对应的用户:

CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
int sociald = oAuth2User.getAttribute("id");
User user = usersUtils.getUserBySocailId(socialId);
enter code here
// add the user details to the Auth
SecurityContextHolder.clearContext();
((OAuth2AuthenticationToken) authentication).setDetails(user);
SecurityContextHolder.getContext().setAuthentication(authentication);

如果我在onAuthenticationSuccess中进行调试,我可以看到一个有效的认证和所有用户详细信息。

登录后,我会重定向到主页,并向服务器发送身份验证请求以检查是否有已登录的用户。

问题在于,有50%的情况下请求成功完成,用户可以进行经过身份验证的请求。

但是另外50%的情况下,我会自动重定向到登录页面,当我检查日志时,Spring Boot会告诉我用户未经过身份验证且认证已丢失。

但在onAuthenticationSuccess中,我始终可以看到正确的认证。

我的ApplicationSecurityConfig如下:

    http.csrf().disable().authorizeRequests()
            .antMatchers("/login*", "/signin/**", "/signup/**", "/oauth2/**").permitAll()
            .antMatchers(Constants.ADMIN_PREFIX + "/**").hasRole("ADMIN")
            .antMatchers(Constants.AUTH_PREFIX + "/**").hasAnyRole("ADMIN", "USER")
            .antMatchers(Constants.PUBLIC_PREFIX + "/**").permitAll()
            .anyRequest().permitAll()
            .and()
            .exceptionHandling().authenticationEntryPoint(new UnauthenticatedRequestHandler())
            .and()
            .formLogin()
            .passwordParameter("password")
            .usernameParameter("email")
            .loginPage("/Login")
            .loginProcessingUrl("/loginSecure").permitAll().successHandler(new LoginSuccess()).failureHandler(new FailureSuccess())
            .and()
            .oauth2Login()
            .loginPage("/Login")
            .userInfoEndpoint()
            .userService(oAuth2UserService)
            .and()
            .successHandler(new OAuth2LoginSuccess())
            .and()
            .rememberMe()
            .rememberMeParameter("remember-me")
            .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(21))
.userDetailsService(this.applicationUserService)
            .and()
            .logout()
         .clearAuthentication(true).invalidateHttpSession(true).logoutSuccessUrl("/login")
            .addLogoutHandler(new CustomLogOutHandler());

这是我检查用户是否已登录的函数:

   @GetMapping(Constants.AUTH_PREFIX + "/checkUserLogged")
public Integer checkUserLogged(Authentication authentication,HttpServletRequest request) {
    try{
        if (authentication != null) {
            User (User) authentication.getDetails();
            if (user == null) {
                return -1;
            }
            return user.getId();
        }
    }
    catch (Exception e){
        logger.warning(e.getLocalizedMessage());
    }
    return -1;
}

但是当问题出现时,由于Spring Security先返回未授权的错误信息,所以无法运行控制器。

非常感谢您的帮助。


你能添加日志吗? - Anand Varkey Philips
日志只是说没有经过身份验证的用户。 - MLugo
1
Spring Security将认证上下文保存在用户会话中,因此可能出现了一些问题。无论如何,请为org.springframework.security打开调试日志记录,这应该可以让您知道出了什么问题。 - stringy05
我已将完整日志添加到问题主体中,似乎会话 ID 在请求之间丢失。 - MLugo
保持OAuth2会话不奇怪吗?我更喜欢无状态服务。如果您想在OAuth2中保留JSessionId,可能会出现问题。状态可以存储在JWT或数据库中。您可以从Json中读取用户名并将其加载到SecurityContextHolder中。 - Gurkan İlleez
显示剩余2条评论
2个回答

1
我找到了解决方案,希望这能有所帮助。
对我造成问题的原因是GCP和GAE使用多个服务器实例,如果用户登录于某个实例,则其他实例也不会熟悉它,因为Spring HTTPSession是在内存中的。
我将会话平台切换为使用spring-session jdbc,并在application.properties中使用以下配置:
spring.session.store-type=jdbc

-- 只要会话存储在所有实例之间的共享位置中,您可以使用 Redis 替代 JDBC。
此外,还需要将事务管理器添加到 SecurtityConfig 中:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

并添加了以下配置:

    http.csrf().disable()
            .sessionManagement()
            .maximumSessions(1)
            .and()
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)

此外,就像@stringy05提到的那样,authrizenClient存储库也需要更新:
    /**
 * Use the servlet container session store for authorized OAuth2 Clients
 */
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
    return new HttpSessionOAuth2AuthorizedClientRepository();
}

并将 .authorizedClientRepository 行添加到 httpconfig 中:

....
                .oauth2Login()
            .loginPage("/Login")
            .authorizedClientRepository(authorizedClientRepository)
            .authorizationEndpoint().and()
            .userInfoEndpoint()
            .userService(oAuth2UserService)
            .and()
            .successHandler(new OAuth2LoginSuccess())

关于GAE,我在app.yaml文件中添加了以下行:
  network:
    session_affinity: true

1

这并不是一个答案,但对于评论而言太长了。

看起来会话出现了某些问题,一定要关注这个问题。

在默认的Spring Boot配置中,会话由底层的servlet容器管理,因此值得检查它是否正常运行。需要检查的内容包括:

  • 您是否正在运行多个应用服务器节点?如果是,请确保会话使用一些集群感知配置(例如Redis / JDBC),否则本地会话肯定会失败
  • 值得检查使用Spring Boot进行OAuth2登录时的默认设置。例如,您可以尝试使用HttpSessionOAuth2AuthorizedClientRepositorySpringSessionBackedSessionRegistry指定OAuth2会话

基本上启用所有日志,尝试观察当出现问题时从servlet容器中的会话状态。

正确地使oauth2会话工作可能并不容易,特别是考虑到没有多少好的博客/文档描述Spring Boot在做什么。

话虽如此,下面是一个使用Redis支持的Spring Boot配置和OAuth 2登录的示例,作为您参考时可能有用的:

应用配置:

spring:
  session:
    store-type: redis
    redis:
      namespace: sample:api
      flush-mode: immediate
  redis:
    host: localhost
    port: 6379
  security:
    oauth2:
      client:
        registration:
# add your oauth2 client details here 

public class SecurityConfig<S extends Session> extends WebSecurityConfigurerAdapter {

    private final ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    private RedisIndexedSessionRepository redisIndexedSessionRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors(Customizer.withDefaults())
                .sessionManagement()
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .and()
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                .and()
                .authorizeRequests(
                        a -> a.antMatchers("/api/login/callback").permitAll().anyRequest().authenticated())
                .oauth2Login()
                .authorizationEndpoint()
                .authorizationRequestResolver(
                        new DefaultOauth2AuthorizationRequestResolver(
                                this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI))
                .and()
                .defaultSuccessUrl("http://localhost:3000/users")
                .authorizedClientRepository(authorizedClientRepository());
    }

    @Bean
    public SpringSessionBackedSessionRegistry<?> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(this.redisIndexedSessionRepository);
    }

    /**
     * Use the servlet container session store for authorized OAuth2 Clients
     */
    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository() {
        return new HttpSessionOAuth2AuthorizedClientRepository();
    }

    /**
     * specify CORS to work from SPA UI
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(List.of("http://localhost:3000*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Configuration
    public static class HttpSessionEventPublisherConfig {

        /**
         * enables session expiry notification
         *
         * <p>Needs to be declared in a different class from the `SpringSessionBackedSessionRegistry` to
         * avoid a circular dependency
         */
        @Bean
        public HttpSessionEventPublisher httpSessionEventPublisher() {
            return new HttpSessionEventPublisher();
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接