SpringSecurity架构

从万米高空看Spring Security

如果站在一个很高的层次看Spring Security的架构的话,Spring的拦截器模型主要用于两个领域:Web 请求 和 方法调用。Spring Securityweb资源和应用程序方法通过过滤器和拦截器封装在其内部,只有满足特定的条件才能访问。Web 请求 和 方法调用的访问控制都是基于核心拦截器组件(SecurityInteceptor),拦截器组件包含了核心的访问控制逻辑。

站在Spring Security的塔尖

再近一点,你可以看到Spring Security的各个协作组件。期中包含:

1、用于配置的XML Namespace和加工之后的配置信息ConfigAtrribute

2、过滤器链中包含的各个过滤器

3、核心拦截器SecurityInteceptor的各个组件及其协作组件:AuthenticationManagerAuthenticationProviderUserDetailServiceAccessDecisionManager

4、用于存放SpringSecurity上下文的容器:SecurityContextHolder

5、用于存放用户认证信息的核心类:Authentication

6、用于访问控制的核心组件ACL

XML Namespace

Spring2.0版本开始支持自定义命名空间。自定义命名空间其实可以看做是一种语法糖,本质上是一种基于XMLDSL(Domain Specific Language)。在Spring框架中自定义XML命名空间也十分简单:

1、 定义一个`xsd`文件来描述`XML`文件的格式

2、在 `spring.schemas` 文件中描述`xsd`文件的`URL`地址与本地存储位置之间的对应关系

3、在 spring.handlers 文件中指定自定义命名空间的处理器`handler`(自己实现)

4、一组负责解析自定义命名空间中各个元素的`parser`(自己实现)

spring.schemasspring.handlers均在META-INF文件夹下,以Spring Security为例,说明:

spring.schemas文件中指定了本地xsd文件与URL之间对应关系:

spring.handlers文件中指定了命名空间的处理类:

SecurityNamespaceHandler实现了命名空间的解析:

public final class SecurityNamespaceHandler implements NamespaceHandler {
   public BeanDefinition parse(Element element, ParserContext pc) {     
      String name = pc.getDelegate().getLocalName(element);
      BeanDefinitionParser parser = parsers.get(name);
      if (parser == null) {
         // SEC-1455. Load parsers when required, not just on init().
         loadParsers();
      }
      if (parser == null) {
         if (Elements.HTTP.equals(name)
               || Elements.FILTER_SECURITY_METADATA_SOURCE.equals(name)
               || Elements.FILTER_CHAIN_MAP.equals(name)
               || Elements.FILTER_CHAIN.equals(name)) {
            reportMissingWebClasses(name, pc, element);
         }
         else {
            reportUnsupportedNodeType(name, pc, element);
         }

         return null;
      }
      return parser.parse(element, pc);
   }
   public void init() {
      loadParsers();
   }

   private void loadParsers() {
      // Parsers
      parsers.put(Elements.LDAP_PROVIDER, new LdapProviderBeanDefinitionParser());
      parsers.put(Elements.LDAP_SERVER, new LdapServerBeanDefinitionParser());
      parsers.put(Elements.LDAP_USER_SERVICE, new LdapUserServiceBeanDefinitionParser());
      parsers.put(Elements.USER_SERVICE, new UserServiceBeanDefinitionParser());
      parsers.put(Elements.JDBC_USER_SERVICE, new JdbcUserServiceBeanDefinitionParser());
      parsers.put(Elements.AUTHENTICATION_PROVIDER,
            new AuthenticationProviderBeanDefinitionParser());
      parsers.put(Elements.GLOBAL_METHOD_SECURITY,
            new GlobalMethodSecurityBeanDefinitionParser());
      parsers.put(Elements.AUTHENTICATION_MANAGER,
            new AuthenticationManagerBeanDefinitionParser());
      parsers.put(Elements.METHOD_SECURITY_METADATA_SOURCE,
            new MethodSecurityMetadataSourceBeanDefinitionParser());

      // Only load the web-namespace parsers if the web classes are available
      if (ClassUtils.isPresent(FILTER_CHAIN_PROXY_CLASSNAME, getClass()
            .getClassLoader())) {
         parsers.put(Elements.DEBUG, new DebugBeanDefinitionParser());
         parsers.put(Elements.HTTP, new HttpSecurityBeanDefinitionParser());
         parsers.put(Elements.HTTP_FIREWALL, new HttpFirewallBeanDefinitionParser());
         parsers.put(Elements.FILTER_SECURITY_METADATA_SOURCE,
               new FilterInvocationSecurityMetadataSourceParser());
         parsers.put(Elements.FILTER_CHAIN, new FilterChainBeanDefinitionParser());
         filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator();
      }

      if (ClassUtils.isPresent(MESSAGE_CLASSNAME, getClass().getClassLoader())) {
         parsers.put(Elements.WEBSOCKET_MESSAGE_BROKER,
               new WebSocketMessageBrokerSecurityBeanDefinitionParser());
      }
   } 
}

