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.

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值