spring Security oAuth2例子分析

oauth2

參考:
1.https://tools.ietf.org/html/rfc6749
2.http://projects.spring.io/spring-security-oauth/docs/oauth2.html

基於spring-security-oauth2,從https://github.com/spring-projects/spring-security-oauth/tree/master/samples/oauth2抽取出來的源碼,父pom的不繼承artifactId>spring-security-oauth-parent,主要是脫離spring boot獨立出來的oauth2

在這個例子中的授權服務端和資源服務端是在同一個應用服務器.
一.在客戶端tonr2:
1.使用OAuth2RestTemplate(即org.springframework.security.oauth.examples.config.WebMvcConfig.ResourceConfiguration.sparklrRestTemplate)向sparklr2發http://localhost:8080/sparklr2/photos?format=xml請求.
2.經org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter過濾,然後正常執行請求前獲取不到accessToken,拋異常給org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter的Catch處理,這裏進行會進行跳轉redirectUser(redirect, request, response);然後再到this.redirectStrategy.sendRedirect(request, response, builder.build().encode().toUriString());這裏再向sparklr2發http://localhost:8080/sparklr2/oauth/authorize?client_id=tonr&redirect_uri=http://localhost:8081/tonr2/sparklr/photos&response_type=code&scope=read%20write&state=1DvnAt這樣的請求,也就是從這裏開始獲取授權碼.

二.轉到服務端sparklr2
3.經過spring security的org.springframework.security.web.FilterChainProxy過濾,用戶沒登錄,將用戶導向登錄頁面登錄,登錄完成後繼續跳轉到之前的獲取授權碼請求org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize,然後接受請求.以下兩行代碼判斷用戶是否授權給客戶端.

authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
//如果用戶授權給了客戶端
if (authorizationRequest.isApproved()) {
    if (responseTypes.contains("token")) {
        return getImplicitGrantResponse(authorizationRequest);
    }
    //直接響應獲取授權碼
    if (responseTypes.contains("code")) {
        return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                (Authentication) principal));
    }
}
//否則還要導向用戶到 授權給客戶端界面.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

4.假設用戶還沒授權過給客戶端,用戶在界面選擇是否授權並提交,org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny再接收請求,然後再響應獲取授權碼(當然用戶都拒絕授權所有權限就會拋UserDeniedAuthorizationException異常,或者正常生成授權碼),再根據回調url回到客戶端.

三.獲取授權碼的響應回到客戶端
5.回到org.springframework.security.oauth.examples.tonr.impl.SparklrServiceImpl.getSparklrPhotoIds再次發請求,此時又調用了sparklrRestTemplate,於是會再次調用org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken
這個方法會判斷accessToken爲null時會調用acquireAccessToken(OAuth2ClientContext oauth2Context)方法,

accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
if (accessToken == null || accessToken.getValue() == null) {
    throw new IllegalStateException(
            "Access token provider returned a null access token, which is illegal according to the contract.");
}
oauth2Context.setAccessToken(accessToken);

調用org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken的accessToken = obtainNewAccessTokenInternal(resource, request);
調用org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal的return tokenProvider.obtainAccessToken(details, request);
調用org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken的return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),getHeadersForTokenRequest(request));
調用org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken的return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());這時就會向sparklr2發起獲取accessToken的請求http://localhost:8080/sparklr2/oauth/token這裏發的是POST請求,參數都在form裏面的.

四.再次向服務端獲取accessToken
org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken接收請求,處理生成accessToken(是一個UUID,實際上包含的授權信息還是在服務端,只是這個UUID會對應Authentication),
這個例子生成accessToken在org.springframework.security.oauth2.provider.token.DefaultTokenServices.createAccessToken(org.springframework.security.oauth2.provider.OAuth2Authentication, org.springframework.security.oauth2.common.OAuth2RefreshToken)
然後調用tokenStore.storeAccessToken(accessToken, authentication);保存到服務端,這裏的tokenStore使用InMemoryTokenStore實現.(用戶的認證信息可以保存到redis來將資源服務器和授權服務器分離,springDataRedis又提供了方便,redis增加了集羣,如果可靠,就沒必要持久化到數據庫了)

五.獲取accessToken的響應回到客戶端
org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken這個方法會將得到的accessToken保存到OAuth2ClientContext.以後用戶用這個accessToken來訪問受保護的資源(直接訪問資源服務端,當然這裏授權服務端和資源服務端連在一起)就可以了.

