Spring 启动,OAuth2 身份验证在请求之间丢失
Spring Boot, OAuth2 authentication is lost between requests
编辑:
日志来自 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
看来session id不一致
我使用 spring 安全内置 oauth2 社交登录选项,
我使用 onAuthenticationSuccess 方法实现了一个 OAuth2LoginSuccess class,并在其中获取用户对应于我从 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 内部进行调试,我可以看到包含所有用户详细信息的有效身份验证。
登录后我重定向到主页,并向服务器发送 auth get 请求以检查是否有用户登录。
问题是 50% 的请求成功完成并且用户可以进行经过身份验证的请求。
但是另外 50% 我被自动重定向到登录页面,当我检查日志时看到 Spring 引导说用户未经身份验证并且身份验证丢失。
但是在 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 安全 return 之前未授权的错误。
提前感谢您的帮助
这不是答案,但是对于评论来说太长了..
看起来会话由于某种原因丢失了,一定要关注它。
在默认的 Spring 引导配置中,会话由底层 servlet 容器管理,因此值得检查它是否正常运行。要检查的事项:
- 您是否 运行 超过 1 个应用程序服务器节点?如果是这样,请确保会话使用某种集群感知配置(即 Redis / JDBC),本地会话肯定会在这里失败
- 值得在 Spring Boot 中使用 OAuth2 登录检查默认值。例如,您可以尝试使用
HttpSessionOAuth2AuthorizedClientRepository
和 SpringSessionBackedSessionRegistry
指定 OAuth2 会话
基本上启用所有日志并尝试在出现问题时从 servlet 容器中观察会话状态。
让 oauth2 会话正常工作并非易事,尤其是考虑到描述 spring 引导正在做什么的好的博客/文档并不多。
也就是说,这里有一个支持 Redis 的工作示例 Spring 使用 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();
}
}
我找到了解决方法,希望对您有所帮助。
对我造成问题的是 GCP 和 GAE 使用服务器的多个实例,如果用户登录到某个实例
并不意味着其他实例也熟悉它,因为 Spring HTTPSession 在内存中。
我使用 application.properties 中的以下配置将会话平台切换为使用 spring-会话 jdbc :
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
编辑:
日志来自 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
看来session id不一致
我使用 spring 安全内置 oauth2 社交登录选项, 我使用 onAuthenticationSuccess 方法实现了一个 OAuth2LoginSuccess class,并在其中获取用户对应于我从 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 内部进行调试,我可以看到包含所有用户详细信息的有效身份验证。
登录后我重定向到主页,并向服务器发送 auth get 请求以检查是否有用户登录。
问题是 50% 的请求成功完成并且用户可以进行经过身份验证的请求。
但是另外 50% 我被自动重定向到登录页面,当我检查日志时看到 Spring 引导说用户未经身份验证并且身份验证丢失。
但是在 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 安全 return 之前未授权的错误。
提前感谢您的帮助
这不是答案,但是对于评论来说太长了..
看起来会话由于某种原因丢失了,一定要关注它。
在默认的 Spring 引导配置中,会话由底层 servlet 容器管理,因此值得检查它是否正常运行。要检查的事项:
- 您是否 运行 超过 1 个应用程序服务器节点?如果是这样,请确保会话使用某种集群感知配置(即 Redis / JDBC),本地会话肯定会在这里失败
- 值得在 Spring Boot 中使用 OAuth2 登录检查默认值。例如,您可以尝试使用
HttpSessionOAuth2AuthorizedClientRepository
和SpringSessionBackedSessionRegistry
指定 OAuth2 会话
基本上启用所有日志并尝试在出现问题时从 servlet 容器中观察会话状态。
让 oauth2 会话正常工作并非易事,尤其是考虑到描述 spring 引导正在做什么的好的博客/文档并不多。
也就是说,这里有一个支持 Redis 的工作示例 Spring 使用 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();
}
}
我找到了解决方法,希望对您有所帮助。
对我造成问题的是 GCP 和 GAE 使用服务器的多个实例,如果用户登录到某个实例 并不意味着其他实例也熟悉它,因为 Spring HTTPSession 在内存中。
我使用 application.properties 中的以下配置将会话平台切换为使用 spring-会话 jdbc :
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