同时SecurityNamespaceHandler加载了一组Parser来具体负责各个元素的解析。

Spring Security启动时XML命名空间的加载过程:

Servlet Filters

Spring Security基于过滤器链模型保护Web应用的安全,该模型基于标准的Servlet Filter。过滤器链由一组单一职责的过滤器组成,每个过滤器负责一个功能,他们共同完成Web安全保护。过滤器链中的过滤器是一个个的Spring Bean,然而标准的Servlet应用是无法感知Spring Bean的。因此需要一个特殊的Servlet Filter跨越标准Servlet API 与Spring应用的边界,将二者联系起来。

DelegatingFilterProxy

DelegatingFilterProxySpring初始化后,从Spring容器中获取代理Bean,并将Filter的执行逻辑委托给该代理Bean,从而将标准ServletSpring应用连接起来.在Spring Security应用中,DelegatingFilterProxy在启动时查找名称为springSecurityFilterChainBean,该Bean正是由Spring启动时构建的过滤器链。

DelegatingFilterProxy是一个标准的Servlet Filter,他在Servlet容器启动时被加载。同时又是一个GenericFilterBean(该Bean的作用是将Filter中的initParameter赋值给Spring Bean属性),在Spring容器加载后将初始化参数赋值给Spring容器中的Bean,Bean的名称是:DelegatingFilterProxy作为Filter创建时指定的初始化参数targetName,默认为springSecurityFilterChain.

SpringSecurityFilterChain

Spring容器中beanNamespringSecurityFilterChainbean,是一个FilterChainProxy实例,该实例中包含Spring Security的各种过滤器,以满足应用使用。这些过滤器在Spring容器启动时根据配置完成初始化,每一个过滤器提供一种功能,很好的遵循了单一职责原则。

实际上,Spring Security提供的过滤器远不止这些,全部的过滤器以enum的形式被定义在org.springframework.security.config.http.SecurityFilters中:

enum SecurityFilters {
   FIRST(Integer.MIN_VALUE),
   CHANNEL_FILTER,
   SECURITY_CONTEXT_FILTER,
   CONCURRENT_SESSION_FILTER,
   WEB_ASYNC_MANAGER_FILTER /** {@link WebAsyncManagerIntegrationFilter} */,
   HEADERS_FILTER, CORS_FILTER,
   CSRF_FILTER,
   LOGOUT_FILTER,
   X509_FILTER,
   PRE_AUTH_FILTER,
   CAS_FILTER,
   FORM_LOGIN_FILTER,
   OPENID_FILTER,
   LOGIN_PAGE_FILTER,
   LOGOUT_PAGE_FILTER,
   DIGEST_AUTH_FILTER,
   BEARER_TOKEN_AUTH_FILTER,
   BASIC_AUTH_FILTER,
   REQUEST_CACHE_FILTER,
   SERVLET_API_SUPPORT_FILTER,
   JAAS_API_SUPPORT_FILTER,
   REMEMBER_ME_FILTER,
   ANONYMOUS_FILTER,
   SESSION_MANAGEMENT_FILTER,
   EXCEPTION_TRANSLATION_FILTER,
   FILTER_SECURITY_INTERCEPTOR,
   SWITCH_USER_FILTER,
   LAST(Integer.MAX_VALUE);   
}

这些过滤器的功能简介如下:

| 过滤器名称

