您的位置:首页 > 编程语言 > Java开发

《Spring3实战》摘要(9)保护Spring应用(Spring Security)

2017-11-14 09:32 387 查看

第九章 保护Spring应用

9.1 Spring Security 介绍

Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案,它能够在 Web 请求级别和方法调用级别处理身份验证和授权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入和面向切面的技术。

Spring Security 从两个角度来解决安全性问题。

它使用 Servlet 过滤器保护 Web 请求并限制 URL 级别的访问,也可以使用 Spring AOP 保护方法调用—-借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。

9.1.1 Spring Security 起步

不管使用 Spring Security 保护哪种类型的应用程序,第一件需要做的事就是将 Spring Security 模块添加到应用程序的类路径下。(用到哪个模块就导入对应的 jar 包)。

Spring Security 3.0 分为了8个模块:

模块描述
ACL支持通过访问控制列表为域对象提供安全性
CAS客户端提供与 JA-SIG 的中心认证服务(CAS,Central Authentication Service)进行集成的功能
配置(config)包含了对 Spring Security XML 的命名空间的支持
核心(core)提供了 Spring Security 基本库
LDAP支持基于轻量目录访问协议(Lightweight Directory Access Protocol)进行认证
OpenID支持分散式 OpenID 标准
Tag Library包含了一组 JSP 标签来实现视图级别的安全性
Web提供了 Spring Security 基于过滤器的 Web 安全性支持
应用程序类路径下至少包含核心和配置这两个模块。

Spring Security 被用于保护 Web 应用,需要添加 Web 模块。

如果需要使用到 Spring Security 的 JSP 标签库,还要已添加 Tag Library 模块。

9.1.2 使用 Spring Security 配置命名空间

Spring Security 提供了安全性相关的命名空间,这极大简化了 Spring的安全性配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd" default-lazy-init="true">

</beans>


在 Web项目中,我们可以将安全性相关的配置拆分到一个单独的 Spring 配置文件中。鉴于这个文件中的所有配置都来自于安全性命名空间,因此我们将安全性命名空间改为这个文件的首要命名空间。

将安全性命名空间作为首要命名空间之后,我们就可以避免为所有元素添加那些令人讨厌的 “security” 前缀了。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd" default-lazy-init="true">

</beans>


9.2 保护 Web 请求

我们使用 Java Web 应用所做的任何事情都是从 HttpServletRequest 开始的。如果说请求是 Web 应用入口的话,那这也是 Web 应用的安全性起始位置。

对于请求级别的安全性来说,最基本的形式涉及声明一个或多个 URL 模式,并要求具备一定级别权限的用户才能对其进行访问,并阻止无这些权限的用户访问这些 URL 背后的内容。更进一步来讲,你可能还会要求只能通过 HTTPS 访问特定的 URL。

9.2.1 代理 Servlet 过滤器

Spring Security 借助一系列 Servlet 过滤器来提供各种安全性功能。

在应用的 web.xml 中配置一个过滤器。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<!-- DelegatingFilterProxy是一个特殊的 Servlet 过滤器,它本身所做的工作并不多,只是将工作委托给一个 javax.servlet.Filter 实现类,这个实现类作为一个 bean 注册在 Spring 应用上下文中 -->
<!-- 我们无法对注册在 web.xml 中的 Servlet 过滤器进行 Bean 注入,通过使用 DelegatingFilterProxy ,我们可以在 Spring 中配置实际的过滤器,从而能够充分利用 Spring 对依赖注入的支持。 -->
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>


DelegatingFilterProxy 的
<filter-name>
值是有意义的。这个名字用于在 Spring 应用上下文中查找过滤器 Bean。Spring Security 将自动创建一个 ID 为 springSecurityFilterChain 的过滤器 Bean,这就是我们在 web.xml 中为 DelegatingFilterProxy 所设置的 name 值。

springSecurityFilterChain 本身是另一个特殊的过滤器,它也被称为 FilterChainProxy。它可以链接任意一个或多个其他的过滤器。Spring Security 依赖一系列 Servlet 过滤器来提供不同的安全特性。但是,你几乎不需要知道这些细节,因为你不需要显式声明 springSecurityFilterChain 以及它所链接在一起的其他过滤器。当配置
<http>
元素时,Spring Security 将会为我们自动创建这些Bean。

9.2.2 配置最小化的Web安全性

