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。
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
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认证过程如下:
- 用户输入用户名和密码;被包装成
UsernamePasswordAuthenticationToken
(Authentication的实现); - 这个token传递到
AuthenticationManager
; - AuthenticationManager验证后,返回一个完全填充(fully populated)的Authentication对象;
- 通过SecurityContextHolder.getContext().setAuthentication,完成安全上下文的创建。
web应用的认证过程会稍微复杂一些,同样经过简化可以表述如下:
- 用户访问某个受保护的url
- AbstractSecurityInterceptor拦截这个请求,并抛出没有权限
- ExceptionTranslationFilter捕获这个异常
- 如果检测到用户没有认证,于是通过AuthenticationEntryPoint重定向用户到一个登录页面;
- 如果发现已经认证但是权限不足,通常返回HTTP 403.
- 接下来的认证过程和上面是类似的。
鉴权机制
鉴权决策的核心接口是AccessDecisionManager,它的核心方法是
void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes)
第一个参数我们已经知道是认证信息,第二个参数代表要访问的受保护对象,第三个参数是这个资源的权限属性集。
什么是受保护对象?可能是一个url请求,或者是某一个方法调用。不同类型的受保护资源,会有不同的拦截器(接口AbstractSecurityInterceptor)来拦截正常的访问流程,插入权限决策机制。
拦截器完成以下工作:
- 查找当前受保护对象的权限属性;
- 将受保护对象(secure object),权限属性(configuration attributes),当前的认证信息(authentication),提交给AccessDecisionManager来鉴权;
- 如果鉴权通过,继续执行正常的访问;
- 否则抛出异常;
权限属性(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的配置类似如下:
<bean id="authenticationManager"
class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="ldapAuthenticationProvider"/>
</list>
</constructor-arg>
</bean>
上一章节讲到UserDetailsService可以通过用户名加载用户的信息(UserDetails),实现该种认证方式的Manager是DaoAuthenticationProvider,他内部配置一个UserDetailsService引用:
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</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,下面看示例:
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
<list>
<sec:filter-chain pattern="/restful/**" filters="
securityContextPersistenceFilterWithASCFalse,
basicAuthenticationFilter,
exceptionTranslationFilter,
filterSecurityInterceptor" />
<sec:filter-chain pattern="/**" filters="
securityContextPersistenceFilterWithASCTrue,
formLoginFilter,
exceptionTranslationFilter,
filterSecurityInterceptor" />
</list>
</constructor-arg>
</bean>
上面对不同的url路径使用了不同的安全策略,而不同的安全策略是通过一个filter chain来实现的。这些filter的全部实现java.servlet.filter接口,但并不由web容器来管理。前面所说的那些“认证”,“鉴权”相关功能实现都隐藏在这些filter背后。
这些Filter有严格的顺序,比如授权相关的Filter就要出现在鉴权相关的Fiter之前,关于位置,Spring Security定义了一组常量。当你想提供一个自定义的Filter的时候,可能会用到。
xml配置之http(示例)
我们基本不会相上面那样配置FilterChain,在xml文件里面一个http元素,就意味着一条完整的FilterChain被配置,Spring Security的配置模块为我们完成大量的工作。
<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>
<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>
<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>
上面三个http元素,第一个对/restful/**
使用基于http-basic的登录方式,使用基于角色的鉴权方式(角色REMOTE可以访问)。
第二个对/login.htm*
不使用任何安全过滤;和第一个相比,使用form-login的登录方式,并指定了登录url和登录后跳转的url,还配置了登出时的默认行为。
http元素的每个属性,每个子元素,都可能对filter chain产生或大或小的影响,简洁的同时也让人不免晕头转向。
核心安全过滤器
FilterSecurityInterceptor
这是负责鉴权的Filter,典型的配置如下:
<bean id="filterSecurityInterceptor"
class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
<security:filter-security-metadata-source>
<security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
<security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
</security:filter-security-metadata-source>
</property>
</bean>
前面的章节讲过,鉴权的过程就是将Authentication,安全对象,安全对象的配置属性,提交给AccessDecisionManager。上面的配置可见,这个Filter包含AuthenticationManager,AccessDecisionManager引用,而内嵌的securityMetadataSource提供了安全对象的配置属性。
ExceptionTranslationFilter
ExceptionTranslationFilter应当位于FilterSecurityInterceptor之前,它起一个粘合剂的作用。负责捕获权限相关的异常,如果用户当前没有登录,则引导应用去登录界面,否则返回失败结果:
<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>
<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>
<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>
这个配置展示了ExceptionTranslationFilter的典型功能,authenticationEntryPoint定义了登录入口,accessDeniedHandler配置了鉴权失败的返回页面。
SecurityContextPersistenceFilter
这个Filter用来在request之间保存Security Context;它的默认配置如下,使用HttpSession来保存conext。
<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
<bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
<property name='allowSessionCreation' value='true' />
</bean>
</property>
</bean>
UsernamePasswordAuthenticationFilter
前面说了ExceptionTranslationFilter里面有个authenticationEntryPoint,引导用户去登录。如果是采用用户名和密码登录的方式,输入的用户名和密码会被UsernamePasswordAuthenticationFilter接收,并完成认证。
<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
UsernamePasswordAuthenticationFilter里面配置一个AuthenticationManager,关于AuthenticationManager的配置,前面已经讲过。还可以配置AuthenticationSuccessHandler,AuthenticationFailureHandler,对登录成功或失败后续行为进行定制。
xml配置之http(解释)
当第一个http元素出现在xml配置文件里时,一个名叫springSecurityFilterChain
的FilterChainProxy会被创建出来,并且http的具体配置会用于创建一个完整的filter chain。继续添加http元素,意味着创建额外的filter chain。 每个http元素至少会创建SecurityContextPersistenceFilter, ExceptionTranslationFilter,FilterSecurityInterceptor这三个Filter,并且无法替换成自定义版本(这里指“无法通过配置http元素的属性和子元素来替换”)。
如果在配置文件里面使用
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。