重定向循环中的 Spring Security OAuth2 (google) web 应用程序
Posted
技术标签:
【中文标题】重定向循环中的 Spring Security OAuth2 (google) web 应用程序【英文标题】:Spring Security OAuth2 (google) web app in redirect loop 【发布时间】:2014-11-21 07:13:06 【问题描述】:我正在尝试构建一个 Spring MVC 应用程序并使用 Spring Security OAuth2 保护它,并且提供者是 Google。我能够让网络应用程序在没有安全性和表单登录的情况下工作。但是,我无法通过 google 获得 OAuth 工作。 Google 应用设置很好,因为我可以让回调等与非 Spring Security 应用一起使用。
我的安全配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:sec="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="googleAuthenticationFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<sec:authentication-manager alias="alternateAuthenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="user" password="password" authorities="DOMAIN_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
</b:beans>
OAuth2保护的资源如下:
@Configuration
@EnableOAuth2Client
class ResourceConfiguration
@Autowired
private Environment env;
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Bean
public OAuth2ProtectedResourceDetails googleResource()
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-app");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
private List<String> parseScopes(String commaSeparatedScopes)
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
@Bean
public OAuth2RestTemplate googleRestTemplate()
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
@Bean
public AbstractAuthenticationProcessingFilter googleAuthenticationFilter()
return new GoogleOAuthentication2Filter(new GoogleAppsDomainAuthenticationManager(), googleRestTemplate(), "https://accounts.google.com/o/oauth2/auth", "http://localhost:9000");
我编写的用于抛出重定向异常以获取 OAuth2 授权的自定义身份验证过滤器如下:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException
try
logger.info("OAuth2 Filter Triggered!! for path ", request.getRequestURI(), request.getRequestURL().toString());
logger.info("OAuth2 Filter hashCode request hashCode ", this.hashCode(), request.hashCode());
String code = request.getParameter("code");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
logger.info("Code is and authentication is ", code, authentication == null ? null : authentication.isAuthenticated());
// not authenticated
if (requiresRedirectForAuthentication(code))
URI authURI = new URI(googleAuthorizationUrl);
logger.info("Posting to to trigger auth redirect", authURI);
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from ", url);
// Should throw RedirectRequiredException
oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
// authentication in progress
return null;
else
logger.info("OAuth callback received");
// get user profile and prepare the authentication token object.
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from ", url);
ResponseEntity<GoogleProfile> forEntity = oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
GoogleProfile profile = forEntity.getBody();
CustomOAuth2AuthenticationToken authenticationToken = getOAuth2Token(profile.getEmail());
authenticationToken.setAuthenticated(false);
Authentication authenticate = getAuthenticationManager().authenticate(authenticationToken);
logger.info("Final authentication is ", authenticate == null ? null : authenticate.isAuthenticated());
return authenticate;
catch (URISyntaxException e)
Throwables.propagate(e);
return null;
来自 Spring Web 应用的过滤器链顺序如下:
o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'metricFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'oauth2ClientContextFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'googleOAuthFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.filterChainProxy' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'applicationContextIdFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'webRequestLoggingFilter' to: [/*]
重定向到 Google 工作正常,我收到了过滤器的回调,并且身份验证成功。但是在那之后,请求会导致重定向并再次调用过滤器(请求是相同的,我已经检查了 hasCode)。在第二次调用时,SecurityContext
中的身份验证是 null
。作为第一次身份验证调用的一部分,身份验证对象被填充到安全上下文中,那么它为什么会消失呢?
我是第一次使用 Spring Security,所以可能犯了新手错误。
【问题讨论】:
【参考方案1】:不是强加一个更好的答案,但此重定向循环的另一个原因是当会话 cookie 的 sameSite 属性设置为 Strict 时会话 cookie 处理。那么,如果授权服务中断了重定向链,则不会传输会话cookie。
见:How can I redirect after OAUTH2 with SameSite=Strict and still get my cookies?
【讨论】:
【参考方案2】:在玩弄了 Spring Security 配置和过滤器之后,我终于能够让它工作了。我必须做出一些重要的改变
我使用了标准 Spring OAuth2 过滤器 (org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter
),而不是我使用的自定义过滤器。
将身份验证过滤器的拦截 URL 更改为 /googleLogin
并添加一个身份验证入口点,该入口点在身份验证失败时重定向到此 URL。
整体流程如下
浏览器访问/
,由于上下文不匹配,请求通过OAuth2ClientContextFilter
和OAuth2ClientAuthenticationProcessingFilter
。配置的登录上下文路径为/googleLogin
安全拦截器FilterSecurityInterceptor
检测到用户是匿名用户并抛出拒绝访问异常。
Spring security 的ExceptionTranslationFilter
捕获访问被拒绝异常并要求配置的身份验证入口点处理它,从而重定向到/googleLogin
。
对于请求/googleLogin
,过滤器OAuth2AuthenticationProcessingFilter
尝试访问受Google 保护的资源,并抛出UserRedirectRequiredException
,OAuth2ClientContextFilter
将其转换为对Google 的HTTP 重定向(带有OAuth2 详细信息)。
从 Google 成功验证后,浏览器将使用 OAuth 代码重定向回 /googleLogin
。过滤器OAuth2AuthenticationProcessingFilter
处理此问题并创建Authentication
对象并更新SecurityContext
。
此时用户已完全通过身份验证并重定向到 / 由 OAuth2AuthenticationProcessingFilter
发出。
FilterSecurityInterceptor
允许请求继续进行,因为 SecurityContext
包含经过身份验证的 Authentication object
。
最终呈现使用isFullyAuthenticated()
或类似表达式保护的应用程序页面。
安全上下文xml如下:
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<!-- This is the crucial part and the wiring is very important -->
<!--
The order in which these filters execute are very important. oauth2ClientContextFilter must be invoked before
oAuth2AuthenticationProcessingFilter, that's because when a redirect to Google is required, oAuth2AuthenticationProcessingFilter
throws a UserRedirectException which the oauth2ClientContextFilter handles and generates a redirect request to Google.
Subsequently the response from Google is handled by the oAuth2AuthenticationProcessingFilter to populate the
Authentication object and stored in the SecurityContext
-->
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="oAuth2AuthenticationProcessingFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="oAuth2AuthenticationProcessingFilter" class="org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter">
<b:constructor-arg name="defaultFilterProcessesUrl" value="/googleLogin"/>
<b:property name="restTemplate" ref="googleRestTemplate"/>
<b:property name="tokenServices" ref="tokenServices"/>
</b:bean>
<!--
These token classes are mostly a clone of the Spring classes but have the structure modified so that the response
from Google can be handled.
-->
<b:bean id="tokenServices" class="com.rst.oauth2.google.security.GoogleTokenServices">
<b:property name="checkTokenEndpointUrl" value="https://www.googleapis.com/oauth2/v1/tokeninfo"/>
<b:property name="clientId" value="$google.client.id"/>
<b:property name="clientSecret" value="$google.client.secret"/>
<b:property name="accessTokenConverter">
<b:bean class="com.rst.oauth2.google.security.GoogleAccessTokenConverter">
<b:property name="userTokenConverter">
<b:bean class="com.rst.oauth2.google.security.DefaultUserAuthenticationConverter"/>
</b:property>
</b:bean>
</b:property>
</b:bean>
<!--
This authentication entry point is used for all the unauthenticated or unauthorised sessions to be directed to the
/googleLogin URL which is then intercepted by the oAuth2AuthenticationProcessingFilter to trigger authentication from
Google.
-->
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<b:property name="loginFormUrl" value="/googleLogin"/>
</b:bean>
OAuth2 资源的 Java 配置如下:
@Configuration
@EnableOAuth2Client
class OAuth2SecurityConfiguration
@Autowired
private Environment env;
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Bean
@Scope("session")
public OAuth2ProtectedResourceDetails googleResource()
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-oauth-client");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
private List<String> parseScopes(String commaSeparatedScopes)
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestTemplate googleRestTemplate()
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
我不得不重写一些 Spring 类,因为来自 Google 的令牌格式与 Spring 期望的格式不匹配。所以那里需要一些定制的手工。
【讨论】:
“来自 Google 的令牌格式与 Spring 预期的格式不匹配”。你能解释一下吗? Spring 不对令牌值做任何假设(它只是一个不透明的字符串)。 另请注意。您应该升级到 2.0.3 并停止使用RestTemplate
的会话范围。
@DaveSyer 有几个不同之处。在 checkToken 的响应中,Google 将“client_id”作为“issued_to”发送,“user_name”作为“user_id”发送。当令牌具有多个范围时,来自 Google 的响应具有由空格分隔的这些范围,而不是字符串集合。需要使用 scope.split(" ") 来提取不同的作用域。我有几个课程可以做到这一点here
我明白了,所以你创建了GoogleTokenServices
和GoogleAccessTokenConverter
?这就是全部?更详细地描述那些尝试做同样的事情的其他人可能会有所帮助(因为其余的是标准 Spring OAuth)。
@DaveSyer 将在答案中添加更多细节。我所做的另一件事是修复 DefaultUserAuthenticationConverter.java 中的错误。 here 有一个未完成的拉取请求以上是关于重定向循环中的 Spring Security OAuth2 (google) web 应用程序的主要内容,如果未能解决你的问题,请参考以下文章
由于重定向循环,Grails Spring Security 无法显示登录页面
使用Spring Security + CAS获得循环重定向,但应该正常工作
Spring Security:OAuth 在获取访问令牌之前陷入重定向循环,但初始页面旁边的任何页面都不受保护
如何在 Spring Security 中的 SSO 登录后重定向我的上一页?
java spring boot / spring security(HttpSecurity)中的会话到期时如何自动注销