六.訪問受資源服務端保護的資源(前面沒有特別說明的服務端都是指授權服務端)
1.先看看客戶端再向資源服務端發起請求org.springframework.security.oauth.examples.tonr.impl.SparklrServiceImpl.getSparklrPhotoIds的sparklrRestTemplate.getForObject(URI.create(sparklrPhotoListURL), byte[].class)
2.資源服務端接受請求org.springframework.security.oauth.examples.sparklr.mvc.PhotoController.getPhoto,進入這個方法之前肯定要做驗證的

先看一下代理攔截鏈springSecurityFilterChain這個最重要的過濾器的產生過程.
1.@EnableWebSecurity–>@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class})–>在實例化org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration這個bean過程當中,會先裝配org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#setFilterChainProxySecurityConfigurer,其中這個方法的第二個參考又會從當前的beanFactory獲取所有的SecurityConfigurer.
因爲sparklr2的授權服務端和資源服務端混在一起,再加上我們一般的Security自定義有一套,就產生了三套SecurityConfigurer,在這個方法排序後,經過webSecurity.apply(webSecurityConfigurer),這些SecurityConfigurer就保存此webSecurity的configurers(org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#configurers)這個變量當中.
a.org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration$$EnhancerBySpringCGLIB$$6a283588
b.org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration$$EnhancerBySpringCGLIB$$54898551
c.org.springframework.security.oauth.examples.sparklr.config.SecurityConfiguration$$EnhancerBySpringCGLIB$$4bd7839
這裏可以看出先授權,再資源,最後自定義那一套.

2.springSecurityFilterChain這個bean是在org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain方法聲明的,當實例化時,就會調用webSecurity.build();看一下構建過程

    @Override
    protected final O doBuild() throws Exception {
        synchronized(configurers) {
            buildState = BuildState.INITIALIZING;
            beforeInit();//提供全部configurers初始化的插入回調
            init();//調用每個configurer的初始化:從每個configurer拿到對應的HttpSecurity放到這個webSecurity的securityFilterChainBuilders;並把一個Runnable放到這個webSecurity的postBuildAction,作用是爲這個webSecurity設置FilterSecurityInterceptor攔截器

            buildState = BuildState.CONFIGURING;
            beforeConfigure();//和上面一樣提供回調
            configure();//調用每個configurer的configure(WebSecurity web)方法,主要是提供對這個webSecurity再做一些設置或說修改.比如在org.springframework.security.oauth.examples.sparklr.config.SecurityConfiguration#configure(org.springframework.security.config.annotation.web.builders.WebSecurity)就可以設置忽略那些請求.

            buildState = BuildState.BUILDING;
            O result = performBuild();//看下面分解

            buildState = BuildState.BUILT;
            return result;
        }
    }

    protected Filter performBuild() throws Exception {
        Assert.state(!securityFilterChainBuilders.isEmpty(),
                "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke "
                        + WebSecurity.class.getSimpleName()
                        + ".addSecurityFilterChainBuilder directly");
        int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
        List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(chainSize);
        for(RequestMatcher ignoredRequest : ignoredRequests) {//先添加忽略請求
            securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));//這個DefaultSecurityFilterChain裏面的過濾器爲空,這樣就達到不攔截的效果
        }
        for(SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {//就是上面init過程中的三套HttpSecurity
            securityFilterChains.add(securityFilterChainBuilder.build());//一個SecurityFilterChain包含兩個方法:a是否支持這個請求;b.如果支持,得到的過濾器來處理這個請求.這裏主要就是針對不同的請求,添加不同的過濾器形成一個SecurityFilterChain。
        }
        FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);//最終使用了所有的securityFilterChains構成這個FilterChainProxy
        if(httpFirewall != null) {
            filterChainProxy.setFirewall(httpFirewall);
        }
        filterChainProxy.afterPropertiesSet();

        Filter result = filterChainProxy;//聲明另一個引用來指向它.不多餘麼...應該是爲了下面的調試再包裝
        if(debugEnabled) {
            logger.warn("\n\n" +
                    "********************************************************************\n" +
                    "**********        Security debugging is enabled.       *************\n" +
                    "**********    This may include sensitive information.  *************\n" +
                    "**********      Do not use in a production system!     *************\n" +
                    "********************************************************************\n\n");
            result = new DebugFilter(filterChainProxy);
        }
        postBuildAction.run();//這裏就是上面在init過程時設的那個Runaable.作用是爲這個webSecurity設置FilterSecurityInterceptor攔截器
        return result;//返回這個最終的filterChainProxy
    }

