使用 Spring Security 保护 Web 应用的安全(转)
原网站:http://www.ibm.com/developerworks/cn/java/j-lo-springsecurity/
?
在 Web 应用开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
本文详细介绍了如何使用 Spring Security 来保护 Web 应用的安全。Spring Security 本身以及 Spring 框架带来的灵活性,能够满足一般 Web 应用开发的典型需求,并允许开发人员进行定制。下面首先简单介绍 Spring Security。
?
如 代码清单 2 所示,首先定义了一个使用 Apache Derby 数据库的数据源,Spring Security 的 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
类使用该数据源来加载用户信息。最后需要配置认证管理器使用该 UserDetailsService
。
接着就可以配置用户对不同资源的访问权限了。这里的资源指的是 URL 地址。配置的内容如 代码清单 3 所示。sec
是 Spring Security 的配置元素所在的名称空间的前缀。
清单 3. 配置对不同 URL 模式的访问权限
?
第一个示例应用中一共定义了三种角色:普通用户、经理和总裁,分别用 ROLE_USER
、ROLE_MANAGER
和 ROLE_PRESIDENT
来表示。代码清单 3 中定义了访问不同的 URL 模式的用户所需要的角色。这是通过 <sec:intercept-url>
元素来实现的,其属性 pattern
声明了请求 URL 的模式,而属性 access
则声明了访问此 URL 时所需要的权限。需要按照 URL 模式从精确到模糊的顺序来进行声明。因为 Spring Security 是按照声明的顺序逐个进行比对的,只要用户当前访问的 URL 符合某个 URL 模式声明的权限要求,该请求就会被允许。如果把 代码清单 3 中本来在最后的 URL 模式 /**
声明放在最前面,那么当普通用户访问 /manager_portal.do
的时候,该请求也会被允许。这显然是不对的。通过 <sec:form-login>
元素声明了使用 HTTP 表单验证。也就是说,当未认证的用户试图访问某个受限 URL 的时候,浏览器会跳转到一个登录页面,要求用户输入用户名和密码。<sec:logout>
元素声明了提供用户注销登录的功能。默认的注销登录的 URL 是 /j_spring_security_logout
,可以通过属性 logout-url
来修改。
当完成这些配置并运行应用之后,会发现 Spring Security 已经默认提供了一个登录页面的实现,可以直接使用。开发人员也可以对登录页面进行定制。通过 <form-login>
的属性 login-page
、login-processing-url
和 authentication-failure-url
就可以定制登录页面的 URL、登录请求的处理 URL 和登录出现错误时的 URL 等。从这里可以看出,一方面 Spring Security 对开发中经常会用到的功能提供了很好的默认实现,另外一方面也提供了非常灵活的定制能力,允许开发人员提供自己的实现。
在介绍如何用 Spring Security 实现基本的用户认证和授权之后,下面介绍其中的核心对象。
?
如 代码清单 6所示,通过 mycompany.service.UserService.raiseSalary=ROLE_MANAGER
声明了 mycompany.service.UserService
类的 raiseSalary
方法只有具有角色 ROLE_MANAGER
的用户才能执行。这就使得只具有角色 ROLE_USER
的用户无法调用此方法。
不过仅对方法名称进行权限控制并不能解决另外的一些问题。比如在第一个示例应用中的增加工资的实现是通过发送 HTTP POST 请求到 salary.do
这个 URL 来完成的。salary.do
对应的控制器 mycompany.controller.SalaryController
会调用 mycompany.service.UserService
类的 raiseSalary
方法来完成增加工资的操作。存在的一种安全漏洞是具有 ROLE_MANAGER
角色的用户可以通过其它工具(如 cURL 或 Firefox 扩展 Poster 等)来创建 HTTP POST 请求来更改其它员工的工资。为了解决这个问题,需要对 raiseSalary
的调用进行更加细粒度的控制。通过 Spring Security 提供的 AspectJ 支持就可以编写相关的控制逻辑,如 代码清单 7所示。
清单 7. 使用 AspectJ 进行细粒度的控制
?
如 代码清单 8所示,需要注意的是 org.springframework.security.acls.jdbc.JdbcMutableAclService
的属性 classIdentityQuery
和 sidIdentityQuery
。Spring Security 的默认数据库模式使用了自动增长的列作为主键。而在实现中,需要能够获取到新插入的列的 ID。因此需要与数据库实现相关的 SQL 查询语言来获取到这个 ID。Spring Security 默认使用的 HSQLDB,因此这两个属性的默认值是 HSQLDB 支持的 call identity()
。如果使用的数据库不是 HSQLDB 的话,则需要根据数据库实现来设置这两个属性的值。第一个示例应用使用的是 Apache Derby 数据库,因此这两个属性的值是 values IDENTITY_VAL_LOCAL()
。对于 MySQL 来说,这个值是 select @@identity
。代码清单 9给出了使用 org.springframework.security.acls.jdbc.JdbcMutableAclService
来管理访问控制列表的 Java 代码。
清单 9. 使用访问控制列表服务
authorize
标签:该标签用来判断其中包含的内容是否应该被显示出来。判断的条件可以是某个表达式的求值结果,或是是否能访问某个 URL,分别通过属性 access
和 url
来指定。如 <sec:authorize access="hasRole('ROLE_MANAGER')">
限定内容只有具有经理角色的用户才可见。<sec:authorize url="/manager_portal.do">
限定内容只有能访问 URL/manager_portal.do
的用户才可见。authentication
标签:该标签用来获取当前认证对象(Authentication
)中的内容。如 <sec:authentication property="principal.username" />
可以用来获取当前认证用户的用户名。accesscontrollist
标签:该标签的作用与 authorize
标签类似,也是判断其中包含的内容是否应该被显示出来。所不同的是它是基于访问控制列表来做判断的。该标签的属性 domainObject
表示的是领域对象,而属性 hasPermission
表示的是要检查的权限。如 <sec:accesscontrollist hasPermission="READ" domainObject="myReport">
限定了其中包含的内容只在对领域对象 myReport
有读权限的时候才可见。值得注意的是,在使用 authorize
标签的时候,需要通过 <sec:http use-expressions="true">
来启用表达式的支持。查看 权限控制表达式一节了解关于表达式的更多内容。
在介绍完 JSP 标签库之后,下面介绍如何与 LDAP 进行集成。
?
如 代码清单 10所示,配置中的核心部分是类 org.springframework.security.ldap.authentication.LdapAuthenticationProvider
,它用来与 LDAP 服务器进行认证以及获取用户的权限信息。一般来说,与 LDAP 服务器进行认证的方式有两种。一种是使用用户提供的用户名和密码直接绑定到 LDAP 服务器;另外一种是比较用户提供的密码与 LDAP 服务器上保存的密码是否一致。前者通过类 org.springframework.security.ldap.authentication.BindAuthenticator
来实现,而后者通过类 org.springframework.security. ldap.authentication.PasswordComparisonAuthenticator
来实现。第二个示例应用中使用的是绑定的方式来进行认证。在进行绑定的时候,需要在 LDAP 服务器上搜索当前的用户。搜索的时候需要指定基本的识别名(Distinguished Name)和过滤条件。在该应用中,用户登录时使用的是其唯一识别符(uid
),如 user.0
,而在 LDAP 服务器上对应的识别名是 uid=user.0,ou=People,dc=mycompany,dc=com
。通过使用过滤条件 (&(uid={0})(objectclass=person))
就可以根据 uid
来搜索到用户并进行绑定。当认证成功之后,就需要获取到该用户对应的权限。一般是通过该用户在 LDAP 服务器上所在的分组来确定的。不过在示例应用中展示了如何提供自己的实现来为用户分配权限。类 mycompany.CompanyAuthoritiesPopulator
实现了 org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator
接口,并为所有的用户分配了单一的角色 ROLE_USER
。
在介绍完与 LDAP 进行集成之后,下面介绍如何与 OAuth 进行集成。
?
如 代码清单 11所示,只需要通过对 <oauth:resource>
元素进行简单的配置,就可以声明使用 LinkedIn 的服务。每个 <oauth:resource>
元素对应一个 OAuth 服务资源。该元素的属性包含了与该服务资源相关的信息。OAuth for Spring Security 在 Spring Security 提供的过滤器的基础上,额外增加了处理 OAuth 认证的过滤器实现。通过 <oauth:consumer>
的子元素 <oauth:url>
可以定义过滤器起作用的 URL 模式和对应的 OAuth 服务资源。当用户访问指定的 URL 的时候,应用会转到服务提供者的页面,要求用户进行授权。当用户授权之后,应用就可以访问其数据。访问数据的时候,需要在 HTTP 请求中添加额外的 Authorization
头。代码清单 12给出了访问数据时使用的代码。
清单 12. 获取访问令牌和构建 HTTP 请求
invalid-session-url
指明了会话超时之后跳转到的 URL 地址。有些 Web 应用会把用户的会话标识符直接通过 URL 的参数来传递,并且在服务器端不进行验证,如用户访问的 URL 可能是 /myurl;jsessionid=xxx
。攻击者可以用一个已知的会话标识符来构建一个 URL,并把此 URL 发给要攻击的对象。如果被攻击者访问这个 URL 并用自己的用户名登录成功之后,攻击者就可以利用这个已经通过认证的会话来访问被攻击者的数据。防范这种攻击的办法就是要求用户在做任何重要操作之前都重新认证。Spring Security 允许开发人员定制用户登录时对已有会话的处理,从而可以有效的防范这种攻击。通过 <sec:session-management>
元素的属性 session-fixation-protection
可以修改此行为。该属性的可选值有 migrateSession
、newSession
和 none
。migrateSession
是默认值。在这种情况下,每次用户登录都会创建一个新的会话,同时把之前会话的数据复制到新会话中。newSession
表示的是只创建新的会话,而不复制数据。none
表示的是保持之前的会话。
在有些情况下,应用需要限定使用同一个用户名同时进行登录所产生的会话数目。比如有些应用可能要求每个用户在同一时间最多只能有一个会话。可以通过 <sec:session-management>
元素的子元素 <sec:concurrency-control>
来限制每个用户的并发会话个数。如 <sec:concurrency-control max-sessions="2" />
就限定了每个用户在同一时间最多只能有两个会话。如果当前用户的会话数目已经达到上限,而用户又再次登录的话,默认的实现是使之前的会话失效。如果希望阻止后面的这次登录的话,可以设置属性 error-if-maximum-exceeded
的值为 true
。这样的话,后面的这次登录就会出错。只有当之前的会话失效之后,用户才能再次登录。
记住用户
有些 Web 应用会在登录界面提供一个复选框,询问用户是否希望在当前计算机上记住自己的密码。如果用户勾选此选项的话,在一段时间内用户访问此应用时,不需要输入用户名和密码进行登录。Spring Security 提供了对这种记住用户的需求的支持。只需要在 <sec:http>
中添加 <sec:remember-me>
元素即可。
一般来说,有两种方式可以实现记住用户的能力。一种做法是利用浏览器端的 cookie。当用户成功登录之后,特定内容的字符串被保存到 cookie 中。下次用户再次访问的时候,保存在 cookie 中的内容被用来认证用户。默认情况下使用的是这种方式。使用 cookie 的做法存在安全隐患,比如攻击者可能窃取用户的 cookie,并用此 cookie 来登录系统。另外一种更安全的做法是浏览器端的 cookie 只保存一些随机的数字,而且这些数字只能使用一次,在每次用户登录之后都会重新生成。这些数字保存在服务器端的数据库中。如果希望使用这种方式,需要创建一个数据库表,并通过 data-source-ref
属性来指定包含此表的数据源。
回页首
总结
对于使用 Spring 开发的 Web 应用来说,Spring Security 是增加安全性时的最好选择。本文详细介绍了 Spring Security 的各个方面,包括实现基本的用户认证和授权、保护服务层方法、使用访问控制列表保护具体的领域对象、JSP 标签库和与 LDAP 和 OAuth 的集成等。通过本文,开发人员可以了解如何使用 Spring Security 来实现不同的用户认证和授权机制。