1

我正在尝试构建Spring MVC应用程序,并使用Spring Security OAuth2来保护它,并且提供程序是Google。我能够在没有安全性和表单登录的情况下运行网络应用程序。不过,我无法使用谷歌的OAuth工作。谷歌应用程序设置是好的,因为我可以让回调等与非Spring安全应用程序一起工作。重定向循环中的Spring Security OAuth2(google)web应用程序

我的安全配置如下:

<?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: [/*] 

重定向到谷歌工作正常,我得到的回调过滤器和认证成功。然而之后,请求会产生重定向,并再次调用过滤器(请求相同,我已经检查过hasCode)。在第二次调用时,SecurityContext中的身份验证为空。作为第一次身份验证调用的一部分,身份验证对象已填充到安全上下文中,为何它会消失? 我正在与Spring Security第一次合作,所以可能已经犯了新手错误。

回答

3

在使用Spring Security配置和过滤器后,我终于能够得到这个工作。我不得不做出几个重要的更改

  • 我使用了标准Spring OAuth2筛选器(org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter)而不是我使用的自定义筛选器。
  • 将认证过滤器的拦截URL更改为/googleLogin,并在认证失败时添加了一个重定向到此URL的认证入口点。

总体流动是如下

  • 浏览器访问/和请求通过OAuth2ClientContextFilterOAuth2ClientAuthenticationProcessingFilter作为上下文不符。用于登录的已配置上下文路径是/googleLogin
  • 安全拦截器FilterSecurityInterceptor检测到用户是匿名的并引发访问被拒绝的异常。
  • Spring安全的ExceptionTranslationFilter捕获拒绝访问的异常,并要求配置的身份验证入口点对其进行处理,该问题重定向到/googleLogin
  • 对于请求/googleLogin,过滤器OAuth2AuthenticationProcessingFilter尝试访问Google受保护的资源,并且UserRedirectRequiredException被抛出,并被翻译成HTTP重定向到Google(与OAuth2细节)OAuth2ClientContextFilter
  • 成功通过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> 

而且在Java配置的OAuth2用户资源如下:

@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预期的格式不匹配。所以那里需要一些定制的手工。

+0

“来自Google的令牌格式和Spring预期的令牌格式不匹配”​​。你能解释一下吗? Spring对令牌值没有任何假设(它只是一个不透明的字符串)。 – 2014-10-03 08:18:37

+0

也N.B.你应该升级到2.0.3并停止使用'RestTemplate'的会话范围。 – 2014-10-03 08:19:21

+0

@DaveSyer有一些差异。在checkToken的响应中,Google将'client_id'作为'issued_to'和'user_name'发送为'user_id'。当令牌具有多个作用域时,Google的响应将这些作用域用空格分隔,而不是字符串的集合。不同的作用域需要使用scope.split(“”)来提取。我有几个这样做[这里]的类(https://github.com/skate056/spring-security-oauth2-google/blob/master/src/main/java/com/rst/oauth2/google/security /GoogleAccessTokenConverter.java) – Saket 2014-10-03 09:55:42