使用 Spring Security 保护 Web 应用的安全 转载

在 Web 应用开发中,安全一直是很是重要的一个方面。安全虽然属于应用的非功能性需求,可是应该在应用开发的初期就考虑进来。若是在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,没法知足用户的要求,并可能形成用户的隐私数据被攻击者窃取;另外一方面,应用的基本架构已经肯定,要修复安全漏洞,可能须要对系统的架构作出比较重大的调整,于是须要更多的开发时间,影响应用的发布进程。所以,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程当中。html

本文详细介绍了如何使用 Spring Security 来保护 Web 应用的安全。Spring Security 自己以及 Spring 框架带来的灵活性,可以知足通常 Web 应用开发的典型需求,并容许开发人员进行定制。下面首先简单介绍 Spring Security。java

Spring Security 简介web

Spring 是一个很是流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。通常来讲,Web 应用的安全性包括用户认证(Authentication)和用户受权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户可否访问该系统。用户认证通常要求用户提供用户名和密码。系统经过校验用户名和密码来完成认证过程。用户受权指的是验证某个用户是否有权限执行某个操做。在一个系统中,不一样用户所具备的权限是不一样的。好比对一个文件来讲,有的用户只能进行读取,而有的用户能够进行修改。通常来讲,系统会为不一样的用户分配不一样的角色,而每一个角色则对应一系列的权限。spring

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户受权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),能够对应用中的领域对象进行细粒度的控制。sql

本文将经过三个具体的示例来介绍 Spring Security 的使用。第一个示例是一个简单的企业员工管理系统。该系统中存在三类用户,分别是普通员工、经理和总裁。不一样类别的用户所能访问的资源不一样。对这些资源所能执行的操做也不相同。Spring Security 能帮助开发人员以简单的方式知足这些安全性相关的需求。第二个示例展现了如何与 LDAP 服务器进行集成。第三个示例展现了如何与 OAuth 进行集成。完整的示例代码见 参考资料。下面首先介绍基本的用户认证和受权的实现。数据库

基本用户认证和受权express

本节从最基本的用户认证和受权开始对 Spring Security 进行介绍。通常来讲,Web 应用都须要保存本身系统中的用户信息。这些信息通常保存在数据库中。用户能够注册本身的帐号,或是由系统管理员统一进行分配。这些用户通常都有本身的角色,如普通用户和管理员之类的。某些页面只有特定角色的用户能够访问,好比只有管理员才能够访问 /admin 这样的网址。下面介绍如何使用 Spring Security 来知足这样基本的认证和受权的需求。apache

首先须要把 Spring Security 引入到 Web 应用中来,这是经过在 web.xml添加一个新的过滤器来实现的,如 代码清单 1 所示。编程


