作为软件开发人员,我们必须采取措施来保护应用程序中的信息。无论你是通过用户名/密码来保护电子邮件账号,还是基于交易个人身份号码来保护经纪账户,安全性都是绝大多数应用系统中的一个重要切面(aspect)。
安全性是超越应用程序功能性的一个关注点。应用系统的绝大部分不应该参与到与自己相关的安全性处理中。尽管你可以直接在你的应用程序中编写安全性功能相关的代码,但更好方式还是将安全性相关的关注点与应用程序本身的关注点进行分离。
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案,它能够在Web请求级别和方法调用级别处理身份验证和授权。因为基于Spring框架,所以Spring Security充分利用了依赖注入和面向切面的技术。
Spring Security从两个角度来解决安全性问题。它使用Servlet过滤器保护Web请求并限制URL级别的访问,也可以使用Spring AOP保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。
不管你想使用Spring Security保护哪种类型的应用程序,第一件需要做的事就是将Spring Security模块添加到应用程序的路径下。Spring Security 3.0分为了8个模块如下:
1、使用Spring Security配置命名空间
Spring Security提供了安全性相关的命名空间,这极大简化了Spring中的安全性配置。要使用安全性命名空间,唯一要做的事情是将命名空间声明添加到XML文件中,如下:
我们可以将安全性相关的配置拆分到一个单独的Spring配置文件。鉴于这个文件中的所有配置都来自于安全性命名空间,因此我们可以将安全性命名空间改为这个文件的首要命名空间如下:
将安全性命名空间作为首要命名空间之后,我们就可以避免为所有元素添加那些令人讨厌的"security:"前缀了。
2、保护Web请求
我们使用Java Web应用所做的任何事情都是从HttpServletRequest开始的。如果说请求是Web应用的入口的话,那这也是Web应用的安全性起始的位置。
对于请求级别的安全性来说,最基本的形式涉及声明一个或多个URL模式,并要求具备一定级别权限的用户才能对其进行访问,并阻止无这些权限的用户访问这些URL背后的内容。更进一步来讲,你可能还会要求只能通过HTTPS访问特定的URL。
Spring Security支持以上这些安全性以及其他多种方式的请求级别安全性。在开始Spring应用的Web安全性之前,我们必须构建那些提供各种安全特性的Servlet过滤器。
Spring Security借助一系列Servlet过滤器来提供各种安全性功能。我们需要在应用的web.xml中配置一个过滤器。具体来讲,需要添加如下的<filter>:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelefatingFilterProxy
</filter-class>
</filter>
DelegatingFilterProxy是一个特殊的Servlet过滤器,它本身所做的工作并不多。只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring应用的上下文中,如图:
为了完成各自的工作内容,Spring Security的过滤器必须注入一些其他的Bean。我们无法对注册在web.xml中的Servlet过滤器进行Bean注入。但是,通过使用DelegatingFilterProxy,我们可以在Spring中配置实际的过滤器,从而能够充分利用Spring对依赖注入的支持。
DelegatingFilterProxy的<filter-name>值是有意义的。这个名字用于在Spring应用上下文中查找过滤器Bean。Spring Security将自动创建一个ID为springSecurityFilterChain的过滤器Bean,这就是我们在web.xml中卫DeletatingFilterProxy所设置的name值。
springSecurityFilterChain本身是另一个特殊的过滤器,它也被称为FilterChainProxy。它可以链接任意一个或多个其他的过滤器。Spring Security依赖一系列Servlet过滤器来提供不同的安全特性。我们几乎不需要知道这些细节,因为当配置<http>元素时,Spring Security将会为我们自动创建这些Bean。
早期版本的Spring Security需要无数的XML配置来构建基本的安全特性。与此形成鲜明对比的是,使用新版本的Spring Security,以下的XML片段包含了许多的功能:
<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_SPITTER"/>
</http>
这3行简单的XML就可以配置Spring Security拦截所有URL请求,并限制只有拥有ROLE_SPITTER角色的认证用户才能访问。<http>元素将自动创建一个FilterChainProxy(它会委托给配置在web.xml中的DelegatingFilterProxy)以及链中的所有过滤器Bean。
除了这些过滤器Bean,通过将quto-config属性配置为true,将会自动为我们的应用提供一个额外的登录页、HTTP基本认真和推出功能。实际上,将auto-config属性配置为true等价于下面这样显示配置的特性:
<http>
<form-login/>
<http-basic/>
<logout/>
<intercept-url pattern="/**" access="ROLE_SPITTER"/>
</http>
将auto-config属性配置为true的一个好处就是Spring Security将会自动为你生成登录页面。你可以通过相对于上下文URL的/spring_security_login路径来访问这个自动生成的表单。
Spring Security提供的登录表单太朴素了,我们可以替换为自己设计的登录页面。为了设置自己的登录页,我们需要显示配置<form-login>元素来取代默认的行为:
<http auto-config="true" use-expressions="false">
<form-login login-processing-url="/static/j_spring_security_check" login-page="/login" authentication-failure-url="/login?login_error=t">
</http>
login-page属性为登录页声明了一个新的且相对于上下文的URL。在这个示例中,我们声明登录页为/login,它最终由一个Spring MVC控制器来进行处理。同样,如果验证失败,通过设置authentication-failure-url属性,就会把用户重定向到相同的登录页。
需要注意的是,我们将login-processing-url属性设置为/static/j_spring_security_check。这是登录表单提交回来进行用户认证的URL。
对于应用程序的人类用户,基于表单的认证是比较理想的。但是当应用程序的使用者是另外一个应用程序的话,使用表单来提示登录的方式就不太合适了。HTTP基本认证(HTTP Basic Authentication)是直接通过HTTP请求本身来对要访问应用程序的用户进行认证的一种方式。你可能在以前见过HTTP基本认证。当在Web浏览器中使用时,它将向用户弹出一个简单的模态对话框。但这只是Web浏览器的显示方式。本质上,这是一个HTTP 401响应,表明必须要在请求中包含一个用户名和密码。在REST客户端向它使用的服务进行认证的场景中,这种方式比较合适。在<http-basic>元素中,并没有太多的可配置项。HTTP基本认真要么开启要么关闭。
<logout>元素会构建一个Spring Security过滤器,这个过滤器用于使用户的会话失效。在使用的时候,通过<logout>构建起来的过滤器将匹配/j_spring_sercurity_logout地址。但这与我们已经构建的DispatcherServlet并不冲突,我们需要像登录表单那样重写这个过滤器的URL。为了做到这一点,需要设置logout-url特性:
<logout logout-url="/static/j_spring_security_logout"/>
<intercept-url>元素是实现请求级别安全游戏中的第一道防线。它的pattern属性定义了对传入请求要进行匹配的URL模式。如果请求匹配这个模式的话,<intercept-url>的安全规则就会启用。<intercept-url>的配置如下:<intercept-url pattern="/**" access="ROLE_SPITTER"/>
pattern属性默认使用Ant风格的路径。但是也可以将<http>元素的path-type属性设置为regex,pattern属性就可以使用正则表达式了。你可以使用任意数量的<intercept-url>条目来保护Web应用程序中的各种路径。并且按从上往下的规则使用。
Spring Security 3.0版本也支持SpEL作为声明访问限制的一种方式。为了启用它,必须将<http>的use-expressions属性设置为true:
<http auto-config="true" use-expressions="true">
...
</http>
现在,我们可以在acess属性中使用SpEL表达式了。如:<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
使用HTTP提交数据是一件具有风险的事情。如果使用HTTP发送无关紧要的信息,这可能不是什么大问题。但是如果你通过HTTP发送诸如密码和信用卡账号这样的敏感信息的话,那你就是在找麻烦了。这就是为什么敏感信息要通过HTTPS来加密发送的原因。<intercept-url>元素的requiers-channel属性将通道增强的任务转移到了Spring Security配置中。如下:
<intercept-url pattern="/spitter/form" requires-channel="https"/>
不管何时,只要是对/spitter/form的请求,Spring Security都视为需要HTTPS通道并自动将请求重定向到HTTPS上。类似地,如果不需要HTTPS,可以设置为:
<intercept-url pattern="/home" requires-channel="http"/>
3、保护试图级别的元素
为了支持视图级别的安全性,Spring Security提供了一个JSP标签库。这个标签库很小且只包含3个标签,如下:
为了使用JSP标签库,需要在对应的JSP中声明:
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
4、认证用户
每个应用程序都会有些差异。每个应用程序是如何存储用户信息的就是一个显而易见的不同之处。有时它存储在关系型数据库中;有时它会存储在基于LDAP的目录中;有些应用依赖分散式的用户识别系统;还有一些可能借助不止一项策略。
Spring Security基本上能够处理任意我们需要的认证策略。Spring Security涵盖了许多常用的认证场景,包含如下的认证策略:
如果没有合适的内置用户认证策略,你还可以很容易的实现自己的认证策略,并将其装配进来。
在可用的认证策略中,最简单的一种就是直接在Spring配置中声明用户的详细信息。这可以通过使用Spring Security XML命名空间中的<user-service>元素来创建一个用户服务来实现。
<user-service id="userService">
<user name="habuma" password="letmein" authorities="ROLE_SPITTER,ROLE_ADMIN"/>
...
<user name="admin" password="admin" authorities="ROLE_ADMIN"/>
</user-service>
用户服务实际上是一个数据访问对象,它在给定用户登录ID时查找用户详细信息。
用户服务准备就绪之后,剩下的事情就是将其装配到Spring Security的认证管理器中:
<authentication-manager>
<authentication-provider user-service-ref="userService"/>
</authentication-manager>
<authentication-manager>元素会注册一个认证管理器。更确切的讲,它将注册一个ProviderManager实例,认证管理器将把认证的任务委托给一个或多个认证提供者。
在进行测试或刚刚将安全性引入进来的时候,在Spring应用上下文中定义用户详细信息是很便利的。但是在生产型的应用程序中,这种管理用户的方式并不现实。将用户详细信息存储在数据库或目录服务器中是更常见的做法。
许多应用程序将包括用户名和密码的用户信息存储在关系型数据库中。如果你的应用程序存储在关系型数据中,那Spring Security提供的<jdbc-user-service>将是一个不错的选择。
<jdbc-user-service>的使用方式与<user-service>相同。这包括将其装配到<authentication-provider>的user-service-ref属性中或者将其嵌入到<authentication-provider>中。配置如下:
<jdbc-user-service id="userService" data-source-ref="datasource"/>
<authentication-manager>
<authentication-provider user-service-ref="userService"/>
</authentication-manager>
<jdbc-user-service>元素使用了一个JDBC数据源——通过data-source-ref属性来进行装配来查询数据库并获取用户详细信息。如果没有其他的配置,用户服务将会使用如下的SQL语句来查询用户信息:
select username,password,enabled from users where username=?
尽管我们现在讨论的是用户认证,但是一部分认证会涉及查找用户被授予的权限。默认情况下,基本的<jdbc-user-service>配置将使用如下SQL语句查询指定用户名的权限:
select username,authority from authorities where username=?
对于大多数应用程序来讲,用户信息表及授权信息表不会与以上的表明匹配。幸好,<jdbc-user-service>能够方便的配置成最适合你应用程序的查询。
如:
<jdbc-user-service id="userService"
data-source-ref="dataSource"
users-by-username-query="select username, password, true from spitter where username=?"
authorities-by-username-query="select username, 'ROLE_SPITTER' from spitter where username=?"/>
关系型数据库是非常有用的,但是它们不能很好的表现层级的数据。而另一方面,LDAP目录恰好擅长存储层级数据。基于以上原因,公司的组织机构在LDAP目录中进行展现是很常见的。另外,你会发现公司的安全性限制往往对于目录中的一个条目。
为了使用基于LDAP的认证,我们首先需要使用到Spring Security的LDAP模块,并在Spring应用上下文中配置LDAP认证。当配置LDAP认证时,有两种选择:
1)使用面向LDAP的认证提供者;
2)使用面向LDAP的用户服务。
对于内存和基于数据库的用户服务,声明<authentication-provider>并装配用户服务。对于面向LDAP的用户服务,同样也可以这么做。但是一种更直接的方式是使用一个特殊的LDAP的认证提供者,可以通过在<authentication-manager>中声明<ldap-authentication-provider>来实现:
<authentication-manager alias="authenticationManager">
<ldap-authentication-provider user-search-filter="(uid={0})" group-search-filter="member={0}"/>
</authentication-manager>
属性user-search-filter和group-search-filter用于为基础LDAP查询提供过滤条件,它们分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP层级机构的根开始。但是我们可以通过指定查询基础来改变这个默认行为:
<ldap-user-service id="userService"
user-search-base="ou=people"
user-search-filter="(uid={0})"
group-search-base="ou=groups"
group-search-filter="member={0}"/>
user-search-base属性为查找用户提供了基础查询。同样,group-search-base为查找组指定了基础查询。
基于LDAP进行认证的默认策略是进行绑定操作,直接通过LDAP服务器认证用户。另一种可选的方式是进行比对操作,这涉及将输入的密码发送到LDAP目录上,并要求服务器将这个密码和用户的密码进行比对。因为比对是在LDAP服务器内完成的,实际的密码能保持私密。
如果希望通过密码比对进行认证,则可以通过声明<password-compare>元素实现:
<ldap-authentication-provider
user-search-filter="(uid={0})"
group-search-filter="member={0}">
<password-compare/>
</ldap-authentication-provider>
正如上面所声明的,在登录表单中提供的密码将会与用户的LDAP条目中的userPassword属性进行比对。
5、保护方法调用
正如前面所述,安全是一个面向切面的概念。Spring AOP是Spring Security中方法级安全性的基础。但是在大多数情况下,你没有必要直接处理Spring Security的切面。保护方法调用中所有涉及的AOP都打包进了一个元素中:<global-method-security>。如下是使用<global-method-security>的常见方式:
<global-method-security secured-annotations="enabled"/>
这将会启用Spring Security保护那些使用Spring Security自定义注解@Secured的方法。Spring Security支持4种方法级安全性的方式,这是其中之一:
1)使用@Secured注解的方法;
2)使用JSR-250@RolesAllowed注解的方法;
3)使用Spring方法调用前和调用后注解的方法;
4)匹配一个或多个明确声明的切点的方法。
使用@Secured注解保护方法调用
当<global-method-security>的secured-annotations属性被设置为enabled时,将创建一个切点来包装使用了@Secured注解的Bean方法。如:
@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle){
//...
}
如果传递给@Secured多个权限值,认证用户必须具备至少其中的一个条件才能进行方法的调用。例如,下面使用@Secured的方式表明用户必须具备ROLE_SPITTER或ROLE_ADMIN才能触发这个方法:
@Secured({"ROLE_SPITTER","ROLE_ADMIN"})
public void addSpittle(Spittle spittle){
//...
}
@Secured注解的不足之处在于它是Spring的注解。如果更倾向于使用标准注解,那么应该考虑使用@RolesAllowed注解。
使用JSR-250的@RolesAllowed注解
@RolesAllowed注解合@Secured注解在各个方面基本上都是一致的。本质区别在于@RolesAllowed是JSR-250定义的Java标准注解。
如果选择使用@RolesAllowed,则需要将<global-method-security>的jsr250-annotations属性设置为true以启用此功能:
<global-method-security jsr250-annotations="enabled"/>
jsr250-annotations和secured-annotations并不冲突。这两种注解风格可以同时启用。
使用SpEL实现调用前后的安全性
尽管@Secured和@RolesAllowed注解在拒绝未认证用户方面表现不错,但这也是它们所能做到的所有事情了。有时候,安全性限制很有意思,不仅仅涉及用户是否拥有权限。
Spring Security 3.0提供了4个新的注解,可以使用SpEL表达式来保护防护方法调用