Spring Security是一个强大的java应用安全管理库,特别适合用作后台管理系统。这个库涉及的模块和概念有一定的复杂度,而大家平时学习Spring的时候也不会涉及;这里基于官方的参考文档,把Spring Security的基本套路介绍一下。

参考的Spring Security文档地址:​​https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/preface.html​

Spring Securitys示例​​https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/samples.html​​;对于新手入门,看一下示例很有必要,但是一般的产品的安全的策略都比示例要复杂得多,很难通过模仿示例程序来达成你的目标。

说明:这篇文章不打算手把手教大家如何使用Spring Security,所以不会有详细的代码以及配置;少量的代码和配置示例,仅仅用来阐述概念和设计,这些示例代码和配置并不一定适合在项目中使用。

Over View

认证和鉴权("authentication" and "authorization" )

应用安全一般可分成两个方面,一是认证:确认使用者的身份,创建对应的principal(这个词代表一个经过确认的身份信息);二是鉴权:判定某个principal是否有访问某个资源或执行某个操作的权限。

Spring Security支持很多的认证方式比如HTTP BASIC, OPEN ID,FORM LOGI等等,这里不列举。而对于鉴权,支持3种主要类型:web请求,方法调用,以及domain对象。

由于Spring Security支持的功能很广泛,这篇文章不会一一介绍。将背景限定为:一个通过http协议访问的web系统,采用表单登录,用户信息存储在数据库里面。

Security-Core

这是使用Spring Security的必然要依赖的一个库,其中包含了最基本的数据结构和接口。在Spring Security 3.0版本以后,这个库经过简化,不再包含web、ldap、configuration相关的功能。从DDD的角度来看,这个库是Spring Security的领域模型。下面介绍一下几个最基本的类。

SecurityContextHolder

SecurityContextHolder是存放当前安全相关上下文对象的地方,它包含一个Authentication对象,包含认证用户的多有信息。它使用ThreadLocal来存放信息,请求执行结束以后清除相关信息。因此如果你需要在其他线程访问Security上下文信息,请注意这一点。

下面的代码展示了如何通过contextHolder访问principal。

  1. ​Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();​

  2. ​if (principal instanceof UserDetails) {​
  3. ​String username = ((UserDetails)principal).getUsername();​
  4. ​} else {​
  5. ​String username = principal.toString();​
  6. ​}​

UserDetails

大多数情况下,principal是一个UserDetails实例。UserDetails是一个很重要的接口,代表认证用户的详细信息。我们可以通过自定义的类来实现它,或者使用security库提供的简单实现。不管如何,它是业务层用户数据和spring security之间的桥梁,在必要时,我们可以把UserDetails转换回具体类型,来访问额外的字段。

创建UserDetails对象是一个叫做UserDetailsService的接口,它只有一个方法:

UserDetails loadUserByUsername(String username)

即通过用户名字查询UserDetails对象。不管实现如何,UserDetailsService被视作一个类似DAO的角色,参与到认证过程中来。

GrantedAuthority

除了principal,Authentication还包含一个GrantedAuthority数组。GrantedAuthority代表赋予principal的一项权限,最通常的情况,是代表某个角色,比如“ROLE_ADMINISTRATOR”。

GrantedAuthority接口只有一个方法,就是​​String getAuthority()​​,意味着如果你的鉴权机制通过字符串的处理就能完成,那么通过字符串表达就好。前缀“ROLE_"就是一个约定,代表基于角色的权限。如果你的权限需要更复杂的数据结构来表示,那么请自定义GrantedAuthority具体实现,这样的话权限鉴定(后面会讲)的过程也需要自定义。

认证

一个简化的Spring Security认证过程如下:

  1. 用户输入用户名和密码;被包装成​​UsernamePasswordAuthenticationToken​​(Authentication的实现);
  2. 这个token传递到​​AuthenticationManager​​;
  3. AuthenticationManager验证后,返回一个完全填充(fully populated)的Authentication对象;
  4. 通过SecurityContextHolder.getContext().setAuthentication,完成安全上下文的创建。

