使用 Spring Security 保護 Web 應用的安全

安全一直是 Web 應用開發中很是重要的一個方面。從安全的角度來講,須要考慮用戶認證和受權兩個方面。爲 Web 應用增長安全方面的能力並不是一件簡單的事情,須要考慮不一樣的認證和受權機制。Spring Security 爲使用 Spring 框架的 Web 應用提供了良好的支持。本文將詳細介紹如何使用 Spring Security 框架爲 Web 應用提供安全支持。html

成 富, 軟件工程師, IBM 中國軟件開發中心java

2010 年 12 月 02 日web

  • +內容

在 Web 應用開發中,安全一直是很是重要的一個方面。安全雖然屬於應用的非功能性需求,可是應該在應用開發的初期就考慮進來。若是在應用開發的後期才考慮安全的問題,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,沒法知足用戶的要求,並可能形成用戶的隱私數據被攻擊者竊取;另外一方面,應用的基本架構已經肯定,要修復安全漏洞,可能須要對系統的架構作出比較重大的調整,於是須要更多的開發時間,影響應用的發佈進程。所以,從應用開發的第一天就應該把安全相關的因素考慮進來,並在整個應用的開發過程當中。spring

本文詳細介紹瞭如何使用 Spring Security 來保護 Web 應用的安全。Spring Security 自己以及 Spring 框架帶來的靈活性,可以知足通常 Web 應用開發的典型需求,並容許開發人員進行定製。下面首先簡單介紹 Spring Security。sql

Spring Security 簡介

Spring 是一個很是流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。通常來講,Web 應用的安全性包括用戶認證(Authentication)和用戶受權(Authorization)兩個部分。用戶認證指的是驗證某個用戶是否爲系統中的合法主體,也就是說用戶可否訪問該系統。用戶認證通常要求用戶提供用戶名和密碼。系統經過校驗用戶名和密碼來完成認證過程。用戶受權指的是驗證某個用戶是否有權限執行某個操做。在一個系統中,不一樣用戶所具備的權限是不一樣的。好比對一個文件來講,有的用戶只能進行讀取,而有的用戶能夠進行修改。通常來講,系統會爲不一樣的用戶分配不一樣的角色,而每一個角色則對應一系列的權限。數據庫

對於上面提到的兩種應用情景,Spring Security 框架都有很好的支持。在用戶認證方面,Spring Security 框架支持主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。在用戶受權方面,Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List,ACL),能夠對應用中的領域對象進行細粒度的控制。express

本文將經過三個具體的示例來介紹 Spring Security 的使用。第一個示例是一個簡單的企業員工管理系統。該系統中存在三類用戶,分別是普通員工、經理和總裁。不一樣類別的用戶所能訪問的資源不一樣。對這些資源所能執行的操做也不相同。Spring Security 能幫助開發人員以簡單的方式知足這些安全性相關的需求。第二個示例展現瞭如何與 LDAP 服務器進行集成。第三個示例展現瞭如何與 OAuth 進行集成。完整的示例代碼見 參考資料。下面首先介紹基本的用戶認證和受權的實現。apache

回頁首編程

基本用戶認證和受權

本節從最基本的用戶認證和受權開始對 Spring Security 進行介紹。通常來講,Web 應用都須要保存本身系統中的用戶信息。這些信息通常保存在數據庫中。用戶能夠註冊本身的帳號,或是由系統管理員統一進行分配。這些用戶通常都有本身的角色,如普通用戶和管理員之類的。某些頁面只有特定角色的用戶能夠訪問,好比只有管理員才能夠訪問 /admin 這樣的網址。下面介紹如何使用 Spring Security 來知足這樣基本的認證和受權的需求。api

首先須要把 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 其實是使用多個過濾器造成的鏈條來工做的。

下一步是配置 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 來實現不一樣的用戶認證和受權機制。

回頁首

下載

描述
名字
大小

簡單的企業員工管理系統1
Company.zip
48 KB

與 LDAP 的集成2
LDAPSample.zip
9 KB

與 OAuth 的集成,使用 LinkedIn 的服務3
LinkedInSample.zip
17 KB

注意:

  1. 不包含依賴的 jar 包。使用 createDB.sql 來建立數據庫。
  2. 不包含依賴的 jar 包。注意根據 LDAP 服務器的地址和結構修改配置文件。
  3. 不包含依賴的 jar 包。使用本身申請的 LinkedIn 開發者帳號來修改配置文件。

參考資料

學習
得到產品和技術
討論
相關文章
相關標籤/搜索