前言:
Spring security是一個能夠為基於Spring的企業應用系統提供描述性安全訪問控制解決方案的安全框架。(Spring Security provides a comprehensive security solution for J2EE-based enterprise software applications)。本文目標為通過一個簡單的例子介紹Spring security3的基本使用。
Spring security官網 :http://www.springsource.org/spring-security/。
Spring security百度百科:http://baike.baidu.com/view/2677773.htm (百科頁首的圖片很形象的描述了Spring security的過濾器鏈)
本文涉及Spring security3版本:3.0.5
友情提示:對於對此技術有興趣的童鞋,本文中代碼和配置文件的注釋部分請細看,如有疏漏,請多提寶貴意見。
1,Spring security3簡述
Spring security相對於之前學習的框架而言,比較的有難度,原因是學習它的時候,你需要明白的概念要多一些。Spring security從前身Spring ACEGI逐步演化,配置也逐漸的簡化,某些目錄結構和類都發生了變化,對於將Spring security2轉換為Spring security3的時候還需要多多少的思考。下面,以官網官方下載包中的英文文檔為參考,說明Spring security3的最為核心的兩個概念。
As you probably know two major areas of application security are “authentication” and “authorization” (or “access-control”).即認證和授權。
Spring security對用戶請求的處理過程,即是這兩個過程的體現。
關於用戶,角色,權限,資源的概念。用戶:應用系統的使用者;角色:區分用戶職能;權限:描述訪問資源級別;用戶和角色是多對多的關系,角色和權限也是多對多的關系。對於某個資源,可能會提供多個訪問權限級別,那么只要是持有該權限的角色都是可訪問的,最終一個用戶是否能訪問該資源的問題就演變為一個用戶對應的角色所擁有的權限是否包含該資源提供的各種訪問級別的權限中的一個。
首先,認證在前,授權在后。其次,認證時用戶獲得登錄的口令,如果口令錯誤,用戶將無法請求資源;口令正確,則用戶需要等待授權,同時在認證的過程中,用戶信息和用戶所擁有的角色會被包裝成為認證對象傳遞給授權過程使用。授權時會檢查請求資源的權限並與用戶所擁有角色的權限對比,如果能夠能匹配,則認證通過,反之不通過。
2,構建基於Spring security的項目
按照文檔步驟Part III. Web Application Security的步驟開始配置,首先使用Maven構建一個Web項目,從官方獲取Spring security3.0.5坐標,添加到Pom文件中,並在Web.xml當中配置Spring容器的監聽器和Spring security的過濾器鏈鈎子。(Spring's DelegatingFilterProxy provides the link between web.xml and the application context. )
web.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <!-- 配置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中的過濾器解決在請求和應答中的中文亂碼問題 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> <init-param> <!--強制轉換編碼(request和response均適用) --> <param-name>ForceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext*.xml </param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- 解決el表達式在頁面中不生效的問題,事實上這是web-app_2_5標准中的 一個小小沖突--> <jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> </jsp-property-group> </jsp-config> </web-app>
3,Spring security3 授權認證及Url訪問控制過濾器自定義
首先,參考 Part?I.?Getting Started ---->2.?Security Namespace Configuration--->2.2?Getting Started with Security Namespace Configuration,並按照2.x的目錄閱讀。下面我們關注三個方面:自定義用戶認證,自定義用戶授權,自定義過濾器。
自定義用戶認證:Using other Authentication Providers 繼承自UserDetailsService,創建CustomUserDetailsService和CustomUserDetails類,具體如下:
package org.wit.ff.authentication; import java.util.HashMap; import java.util.Map; import org.springframework.dao.DataAccessException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * <p>UserDetailService 給用戶提供自定義的授權處理 * @author fangfan * @see UserDetailService */ public class CustomUserDetailsService implements UserDetailsService { /** * 模擬臨時的用戶信息存儲,實際應用當中應該是從數據源加載 */ private Map<String,UserDetails> usersinfo = new HashMap<String,UserDetails>(); /* * <p>用戶訪問應用資源之前,將會調用此方法獲取用戶的登錄信息及對應的權限范圍(ROLE_USER,ROLE_ADMIN,ROLE_LOGIN) * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String) */ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { if(usersinfo.isEmpty()){ loadUsersinfo(); } return usersinfo.get(username); } /** * 模擬用戶信息 */ private void loadUsersinfo(){ UserDetails detailsOne = new CustomUserDetails("ff","123","ROLE_ADMIN",true); usersinfo.put("ff", detailsOne); UserDetails detailsTwo = new CustomUserDetails("zcq","123456","ROLE_USER",true); usersinfo.put("zcq", detailsTwo); } } package org.wit.ff.authentication; import java.util.Arrays; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.UserDetails; /** * <p>自定義用戶信息 * @author fangfan * @see UserDetails * */ public class CustomUserDetails implements UserDetails { /** * */ private static final long serialVersionUID = 1L; /** * 用戶名 */ private String username; /** * 密碼 */ private String password; /** * 角色 */ private String role; private boolean enabled; private static final String ROLE_LOGIN="ROLE_LOGIN"; public CustomUserDetails(){ } public CustomUserDetails(String username, String password, String role, boolean enabled) { super(); this.username = username; this.password = password; this.role = role; this.enabled = enabled; } /** * 獲取當前用戶的權限 * 其實用戶應該擁有多個角色,這里簡單起見只用了一個String類型來表示 * 其實用戶 角色權限 資源三者可以各自創建對象並關聯能實現一個非常復雜的權限控制 */ public Collection<GrantedAuthority> getAuthorities() { GrantedAuthority[] gas = new GrantedAuthority[2]; gas[0] = new GrantedAuthorityImpl(role); //分配登錄權限給所有通過登錄驗證的角色 gas[1] = new GrantedAuthorityImpl(ROLE_LOGIN); return Arrays.asList(gas); } public String getPassword() { return password; } public String getUsername() { return username; } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return enabled; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setEnabled(boolean enabled) { this.enabled = enabled; } }
自定義用戶授權:參考2.5?The Default AccessDecisionManager 自定義實現CustomAccessDecisionManager 繼承自AbstractAccessDecisionManager。
package org.wit.ff.authentication; import java.util.Collection; import java.util.Iterator; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.vote.AbstractAccessDecisionManager; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; public class CustomAccessDecisionManager extends AbstractAccessDecisionManager{ /** * 裁定當前用戶對應權限authentication是否包含所請求資源所擁有的權限 * 如果成立 則通過裁定 否則發生異常 */ public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(configAttributes == null) { return; } //所請求的資源擁有的權限(一個資源對多個權限) Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while(iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //訪問所請求資源所需要的權限 String needPermission = configAttribute.getAttribute(); System.out.println("needPermission is " + needPermission); //用戶所擁有的權限authentication for(GrantedAuthority ga : authentication.getAuthorities()) { if(needPermission.equals(ga.getAuthority())) { return; } } } //沒有權限 throw new AccessDeniedException(" No Access Dendied "); } }
自定義過濾器:自定義過濾器采用了系統自帶的過濾器類實現,這里沒有像網上的大神們一樣自定義,因為使用系統已經實現的過濾器更方便。這里重定義了資源元數據類FilterInvocationSecurityMetadataSource。
package org.wit.ff.authentication; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; /** * <p>自定義 資源與權限對應關系 * @author fangfan * */ public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { /** * 原本資源(url)與角色的對應關系需要從數據源中獲取,這里做了簡化 */ private Map<String, Collection<ConfigAttribute>> resourceMap; public CustomFilterInvocationSecurityMetadataSource() { resourceMap = loadResourceMatchAuthority(); } public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String url = ((FilterInvocation) object).getRequestUrl(); System.out.println("requestUrl is " + url); if (resourceMap == null) { loadResourceMatchAuthority(); } return resourceMap.get(url); } public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } public boolean supports(Class<?> clazz) { return true; } private Map<String, Collection<ConfigAttribute>> loadResourceMatchAuthority() { Map<String, Collection<ConfigAttribute>> map = new HashMap<String, Collection<ConfigAttribute>>(); // admin頁面 Map<String, String> configs = getResourcesConfig(); for (Entry<String, String> entry : configs.entrySet()) { Collection<ConfigAttribute> list = new ArrayList<ConfigAttribute>(); String[] vals = entry.getValue().split(","); for (String val : vals) { ConfigAttribute config = new SecurityConfig(val); list.add(config); } map.put(entry.getKey(), list); } return map; } /** * 定義簡單url 與role的對應 * @return */ private Map<String, String> getResourcesConfig() { Map<String, String> map = new HashMap<String, String>(); map.put("/pages/admin.jsp", "ROLE_ADMIN"); map.put("/**", "ROLE_USER,ROLE_ADMIN"); return map; } }
最后,附上配置文件:
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <!-- 定義上下文返回的消息的國際化。 --> <beans:bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <beans:property name="basename" value="classpath:org/springframework/security/messages_zh_CN" /> </beans:bean> <!-- Spring security provides two major areas of application security are “authentication” and “authorization” 授權和認證 --> <!-- http配置如何攔截用戶請求。將auto-config設為'true'將自動配置幾種常用的權限控制機制,包括form, anonymous, rememberMe。 --> <http auto-config="true"> <!-- 我們利用intercept-url來判斷用戶需要具有何種權限才能訪問對應的url資源, 可以在pattern中指定一個特定的url資源,也可以使用通配符指定一組類似的url資源。 例子中定義的兩個intercepter-url,第一個用來控制對/admin.jsp的訪問, 第二個使用了通配符/**,說明它將控制對系統中所有url資源的訪問。 在實際使用中,Spring Security采用的是一種就近原則, 就是說當用戶訪問的url資源滿足多個intercepter-url時, 系統將使用第一個符合條件的intercept-url進行權限控制。 在我們這個例子中就是,當用戶訪問/admin.jsp時, 雖然兩個intercept-url都滿足要求, 但因為第一個intercept-url排在上面, 所以Spring Security會使用第一個intercept-url中的配置處理對/admin.jsp的請求, 也就是說,只有那些擁有了ROLE_ADMIN權限的用戶才能訪問/admin.jsp。 --> <!-- <intercept-url pattern="/pages/admin.jsp" access="ROLE_ADMIN" /> --> <intercept-url pattern="/pages/login.jsp" filters="none" /> <intercept-url pattern="/pages/css/**" filters="none" /> <intercept-url pattern="/pages/js/**" filters="none" /> <intercept-url pattern="/pages/images/**" filters="none" /> <!-- <intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" /> --> <form-login login-page="/pages/login.jsp" default-target-url="/pages/index.jsp" always-use-default-target="true" /> <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" /> </http> <!-- 認證管理:提供用戶權限對應關系 --> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="customUserDatailsService"> <!-- user-service中定義了兩個用戶,admin和user, 重要的部分是authorities,這里定義了這個用戶登陸之后將會擁有的權限, 它與上面intercept-url中定義的權限內容一一對應。每個用戶可以同時擁有多個權限, 例子中的admin用戶就擁有ROLE_ADMIN和ROLE_USER兩種權限, 這使得admin用戶在登陸之后可以訪問ROLE_ADMIN和ROLE_USER允許訪問的所有資源。 與之對應的是,user用戶就只擁有ROLE_USER權限,所以他只能訪問ROLE_USER允許訪問的資源, 而不能訪問ROLE_ADMIN允許訪問的資源。 --> <!-- <user-service> <user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN" /> <user name="user" password="user" authorities="ROLE_USER" /> </user-service> --> </authentication-provider> </authentication-manager> <!-- 訪問決策管理:通過資源所擁有的權限,對用戶所具備的角色對應的權限進行裁定,決策是否用戶具備訪問資源的權限--> <beans:bean id="accessDecisionManager" class="org.wit.ff.authentication.CustomAccessDecisionManager"> <beans:property name="allowIfAllAbstainDecisions" value="false" /> <beans:property name="decisionVoters"> <beans:list> <beans:bean class="org.springframework.security.access.vote.RoleVoter" /> <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" /> </beans:list> </beans:property> </beans:bean> <!--使用Spring security3提供的過濾器,並將過濾器默認的認證和授權組件替換為我們已經定義的認證和授權組件 --> <beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="accessDecisionManager" ref="accessDecisionManager" /> <beans:property name="securityMetadataSource" ref="customSecurityMetadataSource" /> </beans:bean> <beans:bean id="customSecurityMetadataSource" class="org.wit.ff.authentication.CustomFilterInvocationSecurityMetadataSource" /> <!-- 自定義授權用戶信息處理 --> <beans:bean id="customUserDatailsService" class="org.wit.ff.authentication.CustomUserDetailsService" /> </beans:beans>