web应用的认证过程会稍微复杂一些,同样经过简化可以表述如下:

  1. 用户访问某个受保护的url
  2. AbstractSecurityInterceptor拦截这个请求,并抛出没有权限
  3. ExceptionTranslationFilter捕获这个异常
  4. 如果检测到用户没有认证,于是通过AuthenticationEntryPoint重定向用户到一个登录页面;
  5. 如果发现已经认证但是权限不足,通常返回HTTP 403.
  6. 接下来的认证过程和上面是类似的。

鉴权机制

鉴权决策的核心接口是AccessDecisionManager,它的核心方法是

void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes)

第一个参数我们已经知道是认证信息,第二个参数代表要访问的受保护对象,第三个参数是这个资源的权限属性集。

什么是受保护对象?可能是一个url请求,或者是某一个方法调用。不同类型的受保护资源,会有不同的拦截器(接口AbstractSecurityInterceptor)来拦截正常的访问流程,插入权限决策机制。

拦截器完成以下工作:

  1. 查找当前受保护对象的权限属性;
  2. 将受保护对象(secure object),权限属性(configuration attributes),当前的认证信息(authentication),提交给AccessDecisionManager来鉴权;
  3. 如果鉴权通过,继续执行正常的访问;
  4. 否则抛出异常;

权限属性(configuration attribute)是受保护对象的,与权限相关的属性数据,一般就是普通的字符串。对这个属性的解释取决于AccessDecisionManager的实现。通过为AbstractSecurityInterceptor配置SecurityMetadataSource来实现权限属性的查找。比如,在xml配置里面看到​​<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>​​,那么配置属性“ROLE_A”和“ROLE_B”暗示角色A和B能访问这个pattern的url;当然实际是否如此还要看AccessDecisionManager的配置。这里再强调一下,"ROLE_"这个前缀是Spring Security内的一种约定,用于基于角色的鉴权机制。

核心的服务

AuthenticationManager仅仅是一个接口,具体的实现取决于认证的方式。Spring Security的默认实现叫做ProviderManager,它把认证功能委托给一个AuthenticationProvider列表。每个AuthenticationProvider可以返回一个完全填充(fully populated)的Authentication对象(认证成功),或抛出一个异常;可见,ProviderManager可以组合多种认证方式,一个ProviderManager bean的配置类似如下:

  1. ​<bean id="authenticationManager"​
  2. ​class="org.springframework.security.authentication.ProviderManager">​
  3. ​<constructor-arg>​
  4. ​<list>​
  5. ​<ref local="daoAuthenticationProvider"/>​
  6. ​<ref local="anonymousAuthenticationProvider"/>​
  7. ​<ref local="ldapAuthenticationProvider"/>​
  8. ​</list>​
  9. ​</constructor-arg>​
  10. ​</bean>​

上一章节讲到UserDetailsService可以通过用户名加载用户的信息(UserDetails),实现该种认证方式的Manager是DaoAuthenticationProvider,他内部配置一个UserDetailsService引用:

  1. ​<bean id="daoAuthenticationProvider"​
  2. ​class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">​
  3. ​<property name="userDetailsService" ref="inMemoryDaoImpl"/>​
  4. ​<property name="passwordEncoder" ref="passwordEncoder"/>​
  5. ​</bean>​

上面的PasswordEncoder用于用户密码的编解码。一般用户的密码不会以明文形式存储,同时加密方式也不会支持逆向解密;PasswordEncoder可以将输入的密码进行加密,再与存储的密文进行比较。为了支持同时多种加密方式,Spring Security设计了叫做DelegatingPasswordEncoder的encoder,他将加密委托给多种具体加密方式,依据密文类型查询。于是默认的密文存储格式就变成了​​{id}encodedPassword​​。一个特殊的encode是NoOpPasswordEncoder,表示明文存储,不做任何处理。

Web应用安全

这部分讲一下Spring Security和Spring MVC的结合。上面讲了Spring Security的核心配置,在web应用中,security的功能通过servlet filter的方式与web功能链接在一起。这会使得整个配置更加复杂,因此Spring Security提供了简洁的xml配置方式,一个简单的标签后面,完成了大量功能。

DelegatingFilterProxy

