應用情境
現有的資料庫中包含以下幾張表格用於許可權管理
要求在此基礎上整合SpringSecurity,將表格的資料作為資料來源來完成登入和許可權校正邏輯
SpringSecurity的配置可通過兩種方式呈現,基於自身的namespace配置和傳統的基於Bean的配置。通過namespace來配置Security非常簡潔,隱藏了很多繁瑣的實現細節,但也不便於初學者進行理解,而如果要想對Security進行個人化定製(替換現有功能實現),最好還是採用傳統的基於Bean的方式進行配置,雖然結構複雜,但是細節清晰明了
以下是兩種方式的配置比較:
1.基於namespace來配置
<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/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <http pattern="/js/**" security="none" /> <http use-expressions="true" access-denied-page="/error.html"> <intercept-url pattern="/peoplemanage/**" access="hasRole('admin')" /> <form-login login-page='/login.jsp'/> <logout /> </http> <authentication-manager> <authentication-provider> <user-service> <user name="zhangsan" password="zhangsan" authorities="admin,user"/> <user name="wangwu" password="wangwu" authorities="user" /> </user-service> </authentication-provider> </authentication-manager></beans:beans>
2.同樣的配置還原成Bean的方式
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:sec="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy"> <constructor-arg> <list> <sec:filter-chain pattern="/js/**" filters="none"/> <sec:filter-chain pattern="/**" filters="securityContextPersistenceFilter,authenticationFilter,exceptionTranslationFilter,filterSecurityInterceptor"/> </list> </constructor-arg> </bean> <bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="securityMetadataSource"> <sec:filter-security-metadata-source use-expressions="true"> <sec:intercept-url pattern="/peoplemanage/**" access="hasRole('admin')"/> </sec:filter-security-metadata-source> </property> </bean> <!-- exceptionTranslationFilter --> <bean id="exceptionTranslationFilter" class="org.springframework.security.web.access.ExceptionTranslationFilter"> <property name="authenticationEntryPoint" ref="authenticationEntryPoint"/> <property name="accessDeniedHandler" ref="accessDeniedHandler"/> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="loginFormUrl" value="/login.jsp"/> </bean> <bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> <property name="errorPage" value="/error.html"/> </bean> <!-- securityContextPersistenceFilter --> <bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/> <!-- authenticationFilter --> <bean id="authenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager"/> <property name="filterProcessesUrl" value="/j_spring_security_check"/> </bean> <!-- Core Service --> <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider"/> </list> </property> </bean> <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> </bean> <bean id="inMemoryDaoImpl" class="org.springframework.security.provisioning.InMemoryUserDetailsManager"> <constructor-arg name="users"> <props> <prop key="zhangsan">zhangsan,enabled</prop> <prop key="wangwu">wangwu,enabled</prop> </props> </constructor-arg> </bean> <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> <property name="decisionVoters"> <list> <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"></bean> </list> </property> </bean></beans>
還原成Bean的配置方式之後,在實現個人化的定製就變得清晰明了了。
一、首先需要修改userDetailsService的實現
在上述Demo配置中使用的是Spring內建的InMemoryUserDetailsManager,該類的主要作用是從設定檔載入zhangsan、wangwu等資訊來構建使用者資料來源,而我們的使用者資料是儲存在資料庫裡的,因此需要修改實現,實現方式如下:
1.自訂一個Service,實現org.springframework.security.core.userdetails.UserDetailsService介面
public class MyUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { /** * TODO 從資料庫中載入使用者資訊,並封裝成UserDetails對象 */ }}
2.替換Demo中的對應的配置
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="myUserDetailsService"/></bean><bean id="myUserDetailsService" class="com.youcompany.MyUserDetailsService"/>
二、修改filterSecurityInterceptor中securityMetadataSource屬性的注入方式
在Demo配置中securityMetadataSource屬性的配置是靜態,將每一個資源和資源對應的角色封裝到<sec:intercept-url>標籤裡
而我們的需求情境是資源資訊儲存在資料庫裡,因此不能通過這種靜態方式去描述,修改方式如下:
1.聲明一個Service實現org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource介面
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap; public MyFilterInvocationSecurityMetadataSource(){ requestMap=new HashMap<RequestMatcher, Collection<ConfigAttribute>>(); loadMetadataInfo();//將資料庫中的資源和角色實體封裝到requestMap裡 } private void loadMetadataInfo() { List<Resource> resources=...//TODO 擷取資料庫中所有的資源實體 for(Resource res:resources){ Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); List<Role> roles=...//TODO 擷取該資源對應的訪問角色 for(Role role:roles){ allAttributes.add(new SecurityConfig(role.getRoleName())); } RequestMatcher key=new AntPathRequestMatcher(res.getUrl()+"/**"); requestMap.put(key, allAttributes); } } public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); List<Role> roles...//TODO 擷取庫中所有的角色實體 for(Role role:roles){ allAttributes.add(new SecurityConfig(role.getRoleName())); } return allAttributes; } public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { HttpServletRequest request = ((FilterInvocation) object).getRequest(); for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().matches(request)) { return entry.getValue(); } } return null; } public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); }}
2.修改Demo中相應的配置
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="securityMetadataSource" ref="myFilterInvocationSecurityMetadataSource"/></bean><bean id="myFilterInvocationSecurityMetadataSource" class="com.youcompany.MyFilterInvocationSecurityMetadataSource"/>
三、修改accessDecisionManager中decisionVoters的實現邏輯
SpringSecurity預設使用AffirmativeBased來進行存取權限控制,該類封裝了很多AccessDecisionVoter對象,基於投票的機制來決定訪問是否通過
AccessDecisionVoter之間是OR的邏輯(只要有一個AccessDecisionVoter判斷許可權通過,使用者便可訪問介面)。
在Demo配置裡,使用的是WebExpressionVoter基於運算式的許可權認證邏輯(hasRole('admin')),而我們的需求是將使用者的角色和訪問資源需要的角色進行對比,來判斷該使用者是否具有訪問介面的許可權,因此需要進行以下修改:
1.聲明一個Service實現org.springframework.security.access.AccessDecisionVoter介面
public class MyAccessDecisionVoter implements AccessDecisionVoter<Object> { public boolean supports(ConfigAttribute attribute) { return true; } public boolean supports(Class<?> clazz) { return true; } public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { int result = ACCESS_DENIED; for (ConfigAttribute attribute : attributes) {//可訪問該頁面的角色 for (GrantedAuthority authority : authentication.getAuthorities()) {//登入使用者具備的角色 if (attribute.getAttribute().equals(authority.getAuthority())) {//判斷使用者是否具有相應角色 return ACCESS_GRANTED; } } } return result; }}
2.修改Demo中對應的配置
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> <property name="decisionVoters"> <list> <bean class="com.youcompany.MyAccessDecisionVoter"></bean> </list> </property></bean>
至此,SpringSecurity個人化定製修改完成。有點長,部分代碼加了TODO,有不理解的可與我聯絡,需要源碼的網友可留郵箱