情況一:下面假設用戶沒獲取accessToken,直接訪問/sparklr2/photos?format=xml會是什麼情況
由前面的分析,客戶端會先經org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter#doFilter處理,先拋org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval處理,轉而跳轉發授權碼請求,
再進入授權服務端,理應由第一套HttpSecurity的配置起作用.經調式,最後經一個FilterSecurityInterceptor Filter攔截
a.org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter的invoke(fi);
b.org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoker的InterceptorStatusToken token = super.beforeInvocation(fi);
c.org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation的this.accessDecisionManager.decide(authenticated, object, attributes);
又回到了熟悉的三者.authenticated爲AnonymousAuthenticationToken的一個實例,FilterInvocation的一個實例,attributes爲裝有WebExpressionConfigAttribute的數組,經過這方法一判斷,就會拋org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval,進而導向用戶到登錄界面.

情況二:下面假設用戶沒獲取accessToken,直接訪問/sparklr2/photos?format=xml會是什麼情況
再進入資源服務端,理應由第二套HttpSecurity的配置起作用.經調式,最後經一個FilterSecurityInterceptor Filter攔截
a.org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter的invoke(fi);
b.org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoker的InterceptorStatusToken token = super.beforeInvocation(fi);
c.org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation的this.accessDecisionManager.decide(authenticated, object, attributes);
又回到了熟悉的三者.authenticated爲AnonymousAuthenticationToken的一個實例,FilterInvocation的一個實例,attributes爲裝有WebExpressionConfigAttribute的數組,經過這方法一判斷,就會拋org.springframework.security.access.AccessDeniedException: Insufficient scope for this resource

情況三:走正常流程,用戶獲取完accessToken再訪問受保護資源的跟蹤,發起請求GET http://localhost:8080/sparklr2/photos?format=xml(OAuth2RestTemplate的context有保存accessToken,發請求時將這個accessToken放進了請求頭)
再進入資源服務端,就是第二套HttpSecurity的配置起作用.先看看在資源服務端啓動的時候,會調用
org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild的securityFilterChains.add(securityFilterChainBuilder.build());
當securityFilterChainBuilder爲資源服務端的那套HttpSecurity進入

org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure
private void configure() throws Exception {
    Collection<SecurityConfigurer<O,B>> configurers = getConfigurers();

    for(SecurityConfigurer<O,B> configurer : configurers ) {
        configurer.configure((B) this);
    }
}

這裏獲取有一個configurer爲org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer,進入它的configure

@Override
public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
    //這裏會有這樣一個比較重要的Filter,後面會提到
    resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
    resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
    resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
    if (eventPublisher != null) {
        resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
    }
    if (tokenExtractor != null) {
        resourcesServerFilter.setTokenExtractor(tokenExtractor);
    }
    resourcesServerFilter = postProcess(resourcesServerFilter);
    resourcesServerFilter.setStateless(stateless);
    // @formatter:off
    http
        .authorizeRequests().expressionHandler(expressionHandler)
    .and()
    //這裏加入到過濾鏈
        .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
        .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)
            .authenticationEntryPoint(authenticationEntryPoint);
    // @formatter:on
}

從上面可知也生成了一個OAuth2AuthenticationProcessingFilter,它用於將用戶傳過來的token,從存儲找回用戶的Authentication,下面跟蹤進入它的doFilter方法
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter.doFilter
//從請求獲取accessToken,即那個UUID.並實例化爲PreAuthenticatedAuthenticationToken對象
Authentication authentication = tokenExtractor.extract(request);
//authenticationManager爲OAuth2AuthenticationManager的實例,它會調用OAuth2Authentication auth = tokenServices.loadAuthentication(token);
Authentication authResult = authenticationManager.authenticate(authentication);
//將Authentication存到spring security的上下文.以供後續使用
SecurityContextHolder.getContext().setAuthentication(authResult);
因爲還生成了FilterSecurityInterceptor Filter,經過這個Filter再次回到this.accessDecisionManager.decide(authenticated, object, attributes);
authenticated:使用authentication = authenticationManager.authenticate(authentication);
object:FilterInvocation
attributes:ArrayList[0].WebExpressionConfigAttribute.SpelExpression.expression的值#oauth2.throwOnError(#oauth2.hasScope(‘read’) or (!#oauth2.isOAuth() and hasRole(‘ROLE_USER’)))
如果驗證碼過期這種情況又會怎樣?不想再跟蹤了,這個例子還有很多單元測試.本文我在調試過程中我沒有使用SSL.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章