我们都知道filter应该配置在web.xml中,实际上Spring Security只配置一个唯一的fiter,叫做DelegatingFilterProxy。它将实际的功能委托给其他配置在Spring Context内的Spring Bean。

FilterChainProxy

上面DelegatingFilterProxy将功能委托给FilterChainProxy,FilterChainProxy是一个普通的Spring bean,如果要在xml里面声明它,注意bean id应当于web.xml里面声明DelegatingFilterProxy的filter-name是一样的。

FilterChainProxy的名字暗示了,它背后有很多filter组成的chain,实际上它可以包含多个chain,下面看示例:

  1. ​<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">​
  2. ​<constructor-arg>​
  3. ​<list>​
  4. ​<sec:filter-chain pattern="/restful/**" filters="​
  5. ​securityContextPersistenceFilterWithASCFalse,​
  6. ​basicAuthenticationFilter,​
  7. ​exceptionTranslationFilter,​
  8. ​filterSecurityInterceptor" />​
  9. ​<sec:filter-chain pattern="/**" filters="​
  10. ​securityContextPersistenceFilterWithASCTrue,​
  11. ​formLoginFilter,​
  12. ​exceptionTranslationFilter,​
  13. ​filterSecurityInterceptor" />​
  14. ​</list>​
  15. ​</constructor-arg>​
  16. ​</bean>​

上面对不同的url路径使用了不同的安全策略,而不同的安全策略是通过一个filter chain来实现的。这些filter的全部实现java.servlet.filter接口,但并不由web容器来管理。前面所说的那些“认证”,“鉴权”相关功能实现都隐藏在这些filter背后。

这些Filter有严格的顺序,比如授权相关的Filter就要出现在鉴权相关的Fiter之前,关于位置,Spring Security定义了一组常量。当你想提供一个自定义的Filter的时候,可能会用到。

xml配置之http(示例)

我们基本不会相上面那样配置FilterChain,在xml文件里面一个http元素,就意味着一条完整的FilterChain被配置,Spring Security的配置模块为我们完成大量的工作。

  1. ​<!-- Stateless RESTful service using Basic authentication -->​
  2. ​<http pattern="/restful/**" create-session="stateless">​
  3. ​<intercept-url pattern='/**' access="hasRole('REMOTE')" />​
  4. ​<http-basic />​
  5. ​</http>​

  6. ​<!-- Empty filter chain for the login page -->​
  7. ​<http pattern="/login.htm*" security="none"/>​

  8. ​<!-- Additional filter chain for normal users, matching all other requests -->​
  9. ​<http>​
  10. ​<intercept-url pattern='/**' access="hasRole('USER')" />​
  11. ​<form-login login-page='/login.htm' default-target-url="/home.htm"/>​
  12. ​<logout />​
  13. ​</http>​