<!-- 在Spring Security的配置文件中配置,表示Spring Security拦截所有URL请求(使用Ant风格的路径来声明<intercept-url>的pattern属性) -->
<!-- access属性限制了只有拥有 ROLE_SPITTER(自定义的角色)角色的认证用户才能访问。 -->
<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>


上示代码中,
<http>
元素将会自动构建一个 FilterChainProxy (它会委托给配置在 web.xml 中的 DelegatingFilterProxy)以及链中的所有过滤器 Bean。

将 auto-config 属性配置为 true ,其会为我们的应用提供一个额外的登录页、HTTP 基本认证和退出功能。实际上,将 auto-config 属性配置为 true 等价于下面这样显示配置的特性:

<http>
<form-login />
<http-basic />
<logout />
<intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>


9.2.2.1 通过表单进行登录

将 auto-config 属性配置为 true,Spring Security 将会自动为你生成登录页面。你可以通过相对于应用上下文 URL 的 /spring_security_login 路径来访问这个自动生成的登录表单。

<!-- Spring Security 自动生成的登录页面样例 -->
<html>
...
<form name="f" method="POST" action="/Spitter/j_spring_security_check">
...
<input type="text" name="j_username" value="">
...
<input type="password" name="j_password" />
...
</form>
</html>


但是这个表单并不美观,我们很可能会将其替换为自己设计的登录页面。为了设置自己的登录页,我们需要配置
<form-login>
元素来取代默认的行为:

<!--
login-page 属性:为登录页声明一个新的且相对于上下文的URL。在示例中声明登录页为 /login, 它最终由一个 Spring MVC 控制器来进行处理。
authentication-failure-url 属性:如果认证失败,将会把用户重定向到相同的登录页。
login-processing-url 属性:这是登录表单提交回来进行用户认证的 URL。-->
<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>


通过上文所示的自动生成的登录表单,我们可以知道 Spring Security 处理登录请求时,用户名和密码需要在请求中使用名为 j_username 和 j_password 的输入域来进行提交。这样,我们就可以创建自己的登录页了。

9.2.2.2 处理基本认证

对于应用程序是人类用户,基于表单的认证是比较理想的。但是在第11章中,我们将会看到如何将Web应用的页面转化为 RESTful API 。当应用程序的使用者是另外一个应用程序的话,使用表单来提示登录的方式就不太合适了。

HTTP基本认证(HTTP Basic Authentication)是直接通过HTTP请求本省来对要访问应用程序的用户进行认证的一种方式。你可能在以前见过HTTP基本认证。当在 Web 浏览器中使用时,它将向用户弹出一个简单的模拟对话框。

但这只是 Web 浏览器的显示方式。本质上,这是一个 HTTP 401 响应,表明必须在请求中包含一个用户名和密码。在 REST 客户端向它使用的服务进行认证的场景中,这种方式比较适合。

<http-basic>
元素中,并没有太多的可配置项。HTTP 基本认证要么开启要么关闭。所以,与其进一步讨论这个话题,还不如看看
<logout>
元素为我们带来了什么。

9.2.2.3 退出

<logout>
元素会构建一个 Spring Security 过滤器,这个过滤器用于使用户的会话失效。在使用的时候,通过
<logout>
构建起来的过滤器将匹配“/j_spring_security_logout”地址。但这与我们已经构建的 DispatcherServlet 并不冲突,我们需要像登录表单那样重写这个过滤器的 URL。为了做到这一点,需要设置 logout-url 特性:

<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"/>
...
<logout logout-url="/static/j_spring_security_logout" />
...
</http>


以上就是自动配置为我们带来的功能。

9.2.3 拦截请求

<intercept-url>
元素是实现请求级别安全的第一道防线。它的 pattern 属性定义了对传入请求要进行匹配的 URL 模式。如果请求匹配这个模式的话,
<intercept-url>
的安全规则就会启动。

<http auto-config="true">
<!--
pattern="/**",表明所有的请求都需要具备 ROLE_SPITTER(自定义) 角色才能进行访问。
-->
<intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>


pattern 属性默认为使用 Ant 风格的路径。可以通过将
<http>
元素的 path-type 属性设置为 regex,pattern 属性就可以使用正则表达式了。

假设 Spitter 应用程序中的一些特定区域,只有管理用户才能访问。为了实现这一点,我们可以在已有的那条记录前插入以下的
<intercept-url>
:

