天天看點

深入了解Spring Cloud Security OAuth2資源授權

OAuth2授權概述

在Spring Cloud Security 中,認證和授權都是通過FilterChainProxy(Servlet Filter過濾器)攔截然後進行操作的。在Spring Security中FilterSecurityInterceptor 過濾器會對資源受保護的Http請求進行攔截,然後進行授權處理。其部分源碼如下:

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {

	/**
	 * 授權過濾器攔截邏輯
	 */
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

    /**
	 * 設定權限資訊擷取服務FilterInvocationSecurityMetadataSource
	 */
	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}


	public void invoke(FilterInvocation fi) throws IOException, ServletException {

		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
                
            // 授權邏輯校驗
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
                // 調用下一個過濾器
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
                // 資源伺服器的通路
				super.finallyInvocation(token);
			}
            // 調用結束後的處理
			super.afterInvocation(token, null);
		}
	}

	/**
	 * Indicates whether once-per-request handling will be observed. By default this is
	 * <code>true</code>, meaning the <code>FilterSecurityInterceptor</code> will only
	 * execute once-per-request. Sometimes users may wish it to execute more than once per
	 * request, such as when JSP forwards are being used and filter security is desired on
	 * each included fragment of the HTTP request.
	 *
	 * @return <code>true</code> (the default) if once-per-request is honoured, otherwise
	 * <code>false</code> if <code>FilterSecurityInterceptor</code> will enforce
	 * authorizations for each and every fragment of the HTTP request.
	 */
	public boolean isObserveOncePerRequest() {
		return observeOncePerRequest;
	}

	public void setObserveOncePerRequest(boolean observeOncePerRequest) {
		this.observeOncePerRequest = observeOncePerRequest;
	}
}
           

FilterSecurityInterceptor 攔截處理的大緻流程如下:

  1. 處理授權邏輯校驗
  2. 調用餘下的過濾器
  3. 授權成功後,通路真正的資源伺服器請求。

在第一步授權邏輯的校驗邏輯中,調用的是在父類的AbstractSecurityInterceptor的beforeInvocation方法實作的,大緻流程如下:

  1. 使用SecurityMetadataSource根據http請求擷取對應擁有的權限。
  2. 使用Spring Security授權子產品對使用者通路的資源進行授權驗證。

AbstractSecurityInterceptor的部分源碼如下:

// AbstractSecurityInterceptor.java
	protected InterceptorStatusToken beforeInvocation(Object object) {
        ......

        // 根據http請求擷取對應的配置的權限資訊
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

	    ......
        // 對使用者認證進行校驗
		Authentication authenticated = authenticateIfRequired();
		try {
            // 對使用者的權限與通路資源擁有的權限進行校驗
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}
        ......
	}
           

資源伺服器配置權限擷取邏輯

在FilterSecurityInterceptor中FilterInvocationSecurityMetadataSource,用于擷取資源擁有的授權資訊。在其預設子類DefaultFilterInvocationSecurityMetadataSource 實作類中的源碼如下:

public class DefaultFilterInvocationSecurityMetadataSource implements
		FilterInvocationSecurityMetadataSource {

    /**
	 * 請求與擁有權限的映射
	 */
	private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;

    /**
	 * 擷取資源伺服器擁有的全部權限
	 */
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		Set<ConfigAttribute> allAttributes = new HashSet<>();

		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
				.entrySet()) {
			allAttributes.addAll(entry.getValue());
		}

		return allAttributes;
	}
   /**
	 * 根據請求擷取資源伺服器擁有的權限
	 */
	public Collection<ConfigAttribute> getAttributes(Object object) {
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
				.entrySet()) {
			if (entry.getKey().matches(request)) {
				return entry.getValue();
			}
		}
		return null;
	}

	public boolean supports(Class<?> clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}
}
           

授權處理邏輯

在Spring Security中,對于授權處理的邏輯,通過AccessDecisionManager接口實作的,源碼如下:

public interface AccessDecisionManager {

	/**
	 * authentication 認證以後擁有的權限
     * object 授權的Url資訊
     * configAttributes url路徑權限屬性
	 */
	void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
			InsufficientAuthenticationException;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);
}
           

AccessDecisionManager的實作類自定義授權邏輯的具體實作,其常見的實作類如下:

  • AffirmativeBased:隻要有一個授權處理通過則可以進行通路(預設使用的類)。
  • ConsensusBased:根據少數服務多數的原則進行判斷。
  • UnanimousBased:隻要有一個授權不通過,則不能通路。

