最近在做項目遇到了權限管理,用戶要求可以自己建立不同的角色對系統的資源進行控制, 不同的用戶有不同的角色,又恰恰框架中用到了struts+spring+hibernate,要求在web層調用 業務邏輯層 時不考慮權限,web層可以控制用戶的顯示界面,邏輯層處理用戶權限問題。 想來想去好像只有spring 的aop 可以做到,在調用到 接口 中的方法時,首先檢查用戶的權限,如果檢查通過則繼續執行,否則拋出異常。但是新的問題又出現了,如何在邏輯層上來得到當前用戶的id,以致用戶的 角色,總不能每次都要從web中傳來一個 httprequest,或者 session 這類的吧。在網上看了很多資料,發現了acegi,恰好解決了以上的難題,具體的實現原理這里就不多說了,網上有很多相關資料。 說正題,首先來看看acegi 的官方 example ,我下載的是acegi-security-1.0.0-RC1,解壓縮后可以看到acegi-security-sample-contacts-filter.war,打開配置文件有這樣幾句
java代碼: |
<bean id="contactManagerSecurity" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> <property name="accessDecisionManager"><ref local="businessAccessDecisionManager"/></property> <property name="afterInvocationManager"><ref local="afterInvocationManager"/></property> <property name="objectDefinitionSource"> <value> sample.contact.ContactManager.create=ROLE_USER sample.contact.ContactManager.getAllRecipients=ROLE_USER sample.contact.ContactManager.getAll=ROLE_USER,AFTER_ACL_COLLECTION_READ sample.contact.ContactManager.getById=ROLE_USER,AFTER_ACL_READ sample.contact.ContactManager.delete=ACL_CONTACT_DELETE sample.contact.ContactManager.deletePermission=ACL_CONTACT_ADMIN sample.contact.ContactManager.addPermission=ACL_CONTACT_ADMIN </value> </property> </bean>
|
可以看到它是通過讀配置文件來判斷執行某個方法所需要的角色的,再看這幾句
java代碼: |
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> <property name="accessDecisionManager"><ref local="httpRequestAccessDecisionManager"/></property> <property name="objectDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /index.jsp=ROLE_ANONYMOUS,ROLE_USER /hello.htm=ROLE_ANONYMOUS,ROLE_USER /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER /switchuser.jsp=ROLE_SUPERVISOR /j_acegi_switch_user=ROLE_SUPERVISOR /acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER /**=ROLE_USER </value> </property> </bean>
|
同樣是將頁面的訪問權限寫死在配置文件中,再來看看它的tag是如何處理的
java代碼: |
<auth:authorize ifAnyGranted="ROLE_DELETE"> <a href="">刪除</a> </auth:authorize>
|
可見它是要求我們對鏈接或者其他資源的保護時提供 用戶角色,可是既然角色是用戶自己添加的我們又如何來寫死在這里呢? 還有就是它對用戶驗證默認使用的是jdbc,即 JdbcDaoImpl
java代碼: |
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource"><ref local="dataSource"/></property> </bean>
|
而我們希望基于Hibernate的Dao來實現。 可見僅僅使用現有的acegi 是 無法滿足我們項目開發的需求的。 解決方法:
1: 開發基于數據庫的保護資源。
看過acegi的源代碼就會知道,對保護資源的定義是通過實現ObjectDefinitionSource這個接口來實現的,而且acegi為我們提供了默認實現的抽象類
java代碼: |
public abstract class AbstractMethodDefinitionSource implements MethodDefinitionSource { //~ Static fields/initializers =============================================
private static final Log logger = LogFactory.getLog(AbstractMethodDefinitionSource.class);
//~ Methods ================================================================
public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException { Assert.notNull(object, "Object cannot be null");
if (object instanceof MethodInvocation) { return this.lookupAttributes(((MethodInvocation) object).getMethod()); }
if (object instanceof JoinPoint) { JoinPoint jp = (JoinPoint) object; Class targetClazz = jp.getTarget().getClass(); String targetMethodName = jp.getStaticPart().getSignature().getName(); Class[] types = ((CodeSignature) jp.getStaticPart().getSignature()) .getParameterTypes();
if (logger.isDebugEnabled()) { logger.debug("Target Class: " + targetClazz); logger.debug("Target Method Name: " + targetMethodName);
for (int i = 0; i < types.length; i++) { if (logger.isDebugEnabled()) { logger.debug("Target Method Arg #" + i + ": " + types[i]); } } }
try { return this.lookupAttributes(targetClazz.getMethod(targetMethodName, types)); } catch (NoSuchMethodException nsme) { throw new IllegalArgumentException("Could not obtain target method from JoinPoint: " + jp); } }
throw new IllegalArgumentException("Object must be a MethodInvocation or JoinPoint"); }
public boolean supports(Class clazz) { return (MethodInvocation.class.isAssignableFrom(clazz) || JoinPoint.class.isAssignableFrom(clazz)); }
protected abstract ConfigAttributeDefinition lookupAttributes(Method method); }
|
我們要做的就是實現它的 protected abstract ConfigAttributeDefinition lookupAttributes(Method method);方法, 以下是我的實現方法,大致思路是這樣,通過由抽象類傳來的Method 對象得到 調用這個方法的 包名,類名,方法名 也就是secureObjectName, 查詢數據庫并將結果映射為Function 也就是secureObject ,由于Function 與 Role 的多對多關系 可以得到 Function所對應的 Roles ,在將role 包裝成GrantedAuthority (也就是acegi中的角色)。其中由于頻繁的對數據庫的查詢 所以使用Ehcache 來作為緩存。
java代碼: |
package sample.auth;
import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set;
import org.acegisecurity.ConfigAttributeDefinition; import org.acegisecurity.ConfigAttributeEditor; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.intercept.method.AbstractMethodDefinitionSource; import org.springframework.util.Assert;
import sample.auth.cache.AuthorityBasedFunctionCache; import sample.auth.cache.info.FunctionByNameCache; import sample.dao.IBaseDao; import sample.mappings.function.Function; import sample.mappings.role.Role;
public class DatabaseDrivenMethodDefinitionSourcew extends AbstractMethodDefinitionSource { // baseDao 提供通過HIbenate對數據庫操作的實現 private IBaseDao baseDao; // AuthorityBasedFunctionCache 通過Function 查 Role 時緩存 private AuthorityBasedFunctionCache cache; // FunctionByNameCache 由反射到的方法名查找 數據庫對應的Function 時的緩存 private FunctionByNameCache functionCache;
public FunctionByNameCache getFunctionCache() { return functionCache; }
public void setFunctionCache(FunctionByNameCache functionCache) { this.functionCache = functionCache; }
protected ConfigAttributeDefinition lookupAttributes(Method mi) { Assert.notNull(mi,"lookupAttrubutes in the DatabaseDrivenMethodDefinitionSourcew is null"); String secureObjectName=mi.getDeclaringClass().getName() +"."+ mi.getName(); //Function 為數據庫中保護資源的映射 Function secureObject=functionCache.getFunctionByCache(secureObjectName);
if(secureObject==null)//if secure object not exist in database { secureObject=(Function)baseDao.loadByKey(Function.class, "protectfunction", secureObjectName); functionCache.putFunctionInCache(secureObject); } if(secureObject==null) Assert.notNull(secureObject,"secureObject(Function) not found in db"); //retrieving roles associated with this secure object Collection roles = null; GrantedAuthority[] grantedAuthoritys = cache.getAuthorityFromCache(secureObject.getName()); // 如果是第一次 cache 為空 if(grantedAuthoritys == null){ Set rolesSet = secureObject.getRoles(); Iterator it = rolesSet.iterator(); List list = new ArrayList(); while(it.hasNext()){ Role role = (Role)it.next(); GrantedAuthority g = new GrantedAuthorityImpl(role.getName()); list.add(g); } grantedAuthoritys = (GrantedAuthority[])list.toArray(new GrantedAuthority[0]); cache.putAuthorityInCache(secureObject.getName(),grantedAuthoritys); roles = Arrays.asList(grantedAuthoritys); }else{ roles = Arrays.asList(grantedAuthoritys); } if(!roles.isEmpty()){ ConfigAttributeEditor configAttrEditor=new ConfigAttributeEditor(); StringBuffer rolesStr=new StringBuffer(); for(Iterator it = roles.iterator();it.hasNext();){ GrantedAuthority role=(GrantedAuthority)it.next(); rolesStr.append(role.getAuthority()).append(","); }
configAttrEditor.setAsText( rolesStr.toString().substring(0,rolesStr.length()-1) ); ConfigAttributeDefinition configAttrDef=(ConfigAttributeDefinition)configAttrEditor.getValue(); return configAttrDef; }
Assert.notEmpty(roles,"collection of roles is null or empty"); return null;
}
public Iterator getConfigAttributeDefinitions() { return null; }
public IBaseDao getBaseDao() { return baseDao; }
public void setBaseDao(IBaseDao baseDao) { this.baseDao = baseDao; }
public AuthorityBasedFunctionCache getCache() { return cache; }
public void setCache(AuthorityBasedFunctionCache cache) { this.cache = cache; }
}
|
2:定義 基于方法的 自定義標志
通過以上的分析 , 要想使用acegi 做頁面的顯示控制僅僅靠角色(Role)是不行的,因為用戶可能隨時定義出新的角色,所以只能 基于方法(Function)的控制??墒莂cegi 只是提供了基于 角色的 接口GrantedAuthority ,怎么辦? ,如法炮制。 首先定義出我們自己的GrantedFunction,實現也雷同 GrantedAuthorityImpl
java代碼: |
package sample.auth;
import java.io.Serializable; public class GrantedFunctionImpl implements GrantedFunction , Serializable{
private String function;
//~ Constructors ===========================================================
public GrantedFunctionImpl(String function) { super(); this.function = function; }
protected GrantedFunctionImpl() { throw new IllegalArgumentException("Cannot use default constructor"); }
//~ Methods ================================================================
public String getFunction() { return this.function; }
public boolean equals(Object obj) { if (obj instanceof String) { return obj.equals(this.function); }
if (obj instanceof GrantedFunction) { GrantedFunction attr = (GrantedFunction) obj;
return this.function.equals(attr.getFunction()); }
return false; }
public int hashCode() { return this.function.hashCode(); }
public String toString() { return this.function; }
}
|
以下是我的標志實現,大致思路是 根據 頁面 的傳來的 方法名(即 FunctionName)查詢出對應的Functions,并且包裝成grantedFunctions ,然后根據用戶的角色查詢出用戶對應的Functions ,再取這兩個集合的交集,最后再根據這個集合是否為空判斷是否顯示標志體的內容。
java代碼: |
package sample.auth; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set;
import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.Tag; import javax.servlet.jsp.tagext.TagSupport;
import org.acegisecurity.Authentication; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.context.SecurityContextHolder; import org.springframework.util.StringUtils; import org.springframework.web.util.ExpressionEvaluationUtils;
import sample.web.action.AppContext; /** * * @author limq * */ public class AuthorizeActionTag extends TagSupport{
private String ifAllGranted = ""; private String ifAnyGranted = ""; private String ifNotGranted = ""; public void setIfAllGranted(String ifAllGranted) throws JspException { this.ifAllGranted = ifAllGranted; }
public String getIfAllGranted() { return ifAllGranted; }
public void setIfAnyGranted(String ifAnyGranted) throws JspException { this.ifAnyGranted = ifAnyGranted; }
public String getIfAnyGranted() { return ifAnyGranted; }
public void setIfNotGranted(String ifNotGranted) throws JspException { this.ifNotGranted = ifNotGranted; }
public String getIfNotGranted() { return ifNotGranted; } public int doStartTag() throws JspException { if (((null == ifAllGranted) || "".equals(ifAllGranted)) && ((null == ifAnyGranted) || "".equals(ifAnyGranted)) && ((null == ifNotGranted) || "".equals(ifNotGranted))) { return Tag.SKIP_BODY; }
final Collection granted = getPrincipalFunctionByAuthorities();
final String evaledIfNotGranted = ExpressionEvaluationUtils .evaluateString("ifNotGranted", ifNotGranted, pageContext);
if ((null != evaledIfNotGranted) && !"".equals(evaledIfNotGranted)) { Set grantedCopy = retainAll(granted, parseSecurityString(evaledIfNotGranted));
if (!grantedCopy.isEmpty()) { return Tag.SKIP_BODY; } }
final String evaledIfAllGranted = ExpressionEvaluationUtils .evaluateString("ifAllGranted", ifAllGranted, pageContext);
if ((null != evaledIfAllGranted) && !"".equals(evaledIfAllGranted)) { if (!granted.containsAll(parseSecurityString(evaledIfAllGranted))) { return Tag.SKIP_BODY; } }
final String evaledIfAnyGranted = ExpressionEvaluationUtils .evaluateString("ifAnyGranted", ifAnyGranted, pageContext);
if ((null != evaledIfAnyGranted) && !"".equals(evaledIfAnyGranted)) { Set grantedCopy = retainAll(granted, parseSecurityString(evaledIfAnyGranted));
if (grantedCopy.isEmpty()) { return Tag.SKIP_BODY; } }
return Tag.EVAL_BODY_INCLUDE; } /** * 得到用戶的Authentication,并且從Authentication中獲得 Authorities,進而得到 授予用戶的 Function * @return */ private Collection getPrincipalFunctionByAuthorities() { Authentication currentUser = SecurityContextHolder.getContext() .getAuthentication(); if (null == currentUser) { return Collections.EMPTY_LIST; }
if ((null == currentUser.getAuthorities()) || (currentUser.getAuthorities().length < 1)) { return Collections.EMPTY_LIST; } // currentUser.getAuthorities() 返回的是 GrantedAuthority[] List granted = Arrays.asList(currentUser.getAuthorities()); AuthDao authDao =(AuthDao) AppContext.getInstance().getAppContext().getBean("authDao"); Collection grantedFunctions = authDao.getFunctionsByRoles(granted); return grantedFunctions; }
/** * 得到用戶功能(Function)的集合,并且驗證是否合法 * @param c Collection 類型 * @return Set類型 */ private Set SecurityObjectToFunctions(Collection c) { Set target = new HashSet();
for (Iterator iterator = c.iterator(); iterator.hasNext();) { GrantedFunction function = (GrantedFunction) iterator.next();
if (null == function.getFunction()) { throw new IllegalArgumentException( "Cannot process GrantedFunction objects which return null from getFunction() - attempting to process " + function.toString()); }
target.add(function.getFunction()); }
return target; }
/** * 處理頁面標志屬性 ,用‘ ,‘區分 */ private Set parseSecurityString(String functionsString) { final Set requiredFunctions = new HashSet(); final String[] functions = StringUtils .commaDelimitedListToStringArray(functionsString);
for (int i = 0; i < functions.length; i++) { String authority = functions[i];
// Remove the role‘s whitespace characters without depending on JDK 1.4+ // Includes space, tab, new line, carriage return and form feed. String function = StringUtils.replace(authority, " ", ""); function = StringUtils.replace(function, "\t", ""); function = StringUtils.replace(function, "\r", ""); function = StringUtils.replace(function, "\n", ""); function = StringUtils.replace(function, "\f", "");
requiredFunctions.add(new GrantedFunctionImpl(function)); }
return requiredFunctions; } /** * 獲得用戶所擁有的Function 和 要求的 Function 的交集 * @param granted 用戶已經獲得的Function * @param required 所需要的Function * @return */ private Set retainAll(final Collection granted, final Set required) { Set grantedFunction = SecurityObjectToFunctions(granted); Set requiredFunction = SecurityObjectToFunctions(required); // retailAll() 獲得 grantedFunction 和 requiredFunction 的交集 // 即刪除 grantedFunction 中 除了 requiredFunction 的項 grantedFunction.retainAll(requiredFunction);
return rolesToAuthorities(grantedFunction, granted); }
/** * * @param grantedFunctions 已經被過濾過的Function * @param granted 未被過濾過的,即用戶所擁有的Function * @return */ private Set rolesToAuthorities(Set grantedFunctions, Collection granted) { Set target = new HashSet();
for (Iterator iterator = grantedFunctions.iterator(); iterator.hasNext();) { String function = (String) iterator.next();
for (Iterator grantedIterator = granted.iterator(); grantedIterator.hasNext();) { GrantedFunction grantedFunction = (GrantedFunction) grantedIterator .next();
if (grantedFunction.getFunction().equals(function)) { target.add(grantedFunction);
break; } } }
return target; } }
|
再說明一下吧,通過 AppContext 獲得了Spring的上下文,以及AuthDao(實際意義上講以不再是單純的Dao,應該是Service)
java代碼: |
package sample.auth;
import java.util.Collection; public interface AuthDao {
/** * 根據用戶的角色集合 得到 用戶的 操作權限 * @param granted 已授予用戶的角色集合 * @return 操作權限的集合 */ public Collection getFunctionsByRoles(Collection granted); }
|
以下是AuthDao 的實現
java代碼: |
package sample.auth;
import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set;
import org.acegisecurity.GrantedAuthority;
import sample.auth.cache.FunctionCache; import sample.auth.cache.info.RoleByNameCache; import sample.dao.IBaseDao; import sample.mappings.function.Function; import sample.mappings.role.Role;
public class AuthDaoImpl implements AuthDao {
private IBaseDao baseDao; private FunctionCache cache; private RoleByNameCache roleCache; public RoleByNameCache getRoleCache() { return roleCache; }
public void setRoleCache(RoleByNameCache roleCache) { this.roleCache = roleCache; }
public FunctionCache getCache() { return cache; }
public void setCache(FunctionCache cache) { this.cache = cache; }
public IBaseDao getBaseDao() { return baseDao; }
public void setBaseDao(IBaseDao baseDao) { this.baseDao = baseDao; }
public Collection getFunctionsByRoles(Collection granted) { Set set = new HashSet(); if(null == granted) throw new IllegalArgumentException("Granted Roles cannot be null"); for(Iterator it = granted.iterator();it.hasNext();){ GrantedAuthority grantedAuthority = (GrantedAuthority)it.next(); Role role = roleCache.getRoleByRoleNameCache(grantedAuthority.getAuthority()); // if(role == null){ role = (Role)baseDao.loadByKey(Role.class, "name", grantedAuthority.getAuthority()); roleCache.putRoleInCache(role); } GrantedFunction[] grantedFunctions = cache.getFunctionFromCache(role.getName()); if(grantedFunctions == null){ Set functions = role.getFunctions(); for(Iterator it2 = functions.iterator();it2.hasNext();){ Function function = (Function)it2.next(); GrantedFunction grantedFunction = new GrantedFunctionImpl(function.getName()); set.add( grantedFunction ); } grantedFunctions = (GrantedFunction[]) set.toArray(new GrantedFunction[0]); cache.putFuncitonInCache(role.getName(),grantedFunctions); } for(int i = 0 ; i < grantedFunctions.length; i++){ GrantedFunction grantedFunction = grantedFunctions[i]; set.add(grantedFunction); } } return set; }
}
|
3 基于hibernate的用戶驗證
acegi 默認的 的 用戶驗證是 通過UserDetailsService 接口 實現的 也就是說我們只要實現了 它的loadUserByUsername 方法。
java代碼: |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException;
|
以下是我的實現
java代碼: |
package sample.auth;
import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set;
import org.acegisecurity.GrantedAuthority; import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.userdetails.User; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UserDetailsService; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.springframework.dao.DataAccessException;
import sample.auth.cache.AuthorityBasedUserCache; import sample.dao.IBaseDao; import sample.mappings.role.Role; import sample.utils.MisUtils;
public class HibernateDaoImpl implements UserDetailsService{
private String rolePrefix = ""; private boolean usernameBasedPrimaryKey = false; private AuthorityBasedUserCache cache;
private IBaseDao baseDao;
public String getRolePrefix() { return rolePrefix; } public void setRolePrefix(String rolePrefix) { this.rolePrefix = rolePrefix; } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { UserDetails user = getUsersByUsernameQuery(username); if(user == null) return null; GrantedAuthority[] arrayAuths =getAuthoritiesByUsernameQuery(username); if (arrayAuths.length == 0) { throw new UsernameNotFoundException("User has no GrantedAuthority"); } return new User(username, user.getPassword(), user.isEnabled(), true, true, true, arrayAuths); }
/** * 根據用戶名查找用戶 * @param username * @return * @throws DataAccessException */ public UserDetails getUsersByUsernameQuery(String username)throws DataAccessException { sample.mappings.user.User misUser = (sample.mappings.user.User)baseDao.loadByKey(sample.mappings.user.User.class,"name",username); if(misUser != null) { org.acegisecurity.userdetails.UserDetails user = new User(misUser.getName(),misUser.getPassword(),MisUtils.parseBoolean(misUser.getEnable()),true,true,true,getAuthoritiesByUsernameQuery(username)); return user; }else return null; } /** * 根據用戶名查找角色 * @param username * @return GrantedAuthority[] 用戶角色 * @throws DataAccessException */ public GrantedAuthority[] getAuthoritiesByUsernameQuery(String username) throws DataAccessException { sample.mappings.user.User misUser = (sample.mappings.user.User)baseDao.loadByKey(sample.mappings.user.User.class,"name",username);
if(misUser != null){ GrantedAuthority[] grantedAuthoritys = cache.getAuthorityFromCache(misUser.getName()); if(grantedAuthoritys == null){ Set roles = misUser.getRoles(); Iterator it = roles.iterator(); List list = new ArrayList(); while(it.hasNext() ){ GrantedAuthorityImpl gai = new GrantedAuthorityImpl( ((Role)it.next()).getName() ); list.add(gai); } grantedAuthoritys =(GrantedAuthority[]) list.toArray(new GrantedAuthority[0]); cache.putAuthorityInCache(misUser.getName(),grantedAuthoritys); return grantedAuthoritys; } return grantedAuthoritys; }
return null; }
public IBaseDao getBaseDao() { return baseDao; }
public void setBaseDao(IBaseDao baseDao) { this.baseDao = baseDao; }
public AuthorityBasedUserCache getCache() { return cache; }
public void setCache(AuthorityBasedUserCache cache) { this.cache = cache; }
} 通過以上對acegi 的 處理,足以滿足我們目前在spring下基于RBAC的動態權限管理。同時在對頻繁的數據庫查詢上使用了Ehcache作為緩存,在性能上有了很大的改善。
|
|