类名 作用
CHANNEL_FILTER
ChannelProcessingFilter
确保请求是由正确的通道处理。通常情况下用于决定一个web请求是否需要经过https处理
SECURITY_CONTEXT_FILTER
SecurityContextPersistenceFilter
填充SecurityContextHolder,创建SecurityContext供框架使用
CONCURRENT_SESSION_FILTER
ConcurrentSessionFilter
并发Session处理机制的一部分。其主要作用是在每个用户的并发Session数量达到最大时,校验Session是否过期,如果过期就退出。
WEB_ASYNC_MANAGER_FILTER
WebAsyncManagerIntegrationFilter
提供了对SecurityContextWebAsyncManager的集成,将SecurityContext设置到Callable
LOGOUT_FILTER
LogoutFilter
拦截特定的URL(默认为/logout),在用户退出时完成特定工作:清除cookie,删除remember meSecurityContext信息等
X509_FILTER
X509AuthenticationFilter
使用 X509Certificate从请求中提取PrincipalCredential,并尝试使用这些信息进行认证
PRE_AUTH_FILTER
J2eePreAuthenticatedProcessingFilter
使用J2ee认证机制作为预认证。
CAS_FILTER
CasAuthenticationFilter
单点登录认证支持
FORM_LOGIN_FILTER
UsernamePasswordAuthenticationFilter
提供基于用户名密码的认证机制
OPENID_FILTER
OpenIDAuthenticationFilter
提供基于openId的身份证机制
LOGIN_PAGE_FILTER
LoginPageGeneratingWebFilter
生成默认的登录页
LOGOUT_PAGE_FILTER
LogoutPageGeneratingWebFilter
生成默认的退出页
DIGEST_AUTH_FILTER
DigestAuthenticationFilter
支持HTTP摘要认证机制。查找DigestHTTP认证头,并进行认证,认证成功后生成Authentication对象
BEARER_TOKEN_AUTH_FILTER
OAuth2AuthenticationProcessingFilter
支持OAuth2认证方式
BASIC_AUTH_FILTER
BasicAuthenticationFilter
支持 HTTP BASIC认证方式
REQUEST_CACHE_FILTER
RequestCacheAwareFilter
取出被缓存的请求,重新请求。用于用户登录成功后,重新恢复因为登录被打断的请求
SERVLET_API_SUPPORT_FILTER
SecurityContextHolderAwareRequestFilter
将请求包装在实现Servlet API安全方法(如isUserInRole)的请求包装器中,并将其委托给SecurityContextHolder,使得用户能使使用Servlet API方便的获取认证信息。
JAAS_API_SUPPORT_FILTER
JaasApiIntegrationFilter
从请求中获取Subject,然后以该Subject执行过滤器链
REMEMBER_ME_FILTER
RememberMeAuthenticationFilter
如果开启了“记住我”功能,该过滤器将尝试自动登录,并使用“记住我”里面的信息进行身份认证
ANONYMOUS_FILTER
AnonymousAuthenticationFilter
检查SecurityContext中是否已有Authentication,如果没有,创建一个匿名的Authentication
SESSION_MANAGEMENT_FILTER
SessionManagementFilter
Authentication对象传递给Session管理器,做一些校验和Session相关的处理,目前只有一个ConcurrentSessionControlStrategy,处理Session并发问题
EXCEPTION_TRANSLATION_FILTER
ExceptionTranslationFilter
处理Spring Security异常与HTTP STATUS CODE之间的与转换工作。
FILTER_SECURITY_INTERCEPTOR
FilterSecurityInterceptor
处理具体的授权工作。根据Authenticaiton对象与配置的权限信息决定能否访问该URL
SWITCH_USER_FILTER
SwitchUserFilter
Spring应用提供类似Linuxsu命令的功能,当期用户可以切换至拥有更高权限的用户。

ConfigAttribute

ConfigAttribute包含了Spring Security进行资源保护的配置元信息。其类图如下:

假如你配置了如下信息:

<security:intercept-url pattern="/hello" access="ROLE_SIMPSON_MEMBER" />

Spring Security将使用 FilterInvocationSecurityMetadataSourceParser解析这段配置,pattern部分作为一个AntPattern被解析成一个RequestMatcher实例,而access部分被解析成一个ConfigAttribute,将RequestMatcher作为key,ConfigAttribute作为value组成一个map集合,放置到FilterInvocationSecurityMetadataSource中,在FilterSecurityInterceptor中使用。解析过程如下:

如果需要方法级别的安全,则需要开启@EnableGlobalMethodSecurity或在xml配置中添加如下代码:

<global-method-security secured-annotations = "enabled"/>

Spring将在启动时创建 MethodSecurityMetadataSourceAdvisor实例,并注册到容器中。该 Advisor作为一个基础的 AdivsorInfrastructureAdvisorAutoProxyCreator(实际上是一个 BeanPostProcessor)识别,该InfrastructureAdvisorAutoProxyCreator扫描容器中所有的bean,判断这些bean是否可以应用MethodSecurityMetadataSourceAdvisor(取决于Bean的方法上是否有@Secured注解),如果可以应用,则使用该 Advisor封装bean.

具体过程是:对于每一个Bean及其方法都调用 MethodSecurityMetadataSource.getAttributes方法,判断是否有ConfigAttribute,如果有,InfrastructureAdvisorAutoProxyCreator则调用其 createProxy方法应用 MethodSecurityMetadataSourceAdvisor(内部包含securityMethodInterceptor作为Advice)创建代理。

