Spring Security从单体应用到分布式(四)-基于Zuul的Oauth2应用

1.前言

一个正常的授权码模式如下

(A)用户打开客户端以后,客户端要求用户给予授权。

(B)用户同意给予客户端授权。

(C)客户端使用上一步获得的授权,向认证服务器申请令牌。

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

(E)客户端使用令牌,向资源服务器申请获取资源。

(F)资源服务器确认令牌无误,同意向客户端开放资源。

下图为一个客户信息请求流向图,客户的访问通过Zuul往后进行动态路由进而访问对应的资源服务,本次要通过要Zuul结合Oauth,实现分布式服务的鉴权控制

 

2.时序图

本次需要通过使用@EnableOAuth2Sso,实现Zuul服务自动进行执行命令牌模式,下图为对应时序图

3.依赖及配置

各服务作用

ekserver:注册服务中心

zuulserver:动态路由

authserver:登录信息资源服务&鉴权服务

3.1

Nginx要对微服务框架进行配置,所有信息访问流统一往网关访问,通过网关进行反向代理

    #定义一个upstream块
    upstream nginxtest{
       server 127.0.0.1:1112;#指向网关端口
    }
   server {
        listen       8080;
        #设置方向代理拦截的路径
        location ~ ^/api/? {
			proxy_pass http://nginxtest;
		}
        
        location ~ ^/oauthserver/? {
			proxy_pass http://nginxtest;
		}
   }

3.2 zuulserver配置

服务配置

security:
  oauth2:
    client:
      #配置自动访问相关路径
      clientId: demoApp
      clientSecret: demoAppSecret
      accessTokenUri: http://localcloudoauth:8080/oauthserver/oauth/token
      userAuthorizationUri: http://localcloudoauth:8080/oauthserver/oauth/authorize
      preEstablishedRedirectUri: http://localcloudoauth:8080/api/sso #oauth2restemplate发现没有access的时候需要跳转的地方
      useCurrentUri: false
    resource:
      #获取已登录权限的配置
      id: resourece
      loadBalanced: true
      userInfoUri: http://oauthserver/oauthserver/userinfo #获取用户信息的resttemplate地址 注:因此服务名需要使用对应服务的applicationname
      preferTokenInfo: false

EnableOAuth2Sso虽然能帮助我们注入SSO服务器的相关配置,但是要实现我们这篇文章的内容我们需要定制自己的配置,我们要对所有/api/**的接口进行鉴权,并且追加我们订制的过滤器。

    /**
     * 定制我们自己的oauth2客户端拦截器器
     * 我们只对/api/**的访问进行鉴权
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilterAfter(createoauth2ClientAuthenticationProcessingFilter(),
                AbstractPreAuthenticatedProcessingFilter.class);
        http.authorizeRequests().antMatchers("/api/**").
                access("isAuthenticated()");
    }

自定义OAuth2ClientAuthenticationProcessingFilter通过配置/api/sso这个api接口名来识别并拦截请求,并且建立实际拉取用户信息的UserInfoTokenServices,当遇到來至/api/sso的请求的时候去获取认证信息,并且把认证信息设置到SecurityContext当中

    /**
     * 避免个性化配置初始BEAN循环
     * @return
     */
    @Bean
    OAuth2ClientAuthenticationProcessingFilter createoauth2ClientAuthenticationProcessingFilter() {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
                "/api/sso");
        UserInfoTokenServices userInfoTokenServices = new UserInfoTokenServices(resourceServerProperties.getUserInfoUri(),
                clientId);
        userInfoTokenServices.setRestTemplate(oauth2RestTemplate);
        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("http://localcloudoauth:8080/oauthserver/helloworld"));
        filter.setTokenServices(userInfoTokenServices);
        filter.setRestTemplate(oauth2RestTemplate);
        return filter;
    }

3.3 oauthserver配置

oauthserver在配置成为认证服务器的同时,同时设置该服务为资源服务器,认证后的用户信息保存在该服务上,虽然使用了内存进行信息的保存,但是因为在在同一个服务中,所以能读取到保存在内存的人员信息。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private InMemoryTokenStore inMemoryTokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/userinfo").authorizeRequests().anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(inMemoryTokenStore);
        resources.resourceId("resourece").tokenServices(defaultTokenServices);
    }

}

 设置资源服务自己的cookiename避免不同服务之间的session使用同一名称导致的某个服务的session失效

    @Bean
    public CookieSerializer httpSessionIdResolver() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setCookiePath("/oauthserver");
        cookieSerializer.setCookieName("resourcesession");
        return cookieSerializer;
    }

4.展示

因为在配置中开放了home的访问,所以能直接得到结果

当访问http://localcloudoauth:8080/oauthserver/helloworld的时候因为oauthserver服务只开放了home和loginpage的url访问开放因此会自动转向到登录页面

让网关进行sso登录后,我们通过网关访问oauthserver/helloworld

5.源码分析

5.1 AuthorizationEndpoint,如何使sso过程不需要进行人手认可

在命令牌模式当中,需要通过用户手动认可数据,作为SSO登录,只要输入用户名和密码后,SSO服务器就能闭环完成整个命令牌模式的登录,在源码中可知是否对用户提供认证页面通过以下代码来做判断,其中responseTypes
是必定是code的,所以我们只能通过如何修改authorizationRequest.isApproved()来实现自动认证

AuthorizationEndpoint

if (authorizationRequest.isApproved()) //是否需要认证
{
  if (responseTypes.contains("token")) {
    return getImplicitGrantResponse(authorizationRequest);
  }
  if (responseTypes.contains("code")) {
    return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
        (Authentication) principal));
  }
}

其中,通过注释我们可知一些系统允许默认已认证,可以通过请求来设置approval的值来自动认可请求

// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
    (Authentication) principal);  

从代码可知当请求requestedScopes中的scope的和认证服务器中的AutoApprove存储的scope对应的时候就认为该次请求已认可不需要手动认可

ApprovalStoreUserApprovalHandler

ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
for (String scope : requestedScopes) {
  if (client.isAutoApprove(scope)) {
    approvedScopes.add(scope);
  }
}
if (approvedScopes.containsAll(requestedScopes)) {
  // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store.
  Set<Approval> approvals = new HashSet<Approval>();
  Date expiry = computeExpiry();
  for (String approvedScope : approvedScopes) {
    approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(),
        approvedScope, expiry, ApprovalStatus.APPROVED));
  }
  approvalStore.addApprovals(approvals);

  authorizationRequest.setApproved(true);
  return authorizationRequest;
}

因此在设置认证服务器的时候需要多给autoApprove添加合适的scope,.autoApprove("all")

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()//数据存在内存中
                .withClient("demoApp")//授权服务器id
                .secret("demoAppSecret")//授权密码
                .authorizedGrantTypes("authorization_code", "password", "refresh_token")//获取模式
                .scopes("all")
                .resourceIds("rest_api")//资源服务器id
                .accessTokenValiditySeconds(1200)//token的存在时间
                .autoApprove("all")
                .refreshTokenValiditySeconds(50000);//刷新token的token的存在时间
    }

5.1 OAuth2ClientAuthenticationProcessingFilter,如何完成命令牌模式

当过滤器发现请求是我们定义的路径之后,就会进行拦截并且往下调用。

OAuth2ClientAuthenticationProcessingFilter

	protected boolean requiresAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		return requiresAuthenticationRequestMatcher.matches(request);
	}

若果之前没有获取acesstoken的话,会抛出UserRedirectRequiredException,并且被OAuth2ClientContextFilter捕获,当发现抛出异常是UserRedirectRequiredException时会进行跳转。

OAuth2RestTemplate

	public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {

		OAuth2AccessToken accessToken = context.getAccessToken();

		if (accessToken == null || accessToken.isExpired()) {
			try {
				accessToken = acquireAccessToken(context);
			}
			catch (UserRedirectRequiredException e) {
				context.setAccessToken(null); // No point hanging onto it now
				accessToken = null;
				String stateKey = e.getStateKey();
				if (stateKey != null) {
					Object stateToPreserve = e.getStateToPreserve();
					if (stateToPreserve == null) {
						stateToPreserve = "NONE";
					}
					context.setPreservedState(stateKey, stateToPreserve);
				}
				throw e;
			}
		}
		return accessToken;
	}

OAuth2ClientContextFilter

Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
					.getFirstThrowableOfType(
							UserRedirectRequiredException.class, causeChain);
			if (redirect != null) {
				redirectUser(redirect, request, response);
			} else {
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}
				throw new NestedServletException("Unhandled exception", ex);
			}

当通过/oauth/authorize访问后,再次访问/api/sso的时候就能通过OAuth2RestTemplate访问/oauth/token获取到acesstoken,最后访问security.resource.userInfoUri获取到用户信息形成,形成授权请求。

结语

值得注意的是,本次我使用的是inMemory方式来保存用户信息和token,所以资源服务器和认证服务器必须在同一个服务中,因此当前实现只有一个资源服务器提供资源,可以通过使用其他存储形式来真正实现多个服务下Spring Security的鉴权控制。

参考: 源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
引用: 理解OAuth 2.0

github:https://github.com/tale2009/spring-security-learning/tree/master/cloudsecurity

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值