上面三个http元素,第一个对​​/restful/**​​​使用基于http-basic的登录方式,使用基于角色的鉴权方式(角色REMOTE可以访问)。
第二个对​​​/login.htm*​​不使用任何安全过滤;和第一个相比,使用form-login的登录方式,并指定了登录url和登录后跳转的url,还配置了登出时的默认行为。

http元素的每个属性,每个子元素,都可能对filter chain产生或大或小的影响,简洁的同时也让人不免晕头转向。

核心安全过滤器

FilterSecurityInterceptor

这是负责鉴权的Filter,典型的配置如下:

  1. ​<bean id="filterSecurityInterceptor"​
  2. ​class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">​
  3. ​<property name="authenticationManager" ref="authenticationManager"/>​
  4. ​<property name="accessDecisionManager" ref="accessDecisionManager"/>​
  5. ​<property name="securityMetadataSource">​
  6. ​<security:filter-security-metadata-source>​
  7. ​<security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>​
  8. ​<security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>​
  9. ​</security:filter-security-metadata-source>​
  10. ​</property>​
  11. ​</bean>​

前面的章节讲过,鉴权的过程就是将Authentication,安全对象,安全对象的配置属性,提交给AccessDecisionManager。上面的配置可见,这个Filter包含AuthenticationManager,AccessDecisionManager引用,而内嵌的securityMetadataSource提供了安全对象的配置属性。

ExceptionTranslationFilter

ExceptionTranslationFilter应当位于FilterSecurityInterceptor之前,它起一个粘合剂的作用。负责捕获权限相关的异常,如果用户当前没有登录,则引导应用去登录界面,否则返回失败结果:

  1. ​<bean id="exceptionTranslationFilter"​
  2. ​class="org.springframework.security.web.access.ExceptionTranslationFilter">​
  3. ​<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>​
  4. ​<property name="accessDeniedHandler" ref="accessDeniedHandler"/>​
  5. ​</bean>​

  6. ​<bean id="authenticationEntryPoint"​
  7. ​class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">​
  8. ​<property name="loginFormUrl" value="/login.jsp"/>​
  9. ​</bean>​

  10. ​<bean id="accessDeniedHandler"​
  11. ​class="org.springframework.security.web.access.AccessDeniedHandlerImpl">​
  12. ​<property name="errorPage" value="/accessDenied.htm"/>​
  13. ​</bean>​

这个配置展示了ExceptionTranslationFilter的典型功能,authenticationEntryPoint定义了登录入口,accessDeniedHandler配置了鉴权失败的返回页面。

SecurityContextPersistenceFilter

这个Filter用来在request之间保存Security Context;它的默认配置如下,使用HttpSession来保存conext。

  1. ​<bean id="securityContextPersistenceFilter"​
  2. ​class="org.springframework.security.web.context.SecurityContextPersistenceFilter">​
  3. ​<property name='securityContextRepository'>​
  4. ​<bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>​
  5. ​<property name='allowSessionCreation' value='true' />​
  6. ​</bean>​
  7. ​</property>​
  8. ​</bean>​

UsernamePasswordAuthenticationFilter

前面说了ExceptionTranslationFilter里面有个authenticationEntryPoint,引导用户去登录。如果是采用用户名和密码登录的方式,输入的用户名和密码会被UsernamePasswordAuthenticationFilter接收,并完成认证。

  1. ​<bean id="authenticationFilter" class=​
  2. ​"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">​
  3. ​<property name="authenticationManager" ref="authenticationManager"/>​
  4. ​</bean>​

UsernamePasswordAuthenticationFilter里面配置一个AuthenticationManager,关于AuthenticationManager的配置,前面已经讲过。还可以配置AuthenticationSuccessHandler,AuthenticationFailureHandler,对登录成功或失败后续行为进行定制。

xml配置之http(解释)

当第一个http元素出现在xml配置文件里时,一个名叫​​springSecurityFilterChain​​的FilterChainProxy会被创建出来,并且http的具体配置会用于创建一个完整的filter chain。继续添加http元素,意味着创建额外的filter chain。 每个http元素至少会创建SecurityContextPersistenceFilter, ExceptionTranslationFilter,FilterSecurityInterceptor这三个Filter,并且无法替换成自定义版本(这里指“无法通过配置http元素的属性和子元素来替换”)。

如果在配置文件里面使用定义了AuthenticationManager,那么http动创建的所有Filter,都会按需自动注入这个manager。

http元素的重要属性如下:

  • access-decision-manager-ref 指向AccessDecisionManager,后者通过Spring bean来定义;
  • authentication-manager-ref 指向AuthenticationManager,后者通过Spring bean来定义;
  • entry-point-ref,指向AuthenticationEntryPoint,后者通过Spring bean来定义;
  • pattern,指定urli匹配模式
  • security,如果要设置的话,只能是​​none​​,表示对url pattern不使用安全策略;

http元素的重要子元素如下:

  • access-denied-handler,鉴权失败的处理器;
  • form-login,会创建UsernamePasswordAuthenticationFilter,以及LoginUrlAuthenticationEntryPoint;
  • logout, 创建LogoutFilter,后者和SecurityContextLogoutHandler一起工作;
  • session-management,创建SessionManagementFilter;
  • custom-filter, 添加自定义的Fiter,后者通过spring bean定义;通过属性,可指定该filter放在某个标准filter的前面,后面,或取代这个标准filter。

具体xml配置请参考文档,这里对http元素做简要说明,主要为了阐述如何围绕http这个元素,来构建一个完成的Filter chain。