1

스프링 MVC 애플리케이션을 작성하고 스프링 보안 OAuth2로 보안을 설정하려고 노력 중이며 공급자가 Google입니다. 보안 및 양식 로그인없이 웹 앱을 사용할 수있었습니다. 그러나 Google에서 OAuth를 사용할 수는 없습니다. 비 스프링 시큐리티 앱으로 작업하기 위해 콜백 등을 얻을 수 있기 때문에 구글 앱 셋업은 괜찮습니다. OAuth2를 내가 OAuth2를 허가는받을 수있는 리디렉션 예외를 던질 작성한리디렉션 루프에서 스프링 보안 OAuth2 (google) 웹 앱

@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"); 
    } 
} 

사용자 정의 인증 필터를 다음과 같이 자원이 보호

<?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> 

다음과 같이

내 보안 설정입니다 다음과 같이

@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; 
    } 

필터 체인 시퀀스는 다음과 같습니다.

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입니다. 첫 번째 인증 호출의 일부로 Authentication 객체가 보안 컨텍스트에 채워 졌으므로 왜 사라지나요? 스프링 보안과 처음으로 작업 중이므로 초보자 실수가있을 수 있습니다.

답변

3

스프링 보안 구성 및 필터를 사용하여 마침내이 작업을 수행 할 수있었습니다. 중요한 변경 사항 몇 가지를 만들어야했습니다.

  • 사용하고있는 사용자 정의 필터 대신 표준 Spring OAuth2 필터 (org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter)를 사용했습니다.
  • 인증 필터의 가로 채기 URL을 /googleLogin으로 변경하고 인증 실패시이 URL로 리디렉션되는 인증 진입 점을 추가하십시오.

    • 브라우저 / 액세스 다음 컨텍스트가 일치하지 않기 때문에 요청이 OAuth2ClientContextFilterOAuth2ClientAuthenticationProcessingFilter를 통과

    전반적인 흐름이다. 로그인에 대해 구성된 컨텍스트 경로는 /googleLogin

  • 입니다. 보안 인터셉터 FilterSecurityInterceptor은 사용자가 익명임을 감지하고 액세스가 거부 된 예외를 throw합니다.
  • 스프링 보안의 ExceptionTranslationFilter은 액세스 거부 예외를 포착하고 구성된 인증 엔트리 포인트에 처리하여 /googleLogin으로 리디렉션하는 것을 요청합니다.
  • /googleLogin 요청의 경우 OAuth2AuthenticationProcessingFilter 필터는 Google 보호 리소스에 액세스하려고 시도하고 UserRedirectRequiredException이 전송되어 OAuth2ClientContextFilter의 HTTP 리디렉션 (OAuth2 세부 정보 포함)으로 변환됩니다.
  • Google의 인증 성공시 브라우저는 OAuth 코드로 /googleLogin으로 다시 연결됩니다.필터 OAuth2AuthenticationProcessingFilter이이를 처리하고 Authentication 개체를 만들고 SecurityContext을 업데이트합니다.
  • 이 시점에서 사용자는 완전히 인증되고 OAuth2AuthenticationProcessingFilter으로 리디렉션되거나 발급됩니다.
  • FilterSecurityInterceptorSecurityContext이 인증 된 Authentication object을 포함하므로 요청을 진행할 수 있습니다.
  • 마지막으로 isFullyAuthenticated() 또는 이와 비슷한 표현식을 사용하여 보안 된 응용 프로그램 페이지가 렌더링됩니다. 또한

    <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를 자원에 대해 다음과 같습니다 :

    @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)); 
        } 
    } 
    

    나는대로 봄 클래스의 일부를 대체 한 다음

보안 컨텍스트 XML이다 Google의 토큰 형식과 Spring에서 예상하는 형식이 일치하지 않습니다. 그래서 몇 가지 맞춤식 수공예품이 필요합니다.

+0

"Google의 토큰 형식과 Spring에서 예상하는 형식이 일치하지 않습니다." 그걸 조금 설명 할 수 있니? Spring은 토큰 값에 대한 가정을하지 않는다 (단지 불투명 한 문자열이다). –

+0

N.B. 2.0.3으로 업그레이드하고 'RestTemplate'에 대한 세션 범위 사용을 중단해야합니다. –

+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

관련 문제