AccessDecisionManager的公共子類AbstractAccessDecisionManager中包含一個AccessDecisionVoter清單,用于組合處理授權邏輯,AccessDecisionVoter接口定義如下:

public interface AccessDecisionVoter<S> {

    // 授權通過
	int ACCESS_GRANTED = 1;
    // 授權忽略
	int ACCESS_ABSTAIN = 0;
    // 授權拒絕
	int ACCESS_DENIED = -1;

	/**
	 * 支援的路徑授權屬性
	 */
	boolean supports(ConfigAttribute attribute);

	/**
	 *支援的類
	 */
	boolean supports(Class<?> clazz);

	/**
	 * 授權方法,authentication為使用者認證過後的認證資訊,object為url路徑資訊,attributes為路徑配置的授權資訊
	 */
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}
           

AccessDecisionVoter的常見的實作類如下:

  • RoleVoter(根據角色授權處理)
  • AuthenticatedVoter(認證授權處理)
  • webExpressionVoter(描述語言的授權處理)

資源伺服器的搭建

在Spring Cloud Security資源伺服器的搭建中,通過注解@EnableResourceServer開啟資源伺服器的預設配置,可以繼承ResourceServerConfigurerAdapter自定義資源伺服器的邏輯。主要有兩方面的配置:

  • ResourceServerSecurityConfigurer,用于配置資源伺服器的安全配置,例如,通路令牌的校驗。
  • HttpSecurity,用于配置資源伺服器授權邏輯。例如,擁有的權限配置,授權邏輯的自定義。

個人demo實作中,使用redis存儲token,RemoteTokenServices遠端服務調用方式通路認證伺服器的check_token方法,jwt方式進行token轉換,靜态的配置了通路資源的權限,源碼如下:

public class Oauth2ResourcesConfig extends ResourceServerConfigurerAdapter {

    /**
     * redis連接配接工廠
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    /**
     * jwt
     */
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     *
     */
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 資源伺服器安全配置
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {

        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setAccessTokenConverter(jwtAccessTokenConverter);
        tokenServices.setRestTemplate(restTemplate);
        tokenServices.setClientId("meituan");
        tokenServices.setClientSecret("123456");
        // TODO 本地啟動使用 RestTemplate通過服務名會通路80端口
        // tokenServices.setCheckTokenEndpointUrl("http://oauth2-server/oauth/check_token");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:8766/oauth/check_token");
        resources
                .resourceId("coupon")
                .tokenStore(new RedisTokenStore(redisConnectionFactory))
                .tokenServices(tokenServices)
                // 通路無狀态
                .stateless(true);
    }

    @Autowired
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * 資源伺服器内的資源通路控制
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {

        // session配置,微服務中配置為無狀态
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 授權配置
                .and().authorizeRequests()
                // 無需認證授權即可通路
                .antMatchers("/coupon/demo2", "/coupon/demo3").permitAll()
                // 角色設定
                .antMatchers("/user/**").hasAnyRole("user")
                // 權限設定
                .antMatchers("/coupon/demo").hasAuthority("couponDemo")
                // 剩餘所有請求都需要身份認證才能通路
                .anyRequest().authenticated();
    }

    /**
     * jwt token 配置
     */
    @Configuration
    public static class JwtTokenConfig {

        /**
         * JwtAccessTokenConverter
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey("kuqi-mall");
            return converter;
        }
    }
}
           

授權測試

測試無需授權的api "/coupon/demo2"和 "/coupon/demo3",直接通路"/coupon/demo2"和 "/coupon/demo3"位址,無需token,可以直接放回結果:

深入了解Spring Cloud Security OAuth2資源授權

測試通路"/coupon/demo",需要進行身份認證,并且帶有couponDemo權限才可以通路。在db中配置user(使用者),role(角色),permission(權限)的關系資料,配置了使用者username為admin,password為123456,擁有couponDemo的權限。首先擷取使用者的通路授權碼access token,流程如下:

深入了解Spring Cloud Security OAuth2資源授權

根據傳回的通路授權碼,通路"/coupon/demo",成功傳回測試資料,流程如下:

深入了解Spring Cloud Security OAuth2資源授權

不足與優化之處

Spring Cloud Security資源伺服器的授權處理,在以上的示例中屬于在靜态加載,在啟動資源服務時,會全部加到記憶體。在資源伺服器運作期間,如果需要修改資源擁有的權限,該如何處理呢?請關注後續的Spring Cloud Security動态權限配置章節。