Authentication

一个 Authentication对象代表一个登陆进系统的实体,比如:一个登陆的用户就是一个 Authentication对象。Authentication对象有以下几个实现类:

public interface Authentication extends Principal, Serializable {
     //权限
   Collection<? extends GrantedAuthority> getAuthorities();
    //凭据,密码
   Object getCredentials();
    //详细信息
   Object getDetails();  
    //主体,账号信息
   Object getPrincipal();
   //是否已经过认证
   boolean isAuthenticated();   
   void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
实现类 说明
UsernamePasswordAuthenticationToken 用于用户名密码认证模式
OpenIDAuthenticationToken 用于OpenID 认证模式
PreAuthenticatedAuthenticationToken 用于预认证模式,所谓预认证模式是指实际认证过程是由外部系统负责,Spring Security只从外部系统的消息中提取认证的principal信息

SecurityContext

SecurityContextSpring用于存储Authentication对象的地方,它将Authentication对象与当前线程绑定,放置到ThreadLocal中。SecurityContextHolder使得可以在应用的任何地方访问SecurityContext,并使用策略模式满足不同的应用场景。

AuthenticationProvider

AuthenticationProvider主要用于认证一个Authentication对象。其主要定义了如下两个接口:

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	boolean supports(Class<?> authentication);
}

并且有几种不同的实现类,分别适用于不同的场景:

AccessDecisionManager

AccessDecisionManager用于决定一个具体的Authentication是否具有访问特定资源的权限。在其实现中,它将逻辑委托给AccessDecisionVoterAccessDecisionVoter只是简单的比较Authentication 对象中的GrantedAuthorities是否满足资源配置的ConfigAttribute需求。这些AccessDecisionVoters投票决定是否允许Authentication访问资源。AccessDecisionManager的实现使用特定的策略来考虑投票的结果,决定是否能够访问资源。

实现类 说明
AffirmativeBased 在所有的Voter中,如果有一个或多于一个的Voter投赞成票,就可以访问资源。如果没有人投赞成票,且至少有一人投反对票,则抛出AccessDeniedException
ConsensusBased 只有赞成票的数量多于反对票的数量时,才能访问资源
UnanimousBased 只有所有人投赞成票时,才能访问资源

AccessDecisionVoter

上面说AccessDecisionManager使用``AccessDecisionVoter进行投票,具体的投票过程依赖于特定的支持。其实现类如下:

实现类 说明
Jsr250Voter 为带有JSR 250注解的资源进行投票,如@DenyAll,@PermitAll,@RolesAllowed
PreInvocationAuthorizationAdviceVoter 为带有@PreFilter@PreAuthorize注解的资源进行投票,这两个注解的值支持SpEL表达式,PreInvocationAuthorizationAdviceVoter负责计算SpEL表达式的值,并根据结果进行投票
AclEntryVoter 主要处理ACL规则,对领域模型对象进行投票,主要逻辑实现在AbstractAclVoter中。
AuthenticatedVoter 如果资源的ConfigAttribute中引用了认证信息的三个级别(IS_AUTHENTICATED_FULLY,IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_ANONYMOUSLY)之一,该 Voter就会进行投票
RoleVoter 根据角色进行投票,应用最普遍的一个Voter
WebExpressionVoter 支持SpEL表达式的Voter,例如:hasRole('admin')

UserDetailsService

UserDetailsService负责在身份验证请求到达应用程序时从底层用户存储(内存、数据库等)加载用户信息.该接口只定义了一个方法:

public interface UserDetailsService {	
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

其集成体系如下:

当请求需要认证时,主要有两种策略(AuthenticationUserDetailsServiceUserDetailsService)获取用户信息:UserDetailsService根据用户名获取UserDetails;AuthenticationUserDetailsService 根据 Authentication对象获取用户信息。AuthenticationUserDetailsService 一般用于预认证方式。例如:OpenIDAuthenticationProviderCasAuthenticationProvider将获取UserDetails的逻辑委托给AuthenticationUserDetailsService 处理。

UserDetails

UserDetails对象包含了Spring Security上下文中的完整用户信息,该对象可以在应用的任意位置从SercurityContext中获得。

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

ACL

ACL模块负责以细粒度在单个领域模型上保护应用。在这种情况下,需要为每一个领域模型对象创建一个ID,并创建领域模型与用户之间的关系,这种关系决定一个用户能否访问相关的领域模型对象。ACL提供了非常细粒度的访问控制。当前的ACL模块支持从关系数据库中读取访问规则。

posted @ 2023-01-31 14:10  小张同学哈  阅读(199)  评论(0)    收藏  举报