深入理解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,可以直接放回結果:

測試訪問"/coupon/demo",需要進行身份認證,並且帶有couponDemo權限纔可以訪問。在db中配置user(用戶),role(角色),permission(權限)的關係數據,配置了用戶username爲admin,password爲123456,擁有couponDemo的權限。首先獲取用戶的訪問授權碼access token,流程如下:

 

根據返回的訪問授權碼,訪問"/coupon/demo",成功返回測試數據,流程如下:

不足與優化之處

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

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