<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />


这条
<intercept-url>
限制这个站点的/admin分支只能由具备 ROLE_ADMIN 权限的用户才能访问。你可以使用任意数量的
<intercept-url>
条目来保护 Web 应用程序中的各种路径。但是比较重要的一点是
<intercept-url>
规则是从上往下使用的。所以。这个新的
<intercept-url>
应该放在原有记录之前,否则它会因为前面更宽泛的 /** 路径范围而失去作用。

9.2.3.1 使用 Spring 表达式进行安全保护

列出所需的权限很简单,但这却显得有些功能单一。Spring security 3.0 版本也支持 SpEL 作为声明访问限制的一种方式。为了启用它,必须将
<http>
的 use-espressions 属性设置为 true:

<http auto-config="true" use-espressions="true">
...
</http>


现在我们可以在 access 属性中使用 SpEL 表达式了。

<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />


如果当前用户被授予了给定的权限,则 hasRole() 表达式将会得到 true 值。hasRole() 只是 Spring 支持的安全相关表达式中的一种。

下表列出了 Spring Security 3.0 支持的所有 SpEL 表达式:

安全表达式计算结果
authentication用户认证对象
denyAll结果始终为false
hasAnyRole(list of roles)如果用户被授予了指定的任意权限,结果为true
hasRole(role)如果用户被授予了指定的权限,结果为true
hasIpAddress(IP Address)用户的IP地址(只能用在 Web 安全性中)
isAnonymous()如果当前用户为匿名用户,结果为true
isAuthenticated()如果当前用户不是匿名用户,结果为true
isFullyAuthenticated()如果当前用户不是匿名用户也不是remember-me认证的,结果为true
isRememberMe()如果当前用户是通过remember-me自动认证的,结果为true
permitAll结果始终为true
principal用户的主要信息对象
<!-- 示例:限制 /admin/** 这些URL,不仅需要ROLE_ADMIN,还需要指定的IP地址,才能访问 -->
<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN') and hasIpAddress('192.168.1.2')" />


9.2.3.2 强制请求使用 HTTPS

使用 HTTP 提交数据是一件具有风险的事情。如果你通过 HTTP 发送诸如密码和信用卡账号这样的敏感信息,那你就是在找麻烦了。这就是为什么敏感信息要通过 HTTPS 来加密发送的原因。

<intercept-url>
元素的 requires-channel 属性将通道增强的任务转移到了 Spring Security 配置中。

<!-- 不管何时,只要是对 /spitter/form 的请求,Spring Security 都视为需要HTTPS通道并自动将请求重定向到HTTPS上。 -->
...
<intercept-url pattern="/spitter/form" requires-channel="https" />
...
<!-- 类似地,首页不需要 HTTPS,所以我们可以声明其使用 HTTP 进行发送 -->
...
<intercept-url pattern="/home" requires-channel="http" />
...


9.3 保护视图级别的元素

为了支持视图级别的安全性,Spring Security 提供了一个 JSP 标签库。这个标签库很小且只包含 3 个标签:

JSP 标签作用
<security:accesscontrollist>
如果当前认证用户对特定的域对象具备某一指定的权限,则渲染标签主体中的内容
<security:authentication>
访问当前用户认证对象的属性
<security:authorize>
如果特定的安全性限制满足的话,则渲染标签主体中的内容
为了使用 JSP 标签库,需要在对应的 JSP 中声明它:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>


9.3.1 访问认证信息的细节

<!-- 在页面顶部显示登录用户的用户名 -->
Hello <security:authentication property="principal.username" />


property 属性用来标示用户认证对象的一个属性。可用的属性取决于用户认证的方式。但是,你可以依赖几个通用的属性,在不同的认证方式下,它们都是可用的。

认证属性描述
authorities一组用于表示用户所授权权限的 GranteAuthority 对象
credentials用于核实用户的凭据(通常是用户的密码)
details认证的附加信息(IP地址、证件序列号、会话ID等)
principal用户的主要信息对象
<!--
var 属性可为指定的变量名(示例为loginId)赋值
scope 属性设置变量的作用域,默认是当前页面。
-->
<security:authentication property="principal.username"  var="loginId" scope="request" />


9.3.2 根据权限渲染

Spring Security 的
<security:authorize>
标签能够根据用户被授予的权限有条件地渲染页面的部分内容.

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>

<!-- 只有在具有 ROLE_SPITTER 权限时,才被渲染 -->
<security:authorize access="hasRole('ROLE_SPITTER')">
<s:url value="/spittles" var="spitter_url" />
<sf:form modelAttribute="spittle" action="${spittle_url}" >
<sf:label path="text">
<s:message code="label.spittle" text="Enter spittle:" />
</sf:label>
<sf:textarea path="text" rows="2" cols="40" />
<sf:errors path="text" />
</br>
<div class="spitItSubmitIt">
<input type="submit" value="Spit it" class="status-btn round-btn disabled" />
</div>
</sf:form>
</security:authorize>


access 属性被赋值为一个 SpEL 表达式,这个表达式的值将确定
<security:authorize
标签主体内的内容是否被渲染。

<!-- 如果当前用户不是匿名用户并且用户名为'habuma'则为用户渲染此链接 -->
<security:authorize access="isAuthenticated() and principal.username=='habuma'">
<a href="/admin">Administration</a>
</security:authorize>

<!--
上例只能在视图上阻止链接的渲染,并不能阻止在浏览器中手动输入 /admin URL
可如9.2.3所示,在安全性配置文件中添加一个新的<intercept-url>
-->
<intercept-url pattern="/admin/**"
access="hasRole('ROLE_ADMIN') and hasIpAddress('192.168.1.2')" />
<!--
url 属性对一个给定的 URL 模式间接引用其安全性限制。无需如 access 属性那样明确声明安全性限制。
结合第二条配置,表示只有具备 ROLE_ADMIN 权限的认证用户,而且来自于特定IP地址的请求才能访问 /admin URL.
-->
<security:authorize url="/admin/**">
<s:url value="/admin" var="admin_url" />
<br /> <a href="${admin_url}">Admin</a>
</security:authorize>


9.4 认证用户

Spring Security 非常灵活,基本上能够处理任意我们所需的认证策略。

Spring Security 涵盖了许多常用的认证场景,包含如下的用户认证策略:

内存(基于 Spring 配置)用户存储库

基于 JDBC 的用户存储库

基于 LDAP 的用户存储库

OpenID 分散式的用户身份识别系统

中心认证服务(CAS)

X.509证书

基于 JAAS 的提供者

如果没有合适的内置用户认证策略,你还可以很容易地实现自己的认证策略,并将其装配进来。

9.4.1 配置内存用户存储库

在可用的认证策略中,最简单的一种就是直接在 Spring 配置中声明用户的详细信息。这可以通过使用 Spring Security XML 命名空间中的
<user-service>
元素来创建一个用户服务来实现。

<user-service id="userService">
<user name="habuma" password="letmein"
authorities="ROLE_SPITTER,ROLE_ADMIN" />
<user name="twoqubed" password="longhorns"
authorities="ROLE_SPITTER"/>
<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 实例,认证管理器将把认证的任务委托给一个或多个认证提供者。在本示例中,是一个依赖于用户服务的认证提供者来获取用户详细信息。

我们还可以直接将用户服务嵌入到认证提供者中:

<authentication-provider>
<user-service id="userService">
<user name="xxx" password="xxx"
authorities="aaa,bbb" />
...
</user-service>
</authentication-provider>


9.4.2 基于数据库进行认证

使用 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" />


如果没有其他的配置,用户服务将会使用如下的 SQL 语句

-- 查询用户信息
select username, password, enabled from users where username = ?

--查询用户权限
select username, authority from authorities where username = ?


大多数情况下,默认的 SQL 语句并不能正常工作。幸好,
<jdbc-user-service>
能够方便地配置成最适合你应用程序的查询。
<jdbc-user-service>
的属性能够改变查询用户详细信息的 SQL 语句。

属性作用
users-by-username-query根据用户名查询用户的用户名、,密码以及是否可用的状态
authorities-by-username-query根据用户名查询用户被授予的权限
group-authorities-by-username-query根据用户名查询用户的组权限
<!-- 修改用户认证默认SQL 的简单示例 -->
<jdbc-user-service id="userService" data-source-ref="dataSource"
users-by-username-query="select username, password from spitter where username=?"
authorities-by-username-query="select username,role  from spitter where username=?" />


9.4.3 基于 LDAP 进行认证

大多数组织机构都是等级结构化的。关系型数据库是非常有用的,但是它们不能很好地表示层级的数据。而另一方面,LDAP目录恰好擅长存储层级数据。基于以上原因,公司的组织机构在 LDAP 目录中进行展现是很常见的。另外,你会发现公司的安全性限制往往对应于目录中的一个条目。

为了使用基于 LDAP 的认证,我们首先需要使用到 Spring Security 的 LDAP 模块,并在 Spring 应用上下文中配置 LDAP 认证。当配置 LDAP 认证时,有两种选择:

使用面向 LDAP 的认证提供者 ;

使用面向 LDAP 的用户服务。

9.4.3.1 声明 LDAP 验证提供者

对于面向 LDAP 的用户服务,同样可以声明
<authentication-provider>
并装配用户服务。但是一种更直接的方式是使用一个特殊的面向 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 为查找组指定了基础查询。上例中,我们声明用户应该在名为 people 的组织单元下搜索而不是从跟开始。而组应该在名为 groups 的组织单元下搜索。

9.4.3.2 配置密码比对

基于 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 属性进行比对。如果密码被保护在不同的属性中,可以通过 password-attribute 来声明:

<password-compare hash="md5"
password-attribute="passcode" />


hash 属性可以设置为如下某个值来声明加密策略:

{sha}

{ssha}

md4

md5

plaintext

sha

sha-256

9.4.3.3 引用远程的 LDAP 服务器

默认情况下,Spring Security 的 LDAP 认证假设 LDAP 服务器监听本机的 33389 端口。但是,如果你的 LDAP 服务器在另一台机器上,那么可以使用
<ldap-authentication-provider>
元素来配置这个地址:

<ldap-server url="ldap://habuma.com:389/dc=habuma,dc=com">


9.4.3.4 配置嵌入式的 LDAP 服务器

如果你没有现成的 LDAP 服务器进行认真,那
<ldap-server>
还可以依赖嵌入式的 LDAP 服务器。只需去掉 url 参数。

<ldap-server root="dc=habuma,dc=com" />


root 属性是可选的。它的默认值是”dc=springframework,dc=org”。

当 LDAP 服务器启动时,它会尝试在类路径下查找 LDIF 文件来加载数据。LDIF(LDAP 数据交换格式)是以文本文件显示 LDAP 数据的标准方式。每条记录可以有一行或多行,每项包含一个名值对。记录之间通过空行进行分隔。

如果你想更明确指定加载哪个 LDIF 文件,可以使用 ldif 属性:

<ldap-server root="dc=habuma,dc=com"
ldif="classpath:users.ldif"/>


ldif 文件示例:

dn: ou=groups,dc=habuma,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: out=people,dc=habuma,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=habuma,ou=people,dc=habuma,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Craig Walls
sn: Walls
uid: habuma
userPassword: password

dn: uid=jsmith,ou=people,dc=habuma,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John Smith
sn: Smth
uid: jsmith
userPassword: password

dn: cn=spitter,ou=groups,dc=habuma,dc=com
objectclass: top
objectclass: groupOfNames
cn: spitter
member: uid=habuma,ou=people,dc=habuma,dc=com


9.4.4 启用 remember-me 功能

remember-me 功能,只需要你登录过一次,应用就会记住你,当再次回到应用的时候你就不需要再次登录了。

Spring Security 为应用添加 remember-me 功能非常容易。为了启用这项功能,我们只需要在
<http>
元素中添加一个
<remember-me>
元素:

<http auto-config="true" use-expressions="true">
...
<remember-me key="spitterKey"
token-validity-seconds="2419200" />
</http>


如果你在使用
<remember-me>
元素时没有配置任何属性,那么这个功能是通过在 cookie 中存储一个令牌(token)完成的,这个额令牌最多2周内有效。但是,在上例中,我们制定这个令牌最多4周内有效(2419200秒)。

存储在 cookie 中的令牌包含用户名、密码、过期时间和一个私钥—-在写入 cookie 前都进行了 MD5 哈希。默认情况下,私钥名为 SpringSecured,上例中将它设置为 spitterKey 来使其专门用于 Spitter 应用中。

既然 remember-me 功能已经启动,我们需要有一种方式来让用户表明他们希望应用程序记住他们。为了实现这一点,登录请求必须包含一个名为 _spring_security_remember_me 的参数。登录表单中的简单复选框可以完成这件事情:

<input id="remember_me" name="_spring_security_remember_me"
type="checkbox" />
<label for="remember_me" class="inline">Remember me</label>


9.5 保护方法调用

Spring AOP 是 Spring Security 中方法级安全性的基础。保护方法调用中所有涉及的 AOP 都打包进了一个元素中:
<global-method-security>


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


这将会启用 Spring Security 保护那些使用 Spring Security 自定义注解 @Secured 的方法。Spring Security 支持4种方法级安全性的方式:

使用 @Secured 注解的方法;

使用 JSR-250 @RolesAllowed 注解的方法;

使用 Spring 方法调用前和调用后注解的方法;

匹配一个或多个明确声明的切点的方法。

9.5.1 使用 @Secured 注解保护方法调用

<!-- secured-annotations="enabled"
表明将创建一个切点来包装使用了 @Secured 注解的 Bean 方法。 -->
<global-method-security secured-annotations="enabled" />

//例子1:
@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle){
//...
}


注解 @Secured 使用一个 String 数组作为参数。每个 String 值是一个权限,调用这个方法至少需要具备其中的一个权限。

如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个 Spring Security 异常(可能是 AuthenticationException 或 AccessDeniedException 的子类)。最终,这个异常必须要被捕获。如果被保护的方法是在 Web 请求中调用的,这个异常会被 Spring security 的过过滤器自动处理。否则,你需要编写代码来处理这个异常。

@Secured 注解的不足之处在于它是 Spring 的注解。如果更倾向于使用标准注解,那么你应该考虑使用 @RolesAllowed 注解。

9.5.2 使用 JSR-250 的 @RolesAllowed 注解

@RolesAllowed 注解和 @Secured 注解在各个方便基本上都是一致的。

如果选择使用 @RolesAllowed,则需要将
global-method-security
的 jsr250-annotations 属性设置为 true 以启用此功能:

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


jsr250-annotations 与 secured-annotations 并不冲突,这两种注解风格可以同时开启。它们甚至可以与 Spring 的方法调用前后安全性注解共同使用,这也是我们接下来讲解的内容。

9.5.3 使用 SpEL 实现调用前后的安全性

尽管 @Secured 和 @RolesAllowed 注解在拒绝为认证用户方面表现不错,但这也是他们所能做到的所有事情了。有时候,安全性限制不仅仅涉及用户是否拥有权限。

Spring Security 3.0 引入了几个新注解,它们使用 SpEL 能够在方法调用上实现更有意思的安全性限制。

Spring security 3.0 提供了4个新的注解,可以使用 SpEL 表达式来保护方法调用:

注解描述
@PreAuthorize在方法调用之前,基于表达式的计算结果来限制对方法的访问
@PostAuthorize允许方法调用,但是如果表达式计算结果为false,则会抛出一个安全性异常
@PostFilter允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter允许方法调用,但必须在进入方法之前过滤输入值
如果你希望使用这些注解,则需要将
<global-method-security>
的 pre-post-annotations 属性设置为 enabled 来启用它们:

<global-method-security pre-post-annotations="enabled" />


9.5.3.1 在方法调用前验证权限

// 示例1:只允许拥有 ROLE_SPITTER 角色的用户访问方法
@PreAuthorize("hasRole('ROLE_SPITTER')")
public  void addSpittle(Spittle spittle){
//...
}

// 示例2:ROLE_SPITTER角色的用户传入的 spittle.text 的长度需小于等于140方可访问方法,对 ROLE_PREMIUM 角色用户无限制
//表达式中的 #spittle 部分直接引用了方法中的同名参数。这使得Spring Security 能够检查传入方法的参数。
@PreAuthorize("hasRole('ROLE_SPITTER')
and #spittle.text.length() <= 140
or hasRole('ROLE_PREMIUM')")
public  void addSpittle(Spittle spittle){
//...
}


9.5.3.2 在方法调用后验证权限

在方法调用之后验证权限并不是比较常见的方式。事后验证一般用于基于安全保护方法的返回值来进行安全性决策的场景中。这种情况意味着方法必须被调用执行并且得到了返回值。

除了验证的时机之外,@PostAuthorize 与 @PreAuthorize 的工作方式差不多。

示例:
@PostAuthorize("returnObject.spitter.username == principal.username")
public Spittle getSpittleById(Long id){
//...
}


为了便利地访问受保护方法的返回对象,Spring Security 在 SpEL 提供了 returnObject 变量名。在上述示例中,表达式通过returnObject.spitter.username直接访问返回值中spittle属性中的 username 属性。

上述示例中,表达式到内置的 principal 对象中取出其 username 属性。principal 是另一个 Spring Security 内置的特殊名字,它代表了当前认证用户的主要信息。

需要注意的是,@PostAuthorize 注解的方法会首先执行然后被拦截。这意味着,你需要确保一旦验证失败不会出现一些负面的结果。

9.5.3.3 事后对方法返回值进行过滤

// 示例1:
@PreAuthorize("hasRole('ROLE_SPITTER')")
@PostFilter("filterObject.spitter.username == principal.name")
public List<Spittle> getABunchOfSpittles() {
//...
}


上述示例1中,@PostAuthorize 注解只允许具有 ROLE_SPITTER 权限的用户执行这个方法。如果用户通过了这个检查点,方法将会被调用并返回一个 Spittle 的 List。但是 @PostFilter 注解将过滤这个列表,确保用户只能看到属于自己的 Spittle 对象。

//示例2:过滤掉用户对当前 filterObject(示例中为Spittle对象) 没有删除权限的对象
@PreAuthorize("hasRole('ROLE_SPITTER')")
@PostFilter("hasPermission(filterObject,'delete')")
public List<Spittle> getABunchOfSpittles() {
//...
}


实际上 hasPermission() 默认一直返回 false。需要重写 hasPermission() 的默认行为,这涉及到创建和注册一个许可计算器。示例代码如下:

import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import com.spring.springdemo.spittermvc.entity.Spittle;

public class SpittlePermissionEvaluator implements PermissionEvaluator {

@Override
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
if(target instanceof Spittle){
Spittle spittle = (Spittle) target;
if("delete".equals(permission)){
return spittle.getSpitter().getUsername()
.equals(authentication.getName())
// hasProfanity()方法表示是否有侮辱性的词汇
|| hasProfanity(spittle);
}

}
throw new UnsupportedOperationException("hasPermission not supported for object <"
+ target
+ "> and permission <" + permission +">");
}

@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
throw new UnsupportedOperationException();
}

private boolean hasProfanity(Spittle spittle){
// ...
return false;
}
}


SpittlePermissionEvaluator 实现了 Spring Security 的 PermissionEvaluator 接口,它需要实现两个不同的 hasPermission() 方法。其中的一个 hasPermission() 方法把要评估的对象作为第二个参数。第二个 hasPermission() 方法在只有目标对象的 ID 可以得到的时候才有用,并将 ID 作为 Serializable 传入第二个参数。(这里只是简单地抛出异常)。

许可计算器已经准备就绪,你需要将其注册到 Spring Security 中,以便在使用 @PostFilter 时支持 haspermission() 操作。要做到这一点需要创建一个表达式处理器并注册到
<global-method-security>
中。

对于表达式处理器,你需要创建 DefaultmethodSecurityExpressionHandler 类型的 Bean,并将 SpittlePermissionEvaluator 的实例作为它的 permissionEvalutor 属性注入进去。

<beans:bean id="expressionHandler"
class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<beans:property name="permissionEvalutor">
<beans:bean class="com.xxx.xxx.SpittlePermissionEvaluator" />
</beans:property>
</beans:bean>


接下来,我们就可以在
<global-method-security>
中配置 expressionHandler,如下所示:

<global-method-security pre-post-annotations="enabled">
<expression-handler ref="expressionHandler" />
</global-method-security>


以前,在配置
<global-method-security>
时,我们没有指定表达式处理器。但是在这里,配置了帮我们计算的表达式处理器,用于替换默认的表达式处理器。

9.5.4 声明方法级别的安全性切点

方法级别的安全性限制在不同的方法见往往有所差别。为每个方法添加最合适的约束注解有很大的意义。但是,有时候为几个方法设置相同的授权检查也是很有意义的,也称为横切的授权。

为了限制对多个方法进行访问,可以使用
<protect-pointcut>
作为
<global-method-security>
元素的子元素。例如:

<global-method-security>
<protect-pointcut access="ROLE_SPITTER"
expression="execution(@com.xx.xxx.Sensitive * *.*(String))" />
</global-method-security>


expression 属性被设置成了一个 AspectJ 切面表达式。在本示例中,它标示了所有使用 @Sensitive 自定义注解的方法。同时,access 属性标示认证用户需要什么样的权限才能访问 expression 属性所指定的方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: