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

相關問題