深入理解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动态权限配置章节。

发布了37 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/new_com/article/details/104728471