清单 1. 在 web.xml 中添加 Spring Security 的过滤器
<filter> 
    <filter-name>springSecurityFilterChain</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
 </filter> 

 <filter-mapping> 
    <filter-name>springSecurityFilterChain</filter-name> 
    <url-pattern>/*</url-pattern> 
 </filter-mapping>

Spring Security 使用的是 Servlet 规范中标准的过滤器机制。对于特定的请求,Spring Security 的过滤器会检查该请求是否经过认证,以及当前用户是否有足够的权限来访问此资源。对于非法的请求,过滤器会跳转到指定页面让用户进行认证,或是返回出错信息。须要注意的是,代码清单 1 中虽然只定义了一个过滤器,Spring Security 其实是使用多个过滤器造成的链条来工做的。api

下一步是配置 Spring Security 来声明系统中的合法用户及其对应的权限。用户相关的信息是经过 org.springframework.security.core.userdetails.UserDetailsService 接口来加载的。该接口的惟一方法是 loadUserByUsername(String username),用来根据用户名加载相关的信息。这个方法的返回值是 org.springframework.security.core.userdetails.UserDetails 接口,其中包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、是否过时等。其中最重要的是用户权限,由 org.springframework.security.core.GrantedAuthority 接口来表示。虽然 Spring Security 内部的设计和实现比较复杂,可是通常状况下,开发人员只须要使用它默认提供的实现就能够知足绝大多数状况下的需求,并且只须要简单的配置声明便可。

在第一个示例应用中,使用的是数据库的方式来存储用户的信息。Spring Security 提供了 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 类来支持从数据库中加载用户信息。开发人员只须要使用与该类兼容的数据库表结构,就能够不须要任何改动,而直接使用该类。代码清单 2 中给出了相关的配置。


清单 2. 声明使用数据库来保存用户信息
<bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 
    <property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver" /> 
    <property name="url" value="jdbc:derby://localhost:1527/mycompany" /> 
    <property name="username" value="app" /> 
    <property name="password" value="admin" /> 
 </bean> 

 <bean id="userDetailsService" 
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> 
    <property name="dataSource" ref="dataSource" /> 
 </bean> 

 <sec:authentication-manager> 
    <sec:authentication-provider user-service-ref="userDetailsService" /> 
 </sec:authentication-manager>

代码清单 2 所示,首先定义了一个使用 Apache Derby 数据库的数据源,Spring Security 的 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 类使用该数据源来加载用户信息。最后须要配置认证管理器使用该 UserDetailsService

接着就能够配置用户对不一样资源的访问权限了。这里的资源指的是 URL 地址。配置的内容如 代码清单 3 所示。sec 是 Spring Security 的配置元素所在的名称空间的前缀。


清单 3. 配置对不一样 URL 模式的访问权限
<sec:http> 
    <sec:intercept-url pattern="/president_portal.do**" access="ROLE_PRESIDENT" /> 
    <sec:intercept-url pattern="/manager_portal.do**" access="ROLE_MANAGER" /> 
    <sec:intercept-url pattern="/**" access="ROLE_USER" /> 
    <sec:form-login /> 
    <sec:logout /> 
 </sec:http>

第一个示例应用中一共定义了三种角色:普通用户、经理和总裁,分别用 ROLE_USERROLE_MANAGERROLE_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-pagelogin-processing-urlauthentication-failure-url就能够定制登陆页面的 URL、登陆请求的处理 URL 和登陆出现错误时的 URL 等。从这里能够看出,一方面 Spring Security 对开发中常常会用到的功能提供了很好的默认实现,另一方面也提供了很是灵活的定制能力,容许开发人员提供本身的实现。

在介绍如何用 Spring Security 实现基本的用户认证和受权以后,下面介绍其中的核心对象。

回页首

SecurityContext 和 Authentication 对象

下面开始讨论几个 Spring Security 里面的核心对象。org.springframework.security.core.context.SecurityContext接口表示的是当前应用的安全上下文。经过此接口能够获取和设置当前的认证对象。org.springframework.security.core.Authentication接口用来表示此认证对象。经过认证对象的方法能够判断当前用户是否已经经过认证,以及获取当前认证用户的相关信息,包括用户名、密码和权限等。要使用此认证对象,首先须要获取到 SecurityContext 对象。经过 org.springframework.security.core.context.SecurityContextHolder 类提供的静态方法 getContext() 就能够获取。再经过 SecurityContext对象的 getAuthentication()就能够获得认证对象。经过认证对象的 getPrincipal() 方法就能够得到当前的认证主体,一般是 UserDetails 接口的实现。联系到上一节介绍的 UserDetailsService,典型的认证过程就是当用户输入了用户名和密码以后,UserDetailsService经过用户名找到对应的 UserDetails 对象,接着比较密码是否匹配。若是不匹配,则返回出错信息;若是匹配的话,说明用户认证成功,就建立一个实现了 Authentication接口的对象,如 org.springframework.security. authentication.UsernamePasswordAuthenticationToken 类的对象。再经过 SecurityContextsetAuthentication() 方法来设置此认证对象。

代码清单 4 给出了使用 SecurityContextAuthentication的一个示例,用来获取当前认证用户的用户名。


清单 4. 获取当前认证用户的用户名
public static String getAuthenticatedUsername() { 
    String username = null; 
    Object principal = SecurityContextHolder.getContext() 
        .getAuthentication().getPrincipal(); 
    if (principal instanceof UserDetails) { 
        username = ((UserDetails) principal).getUsername(); 
    } else { 
        username = principal.toString(); 
    } 
    return username; 
 }

默认状况下,SecurityContextHolder使用 ThreadLocal来保存 SecurityContext对象。所以,SecurityContext对象对于当前线程上全部方法都是可见的。这种实现对于 Web 应用来讲是合适的。不过在有些状况下,如桌面应用,这种实现方式就不适用了。Spring Security 容许开发人员对此进行定制。开发人员只须要实现接口 org.springframework.security.core.context.SecurityContextHolderStrategy并经过 SecurityContextHoldersetStrategyName(String)方法让 Spring Security 使用此实现便可。另一种设置方式是使用系统属性。除此以外,Spring Security 默认提供了另外两种实现方式:MODE_GLOBAL表示当前应用共享惟一的 SecurityContextHolderMODE_INHERITABLETHREADLOCAL表示子线程继承父线程的 SecurityContextHolder代码清单 5给出了使用全局惟一的 SecurityContextHolder的示例。


清单 5. 使用全局惟一的 SecurityContextHolder
public void useGlobalSecurityContextHolder() { 
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); 
 }

在介绍完 Spring Security 中的 SecurityContextAuthentication以后,下面介绍如何保护服务层的方法。

回页首

服务层方法保护

以前章节中介绍的是在 URL 这个粒度上的安全保护。这种粒度的保护在不少状况下是不够的。好比相同的 URL 对应的页面上,不一样角色的用户所能看到的内容和执行的操做是有可能不一样的。在第一个示例应用中,系统中记录了每一个员工的工资收入。全部员工均可以查看本身的工资,可是只有员工的直接经理才能够修改员工的工资。这就涉及到对应用中服务层的方法进行相应的权限控制,从而避免安全漏洞。

保护服务层方法涉及到对应用中的方法调用进行拦截。经过 Spring 框架提供的良好面向方面编程(AOP)的支持,能够很容易的对方法调用进行拦截。Spring Security 利用了 AOP 的能力,容许以声明的方式来定义调用方式时所需的权限。代码清单 6中给出了对方法调用进行保护的配置文件示例。


清单 6. 对方法调用进行保护
<bean id="userSalarySecurity" 
    class="org.springframework.security.access.intercept.aspectj. 
        AspectJMethodSecurityInterceptor"> 
    <property name="authenticationManager" ref="authenticationManager" /> 
    <property name="accessDecisionManager" ref="accessDecisionManager" /> 
    <property name="securityMetadataSource"> 
        <value> 
            mycompany.service.UserService.raiseSalary=ROLE_MANAGER 
        </value> 
    </property> 
 </bean>

代码清单 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 进行细粒度的控制
public aspect SalaryManagementAspect { 
    private AspectJMethodSecurityInterceptor securityInterceptor; 

    private UserDao userDao; 

    pointcut salaryChange(): target(UserService) 
        && execution(public void raiseSalary(..)) &&!within(SalaryManagementAspect); 

    Object around(): salaryChange() { 
        if (this.securityInterceptor == null) { 
            return proceed(); 
        } 
        AspectJCallback callback = new AspectJCallback() { 
            public Object proceedWithObject() { 
                return proceed(); 
            } 
        }; 
        Object[] args = thisJoinPoint.getArgs(); 
        String employee = (String) args[0]; // 要修改的员工的用户名
        User user = userDao.getByUsername(employee); 
        String currentUser = UsernameHolder.getAuthenticatedUsername(); // 当前登陆用户
        if (!currentUser.equals(user.getManagerId())) { 
            throw new AccessDeniedException 
                ("Only the direct manager can change the salary."); 
        } 

        return this.securityInterceptor.invoke(thisJoinPoint, callback); 
    } 
 }

代码清单 7所示,定义了一个切入点(pointcut)salaryChange和对应的环绕加强。当方法 raiseSalary被调用的时候,会比较要修改的员工的经理的用户名和当前登陆用户的用户名是否一致。当不一致的时候就会抛出 AccessDeniedException异常。

在介绍了如何保护方法调用以后,下面介绍如何经过访问控制列表来保护领域对象。

回页首

访问控制列表

以前提到的安全保护和权限控制都是只针对 URL 或是方法调用,只对一类对象起做用。而在有些状况下,不一样领域对象实体所要求的权限控制是不一样的。以第一类示例应用来讲,系统中有报表这一类实体。因为报表的特殊性,只有具备角色 ROLE_PRESIDENT的用户才能够建立报表。对于每份报表,建立者能够设定其对于不一样用户的权限。好比有的报表只容许特定的几个用户能够查看。对于这样的需求,就须要对每一个领域对象的实例设置对应的访问控制权限。Spring Security 提供了对访问控制列表(Access Control List,ACL)的支持,能够很方便的对不一样的领域对象设置针对不一样用户的权限。

Spring Security 中的访问控制列表的实现中有 3 个重要的概念,对应于 4 张数据库表。

  • 受权的主体:通常是系统中的用户。由 ACL_SID表来表示。
  • 领域对象:表示系统中须要进行访问控制的实体。由 ACL_CLASSACL_OBJECT_IDENTITY表来表示,前者保存的是实体所对应的 Java 类的名称,然后者保存的是实体自己。
  • 访问权限:表示一个用户对一个领域对象所具备的权限。由表 ACL_ENTRY来表示。

Spring Security 已经提供了参考的数据库表模式和相应的基于 JDBC 的实现。在大多数状况下,使用参考实现就能够知足需求了。类 org.springframework.security.acls.jdbc.JdbcMutableAclService能够对访问控制列表进行查询、添加、更新和删除的操做,是开发人员最常直接使用的类。该类的构造方法须要 3 个参数,分别是 javax.sql.DataSource表示的数据源、org.springframework.security.acls.jdbc.LookupStrategy表示的数据库的查询策略和 org.springframework.security.acls.model.AclCache表示的访问控制列表缓存。数据源可使用第一个示例应用中已有的数据源。查询策略可使用默认的实现 org.springframework.security.acls.jdbc.BasicLookupStrategy。缓存可使用基于 EhCache 的缓存实现 org.springframework.security.acls.domain.EhCacheBasedAclCache代码清单 8中给出了相关代码。


清单 8. 使用 JDBC 的访问控制列表服务基本配置
<bean id="aclService"
    class="org.springframework.security.acls.jdbc.JdbcMutableAclService"> 
    <constructor-arg ref="dataSource" /> 
    <constructor-arg ref="lookupStrategy" /> 
    <constructor-arg ref="aclCache" /> 
    <property name="classIdentityQuery" value="values IDENTITY_VAL_LOCAL()"/> 
    <property name="sidIdentityQuery" value="values IDENTITY_VAL_LOCAL()"/> 	
 </bean>

代码清单 8所示,须要注意的是 org.springframework.security.acls.jdbc.JdbcMutableAclService的属性 classIdentityQuerysidIdentityQuery。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. 使用访问控制列表服务
public void createNewReport(String title, String content) throws ServiceException { 
    final Report report = new Report(); 
    report.setTitle(title); 
    report.setContent(content); 
		
    transactionTemplate.execute(new TransactionCallback<Object>() { 
        public Object doInTransaction(TransactionStatus status) { 
            reportDao.create(report); 
            addPermission(report.getId(), new PrincipalSid(getUsername()), 
                BasePermission.ADMINISTRATION); 
            return null; 
        } 
    }); 
 } 
	
 public void grantRead(final String username, final Long reportId) { 
    transactionTemplate.execute(new TransactionCallback<Object>() { 
        public Object doInTransaction(TransactionStatus status) { 
            addPermission(reportId, new PrincipalSid(username), BasePermission.READ); 
            return null; 
        } 
    }); 
 } 

 private void addPermission(Long reportId, Sid recipient, Permission permission) { 
    MutableAcl acl; 
    ObjectIdentity oid = new ObjectIdentityImpl(Report.class, reportId); 

    try { 
        acl = (MutableAcl) mutableAclService.readAclById(oid); 
    } catch (NotFoundException nfe) { 
        acl = mutableAclService.createAcl(oid); 
    } 

    acl.insertAce(acl.getEntries().size(), permission, recipient, true); 
    mutableAclService.updateAcl(acl); 
 }

代码清单 9中的 addPermission(Long reportId, Sid recipient, Permission permission)方法用来为某个报表添加访问控制权限,参数 reportId表示的是报表的 ID,用来标识一个报表;recipient表示的是须要受权的用户;permission表示的是授予的权限。createNewReport()方法用来建立一个报表,同时给建立报表的用户授予管理权限(BasePermission.ADMINISTRATION)。grantRead()方法用来给某个用户对某个报表授予读权限(BasePermission.READ)。这里须要注意的是,对访问控制列表的操做都须要在一个事务中进行处理。利用 Spring 提供的事务模板(org.springframework.transaction.support.TransactionTemplate)就能够很好的处理事务。对于权限,Spring Security 提供了 4 种基本的权限:读、写、删除和管理。开发人员能够在这基础上定义本身的权限。

在介绍完访问控制列表以后,下面介绍 Spring Security 提供的 JSP 标签库。

回页首

JSP 标签库

以前的章节中介绍了在 Java 代码中如何使用 Spring Security 提供的能力。不少状况下,用户可能有权限访问某个页面,可是页面上的某些功能对他来讲是不可用的。好比对于一样的员工列表,普通用户只能查看数据,而具备经理角色的用户则能够看到对列表进行修改的连接或是按钮等。Spring Security 提供了一个 JSP 标签库用来方便在 JSP 页面中根据用户的权限来控制页面某些部分的显示和隐藏。使用这个 JSP 标签库很简单,只须要在 JSP 页面上添加声明便可:<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>。这个标签库包含以下 3 个标签:

  • authorize标签:该标签用来判断其中包含的内容是否应该被显示出来。判断的条件能够是某个表达式的求值结果,或是是否能访问某个 URL,分别经过属性 accessurl来指定。如 <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 进行集成。

回页首

使用 LDAP

不少公司都使用 LDAP 服务器来保存员工的相关信息。内部的 IT 系统都须要与 LDAP 服务器作集成来进行用户认证与访问受权。Spring Security 提供了对 LDAP 协议的支持,只须要简单的配置就可让 Web 应用使用 LDAP 来进行认证。第二个示例应用使用 OpenDS LDAP 服务器并添加了一些测试用户。代码清单 10中给出了配置文件的示例,完整的代码见 参考资料


清单 10. 集成 LDAP 服务器的配置文件
<bean id="contextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource"> 
    <constructor-arg value="ldap://localhost:389" /> 

 </bean> 
   
 <bean id="ldapAuthProvider"
    class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider"> 
    <constructor-arg> 
        <bean class="org.springframework.security.ldap.authentication.BindAuthenticator"> 
            <constructor-arg ref="contextSource" /> 
            <property name="userSearch"> 
                <bean id="userSearch" 
        class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch"> 
                    <constructor-arg index="0" value="ou=People,dc=mycompany,dc=com" /> 
                    <constructor-arg index="1" 
                value="(&amp;(uid={0})(objectclass=person))" /> 
                    <constructor-arg index="2" ref="contextSource" /> 
                </bean> 
            </property> 
        </bean> 
    </constructor-arg> 
    <constructor-arg> 
        <bean class="mycompany.CompanyAuthoritiesPopulator"></bean> 
    </constructor-arg> 
 </bean> 

 <sec:authentication-manager> 
    <sec:authentication-provider ref="ldapAuthProvider" /> 
 </sec:authentication-manager>

代码清单 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。经过使用过滤条件 (&amp;(uid={0})(objectclass=person))就能够根据 uid来搜索到用户并进行绑定。当认证成功以后,就须要获取到该用户对应的权限。通常是经过该用户在 LDAP 服务器上所在的分组来肯定的。不过在示例应用中展现了如何提供本身的实现来为用户分配权限。类 mycompany.CompanyAuthoritiesPopulator实现了 org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator接口,并为全部的用户分配了单一的角色 ROLE_USER

在介绍完与 LDAP 进行集成以后,下面介绍如何与 OAuth 进行集成。

 

OAuth 集成

如今的不少 Web 服务都提供 API 接口,容许第三方应用使用其数据。当第三方应用须要访问用户私有数据的时候,须要进行认证。OAuth 是目前流行的一种认证方式,被不少 Web 服务采用,包括 Twitter、LinkedIn、Google Buzz 和新浪微博等。OAuth 的特色是第三方应用不能直接获取到用户的密码,而只是使用一个通过用户受权以后的令牌(token)来进行访问。用户能够对可以访问其数据的第三方应用进行管理,经过回收令牌的方式来终止第三方应用对其数据的访问。OAuth 的工做方式涉及到服务提供者、第三方应用和用户等 3 个主体。其基本的工做流程是:第三方应用向服务提供者发出访问用户数据的请求。服务提供者会询问用户是否赞成此请求。若是用户赞成的话,服务提供者会返回给第三方应用一个令牌。第三方应用只须要在请求数据的时候带上此令牌就能够成功获取。

第三方应用在使用 OAuth 认证方式的时候,其中所涉及的交互比较复杂。Spring Security 自己并无提供 OAuth 的支持,经过另一个开源库 OAuth for Spring Security 能够实现。OAuth for Spring Security 与 Spring Security 有着很好的集成,能够很容易在已有的使用 Spring Security 的应用中添加 OAuth 的支持。不过目前 OAuth for Spring Security 只对 Spring Security 2.0.x 版本提供比较好的支持。对 OAuth 的支持包括服务提供者和服务消费者两个部分:服务提供者是数据的提供者,服务消费者是使用这些数据的第三方应用。通常的应用都是服务消费者。OAuth for Spring Security 对服务提供者和消费者都提供了支持。下面经过获取 LinkedIn 上的状态更新的示例来讲明其用法。

做为 OAuth 的服务消费者,须要向服务提供者申请表示其应用的密钥。服务提供者会提供 3 个 URL 来与服务消费者进行交互。代码清单 11中给出了使用 OAuth for Spring Security 的配置文件。


清单 11. 使用 OAuth for Spring Security 的配置文件
<oauth:consumer resource-details-service-ref="linkedInResourceDetails"
    oauth-failure-page="/oauth_error.jsp"> 
    <oauth:url pattern="/linkedin.do**" resources="linkedIn" /> 
 </oauth:consumer> 

 <bean id="oauthConsumerSupport"
    class="org.springframework.security.oauth.consumer.CoreOAuthConsumerSupport"> 
    <property name="protectedResourceDetailsService" ref="linkedInResourceDetails" /> 
 </bean> 

 <oauth:resource-details-service id="linkedInResourceDetails"> 
    <oauth:resource id="linkedIn"
        key="***" secret="***"
        request-token-url="https://api.linkedin.com/uas/oauth/requestToken"
        user-authorization-url="https://www.linkedin.com/uas/oauth/authorize"
        access-token-url="https://api.linkedin.com/uas/oauth/accessToken" /> 
 </oauth:resource-details-service>

代码清单 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 请求
public OAuthConsumerToken getAccessTokenFromRequest(HttpServletRequest request) { 
    OAuthConsumerToken token = null; 

    List<OAuthConsumerToken> tokens = (List<OAuthConsumerToken>) request 
        .getAttribute(OAuthConsumerProcessingFilter.ACCESS_TOKENS_DEFAULT_ATTRIBUTE); 
    if (tokens != null) { 
        for (OAuthConsumerToken consumerToken : tokens) { 
            if (consumerToken.getResourceId().equals(resourceId)) { 
                token = consumerToken; 
                break; 
            } 
        } 
    } 
    return token; 
 } 

 public GetMethod getGetMethod(OAuthConsumerToken accessToken, URL url) { 
    GetMethod method = new GetMethod(url.toString()); 
    method.setRequestHeader("Authorization", 
				 getHeader(accessToken, url, "GET")); 
    return method; 
 } 

 public String getHeader(OAuthConsumerToken accessToken, URL url, 
			 String method) { 
    ProtectedResourceDetails details = support 
        .getProtectedResourceDetailsService() 
        .loadProtectedResourceDetailsById(accessToken.getResourceId()); 
    return support.getAuthorizationHeader(details, accessToken, url, method, null); 
 }

代码清单 12所示,OAuth for Spring Security 的过滤器会把 OAuth 认证成功以后的令牌保存在当前的请求中。经过 getAccessTokenFromRequest()方法就能够从请求中获取到此令牌。有了这个令牌以后,就能够经过 getHeader()方法构建出 HTTP 请求所需的 Authorization头。只须要在请求中添加此 HTTP 头,就能够正常访问到所需的数据。默认状况下,应用的 OAuth 令牌是保存在 HTTP 会话中的,开发人员能够提供其它的令牌保存方式,如保存在数据库中。只须要提供 org.springframework.security.oauth.consumer.token.OAuthConsumerTokenServices接口的实现就能够了。

在介绍完与 OAuth 的集成方式以后,下面介绍一些高级话题。

 

高级话题

这些与 Spring Security 相关的高级话题包括权限控制表达式、会话管理和记住用户等。

权限控制表达式

有些状况下,对于某种资源的访问条件可能比较复杂,并不仅是简单的要求当前用户具备某一个角色便可,而是由多种条件进行组合。权限控制表达式容许使用一种简单的语法来描述比较复杂的受权条件。Spring Security 内置了一些经常使用的表达式,包括 hasRole()用来判断当前用户是否具备某个角色,hasAnyRole()用来判断当前用户是否具有列表中的某个角色,以及 hasPermission()用来判断当前用户是否具有对某个领域对象的某些权限等。这些基本表达式能够经过 andor等组合起来,表示复杂的语义。当经过 <sec:http use-expressions="true">启用了表达式支持以后,就能够在 <sec:intercept-url>元素的 access属性上使用表达式。

表达式还能够用来对方法调用进行权限控制,主要是用在方法注解中。要启用 Spring Security 提供的方法注解,须要添加元素 <global-method-security pre-post-annotations="enabled"/>。这几个方法注解分别是:

  • @PreAuthorize:该注解用来肯定一个方法是否应该被执行。该注解后面跟着的是一个表达式,若是表达式的值为真,则该方法会被执行。如 @PreAuthorize("hasRole('ROLE_USER')")就说明只有当前用户具备角色 ROLE_USER的时候才会执行。
  • @PostAuthorize:该注解用来在方法执行完以后进行访问控制检查。
  • @PostFilter:该注解用来对方法的返回结果进行过滤。从返回的集合中过滤掉表达式值为假的元素。如 @PostFilter("hasPermission(filterObject, 'read')")说明返回的结果中只保留当前用户有读权限的元素。
  • @PreFilter:该注解用来对方法调用时的参数进行过滤。

会话管理

Spring Security 提供了对 HTTP 会话的管理功能。这些功能包括对会话超时的管理、防范会话设置攻击(Session fixation attack)和并发会话管理等。

若是当前用户的会话由于超时而失效以后,若是用户继续使用此会话来访问,Spring Security 能够检测到这种状况,并跳转到适当的页面。只须要在 <sec:http>元素下添加 <sec:session-management invalid-session-url="/sessionTimeout.jsp" />元素便可,属性 invalid-session-url指明了会话超时以后跳转到的 URL 地址。

有些 Web 应用会把用户的会话标识符直接经过 URL 的参数来传递,而且在服务器端不进行验证,如用户访问的 URL 多是 /myurl;jsessionid=xxx。攻击者能够用一个已知的会话标识符来构建一个 URL,并把此 URL 发给要攻击的对象。若是被攻击者访问这个 URL 并用本身的用户名登陆成功以后,攻击者就能够利用这个已经经过认证的会话来访问被攻击者的数据。防范这种攻击的办法就是要求用户在作任何重要操做以前都从新认证。Spring Security 容许开发人员定制用户登陆时对已有会话的处理,从而能够有效的防范这种攻击。经过 <sec:session-management>元素的属性 session-fixation-protection能够修改此行为。该属性的可选值有 migrateSessionnewSessionnonemigrateSession是默认值。在这种状况下,每次用户登陆都会建立一个新的会话,同时把以前会话的数据复制到新会话中。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 来实现不一样的用户认证和受权机制。

相关文章
相关标签/搜索