姚博文 集成ACEGI 进行权限控制

本文介绍如何集成ACEGI安全框架到项目中,并实现基于数据库的用户认证和权限控制。涵盖自定义用户信息类、扩展JdbcDaoImpl获取更多用户属性、实现权限动态更新等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集成ACEGI 进行权限控制

一. 简单介绍

1.1 本文目的

集成Acegi到自己的项目中, 并且将用户信息和权限放到数据库, 提供方法允许权限动态变化,变化后自动加载最新的权限

本文介绍Acegi例子的时候采用的是acegi-security-samples-tutorial-1.0.6.war

阅读本文需要对Spring有一定的了解, 如果你还没有接触过, 有些地方可能不容易理解, 这时候可能需要参考本文后附的Spring地址, 先了解一下Spring的基本知识.

本文使用的是Mysql数据库, 如果你使用其他的数据库, 可能需要修改相应的SQL.

本文及所附的全部代码放在http://acegi-test.sourceforge.net/

1.2 安装与配置

项目主页: http://www.acegisecurity.org/

下载地址: http://sourceforge.net/project/showfiles.php?group_id=104215

解压文件后, 将acegi-security-samples-tutorial-1.0.6.war复制Your_Tomcat_Path/webapps/

启动Tomcat, 访问http://localhost:8080/acegi-security-samples-tutorial-1.0.6/

点击页面上任何一个链接,都需要用户登录后访问, 可以在页面上看到可用的用户名和密码.

二. 开始集成到自己的程序中

2.1 将用户和角色放在数据库中

可能是为了演示方便, 简单的展示Acegi如何控制权限, 而不依赖于任何数据库, ACEGI给出的例子采用InMemoryDaoImpl获取用户信息, 用户和角色信息放在WEB-INF/users.properties 文件中, InMemoryDaoImpl 一次性的从该配置文件中读出用户和角色信息, 格式是: 用户名=密码, 角色名, 如第一行是:

marissa=koala,ROLE_SUPERVISOR

就是说marissa的密码是koala, 并且他的角色是ROLE_SUPERVISOR

对这个文件的解析是通过applicationContext-acegi-security.xml中如下的设置进行的:

<!-- UserDetailsService is the most commonly frequently Acegi Security interface implemented by end users -->
<bean id="userDetailsService"
class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userProperties">
<bean
class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location"
value="classpath:users.properties" />
</bean>
</property>
</bean>

除了InMemoryDaoImpl之外, ACEGI还提供了Jdbc和 ldap的支持, 由于使用数据库进行验证比较常见, 下面仅就jdbc实现做出介绍.

不管是InMemoryDaoImpl还是JdbcDaoImpl都是实现了UserDetailsService接口, 而这个接口里只定义了一个方法: UserDetails loadUserByUsername(String username) 就是根据用户名加载UserDetails对象, UserDetails也是一个接口, 定义了一个用户所需要的基本信息, 包括: username, password, authorities等信息

2.1.1 直接使用JdbcDaoImpl 访问数据库中的用户信息

如果ACEGI提供的信息满足你的需要, 也就是说你只需要用户的username, password等信息, 你可以直接使用ACEGI提供的Schema, 这样, 不需要任何变动, JdbcDaoImpl就可以使用了.

如果你的数据库已经定义好了, 或者不想使用ACEGI提供的Schema,那么你也可以自定义JdbcDaoImpl的查询语句

        <property name="usersByUsernameQuery">
<value>
SELECT email, password, enabled from user u where email = ?
</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>
SELECT u.email, r.role_name FROM user_role ur, user u, role r WHERE
ur.user_id = u.user_id and ur.role_id = r.role_id and u.email = ?
</value>
</property>
2.1.2 扩展JdbcDaoImpl获取更多用户信息

如果上面提到的定制查询SQL语句不能提供足够的灵活性, 那么你可能就需要定义一个JdbcDaoImpl的子类, 如果变动不大, 通过覆盖initMappingSqlQueries方法重新定义MappingSqlQuery的实例. 而如果你需要获取更多信息, 比如userId, companyId等, 那就需要做更多的改动, 第一种改动不大, 所以不具体介绍, 下面以第二种改动为例,介绍如何实现这种需求.

我们需要三张表User, Role, User_Role, 具体的SQL如下:

#

# Structure for the `role` table : 

#



DROP TABLE IF EXISTS `role`;



CREATE TABLE `role` (

`role_id` int(11) NOT NULL auto_increment,

`role_name` varchar(50) default NULL,

`description` varchar(20) default NULL,

`enabled` tinyint(1) NOT NULL default '1',

PRIMARY KEY  (`role_id`)

);



#

# Structure for the `user` table : 

#



DROP TABLE IF EXISTS `user`;



CREATE TABLE `user` (

`user_id` int(11) NOT NULL auto_increment,

`company_id` int(11) default NULL,

`email` varchar(200) default NULL,

`password` varchar(10) default NULL,

`enabled` tinyint(1) default NULL,

PRIMARY KEY  (`user_id`)

);



#

# Structure for the `user_role` table : 

#



DROP TABLE IF EXISTS `user_role`;



CREATE TABLE `user_role` (

`user_role_id` int(11) NOT NULL auto_increment,

`user_id` varchar(50) NOT NULL,

`role_id` int(11) NOT NULL,

PRIMARY KEY  (`user_role_id`)

);

前面讲过, UserDetailsService接口中只定义了一个方法: UserDetails loadUserByUsername(String username), UserDetails中不存在我们需要的userId 和companyId等信息, 所以我们首先需要扩展UserDetails接口, 并扩展org.acegisecurity.userdetails.User:

IUserDetails.java

package org.security;



import org.acegisecurity.GrantedAuthority;



/**
 * The class <code>IUserDetails</code> extends the org.acegisecurity.userdetails.UserDetails interface, and provides additional userId, companyId information<br><br>
 * @author wade
 * @see UserDetails
 */
public interface IUserDetails extends org.acegisecurity.userdetails.UserDetails{



public int getUserId();



public void setUserId(int user_id);



public int getCompanyId();



public void setCompanyId(int company_id);



public String getUsername();



public void setUsername(String username);



public GrantedAuthority[] getAuthorities();



public void setAuthorities(GrantedAuthority[] authorities);

}

UserDetailsImpl.java

package org.security;



import org.acegisecurity.GrantedAuthority;

import org.acegisecurity.userdetails.User;



/**
 * The class <code>UserDetailsImpl</code> extends the org.acegisecurity.userdetails.User class, and provides additional userId, companyId information
 * @author wade
 * 
 * @see IUserDetails, User
 */
public class UserDetailsImpl extends User implements IUserDetails{

private int user_id;

private int company_id;

private String username;

private GrantedAuthority[] authorities;



public UserDetailsImpl(String username, String password, boolean enabled,

boolean accountNonExpired, boolean credentialsNonExpired,

boolean accountNonLocked, GrantedAuthority[] authorities)

throws IllegalArgumentException {

super(username, password, enabled, accountNonExpired, credentialsNonExpired,

accountNonLocked, authorities);

setUsername(username);

setAuthorities(authorities);        

}



public UserDetailsImpl(int userid, int companyid, String username, String password, boolean enabled,

boolean accountNonExpired, boolean credentialsNonExpired,

boolean accountNonLocked, GrantedAuthority[] authorities)

throws IllegalArgumentException {

super(username, password, enabled, accountNonExpired, credentialsNonExpired,

accountNonLocked, authorities);

this.user_id = userid;

this.company_id = companyid;

setUsername(username);

setAuthorities(authorities);    

}



public int getUserId() {

return user_id;

}



public void setUserId(int user_id) {

this.user_id = user_id;

}



public int getCompanyId() {

return company_id;

}



public void setCompanyId(int company_id) {

this.company_id = company_id;

}



public String getUsername() {

return username;

}



public void setUsername(String username) {

this.username = username;

}



public GrantedAuthority[] getAuthorities() {

return authorities;

}



public void setAuthorities(GrantedAuthority[] authorities) {

this.authorities = authorities;

}

}

到此为止, 我们已经准备好了存放用户信息的类, 下面就开始动手修改取用户数据的代码.

假设我们用下面的SQL取用户信息:

SELECT u.user_id, u.company_id, email, password, enabled 

FROM role r, user_role ur, user u 

WHERE r.role_id = ur.role_id 

and ur.user_id = u.user_id 

and email = ? 

limit 1 

用下面的SQL取用户具有的Role列表

SELECT u.email, r.role_name 

FROM user_role ur, user u, role r 

WHERE ur.user_id = u.user_id 

and ur.role_id = r.role_id 

and u.email = ? 

我们需要修改的主要是两部分:

1. 取用户和用户角色的MappingSqlQuery, 增加了查询的userId和companyId.

2. loadUserByUsername方法, 修改了返回的对象类型,和很少的内部代码.

AcegiJdbcDaoImpl.java

package org.security.acegi;



import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Types;

import java.util.List;



import javax.sql.DataSource;



import org.acegisecurity.GrantedAuthority;

import org.acegisecurity.GrantedAuthorityImpl;

import org.acegisecurity.userdetails.UsernameNotFoundException;

import org.acegisecurity.userdetails.jdbc.JdbcDaoImpl;



import org.security.IUserDetails;

import org.security.UserDetailsImpl;

import org.springframework.dao.DataAccessException;

import org.springframework.jdbc.core.SqlParameter;

import org.springframework.jdbc.object.MappingSqlQuery;



/**
 * The class AcegiJdbcDaoImpl provides the method to get IUserDetail information from db which contains userId, companyId and UserDetail information.
 * 
 * @author wade
 *
 */
public class AcegiJdbcDaoImpl extends JdbcDaoImpl {

public static final String DEF_USERS_BY_USERNAME_QUERY =

"SELECT u.user_id, u.company_id, email, password, enabled from role r, user_role ur, user u where r.role_id = ur.role_id and ur.user_id = u.user_id and email = ? limit 1";

public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =

"SELECT username,authority FROM authorities WHERE username = ?";



protected MappingSqlQuery rolesByUsernameMapping;

protected MappingSqlQuery usersByNameMapping;



private String authoritiesByUsernameQuery;

private String rolePrefix = "";

private String usersByUsernameQuery;

private boolean usernameBasedPrimaryKey = true;



public AcegiJdbcDaoImpl(){

usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;

authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;

}

public String getAuthoritiesByUsernameQuery() {

return authoritiesByUsernameQuery;

}



public String getRolePrefix() {

return rolePrefix;

}



public String getUsersByUsernameQuery() {

return usersByUsernameQuery;

}



protected void initMappingSqlQueries() {

this.usersByNameMapping = new UsersByUsernameMapping(getDataSource());

this.rolesByUsernameMapping = new AuthoritiesByUsernameMapping(getDataSource());

}



/**
     * Allows the default query string used to retrieve authorities based on username to be overriden, if
     * default table or column names need to be changed. The default query is {@link
     * #DEF_AUTHORITIES_BY_USERNAME_QUERY}; when modifying this query, ensure that all returned columns are mapped
     * back to the same column names as in the default query.
     *
     * @param queryString The query string to set
     */
public void setAuthoritiesByUsernameQuery(String queryString) {

authoritiesByUsernameQuery = queryString;

}



/**
     * Allows a default role prefix to be specified. If this is set to a non-empty value, then it is
     * automatically prepended to any roles read in from the db. This may for example be used to add the
     * <code>ROLE_</code> prefix expected to exist in role names (by default) by some other Acegi Security framework
     * classes, in the case that the prefix is not already present in the db.
     *
     * @param rolePrefix the new prefix
     */
public void setRolePrefix(String rolePrefix) {

this.rolePrefix = rolePrefix;

}



/**
     * If <code>true</code> (the default), indicates the {@link #getUsersByUsernameQuery()} returns a username
     * in response to a query. If <code>false</code>, indicates that a primary key is used instead. If set to
     * <code>true</code>, the class will use the database-derived username in the returned <code>UserDetailsImpl</code>.
     * If <code>false</code>, the class will use the {@link #loadUserByUsername(String)} derived username in the
     * returned <code>UserDetailsImpl</code>.
     *
     * @param usernameBasedPrimaryKey <code>true</code> if the mapping queries return the username <code>String</code>,
     *        or <code>false</code> if the mapping returns a database primary key.
     */
public void setUsernameBasedPrimaryKey(boolean usernameBasedPrimaryKey) {

this.usernameBasedPrimaryKey = usernameBasedPrimaryKey;

}



/**
     * Allows the default query string used to retrieve users based on username to be overriden, if default
     * table or column names need to be changed. The default query is {@link #DEF_USERS_BY_USERNAME_QUERY}; when
     * modifying this query, ensure that all returned columns are mapped back to the same column names as in the
     * default query. If the 'enabled' column does not exist in the source db, a permanent true value for this column
     * may be returned by using a query similar to <br><pre>
     * "SELECT username,password,'true' as enabled FROM users WHERE username = ?"</pre>
     *
     * @param usersByUsernameQueryString The query string to set
     */
public void setUsersByUsernameQuery(String usersByUsernameQueryString) {

this.usersByUsernameQuery = usersByUsernameQueryString;

}



public IUserDetails loadUserByUsername(String username)

throws UsernameNotFoundException, DataAccessException {

List users = usersByNameMapping.execute(username);



if (users.size() == 0) {

throw new UsernameNotFoundException("User not found");

}



IUserDetails user = (IUserDetails) users.get(0); // contains no GrantedAuthority[]
List dbAuths = rolesByUsernameMapping.execute(user.getUsername());
addCustomAuthorities(user.getUsername(), dbAuths);
if (dbAuths.size() == 0) {

throw new UsernameNotFoundException("User has no GrantedAuthority");

}



GrantedAuthority[] arrayAuths = (GrantedAuthority[]) dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);



user.setAuthorities(arrayAuths);



if (!usernameBasedPrimaryKey) {

user.setUsername(username);

}



return user;

}



/**
     * Query object to look up a user's authorities.
     */
protected class AuthoritiesByUsernameMapping extends MappingSqlQuery {

protected AuthoritiesByUsernameMapping(DataSource ds) {

super(ds, authoritiesByUsernameQuery);

declareParameter(new SqlParameter(Types.VARCHAR));

compile();

}



protected Object mapRow(ResultSet rs, int rownum)

throws SQLException {

String roleName = rolePrefix + rs.getString(2);

GrantedAuthorityImpl authority = new GrantedAuthorityImpl(roleName);



return authority;

}

}



/**
     * Query object to look up a user.
     */
protected class UsersByUsernameMapping extends MappingSqlQuery {

protected UsersByUsernameMapping(DataSource ds) {

super(ds, usersByUsernameQuery);

declareParameter(new SqlParameter(Types.VARCHAR));

compile();

}



protected Object mapRow(ResultSet rs, int rownum)

throws SQLException {

int user_id = rs.getInt(1);

int company_id = rs.getInt(2);

String username = rs.getString(3);

String password = rs.getString(4);

boolean enabled = rs.getBoolean(5);



IUserDetails user = new UserDetailsImpl(username, password, enabled, true, true, true,

new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});

user.setUserId(user_id);

user.setCompanyId(company_id);

return user;

}

}

}

修改spring配置, 使用我们新建立的类:

    <bean id="userDetailsService"
class="org.security.acegi.AcegiJdbcDaoImpl">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="usersByUsernameQuery">
<value>
SELECT u.user_id, u.company_id, email, password, enabled
from role r, user_role ur, user u where r.role_id = ur.role_id and ur.user_id = u.user_id
and email = ?
limit 1
</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>
SELECT u.email, r.role_name FROM user_role ur, user u, role r WHERE
ur.user_id = u.user_id and ur.role_id = r.role_id and u.email = ?
</value>
</property>
</bean>

好了, 如果再有用户登录,就会调用我们的loadUserByUsername, 从数据库中读取用户数据了, 那用户的权限都有什么呢? 一个用户又对应着哪些ROLE呢? 下面先讲一下ACEGI 例子中的权限设置

2.2 将权限放在数据库中

截止到1.0.6版, Acegi没有提供直接从数据库读取权限的方法, 而是采用通过如下的配置设置权限:

    <bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager">
<bean class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<bean class="org.acegisecurity.vote.RoleVoter" />
<bean class="org.acegisecurity.vote.AuthenticatedVoter" />
</list>
</property>
</bean>
</property>
<property name="objectDefinitionSource">
<value><![CDATA[

CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON

PATTERN_TYPE_APACHE_ANT

/secure/extreme/**=ROLE_SUPERVISOR

/secure/**=IS_AUTHENTICATED_REMEMBERED

/project/**=IS_AUTHENTICATED_REMEMBERED

/task/**=ROLE_DEVELOPER                

/**=IS_AUTHENTICATED_ANONYMOUSLY

]]></value>
</property>
</bean>

而对大部分项目, 将权限放在数据库中可能是更灵活的, 为此, 我们需要写一个类去读取权限, 为了使这个类尽量简单, 我们把它做成PathBasedFilterInvocationDefinitionMap和RegExpBasedFilterInvocationDefinitionMap的代理类, PathBasedFilterInvocationDefinitionMap 采用的是Ant Path 风格的匹配方式, 而RegExpBasedFilterInvocationDefinitionMap采用的是Perl5风格的匹配方式. 用户可以通过在配置文件中设置来选择具体比较方式, 默认的比较方式是Ant Path 风格的匹配方式.

这样我们需要做的就是读取权限列表, 并放到相应的代理类里面, 而具体的比较则由代理类进行.

需要的表结构: Resource, Role_Resource

DROP TABLE IF EXISTS `resource`;



CREATE TABLE `resource` (

`resource_id` int(11) NOT NULL auto_increment,

`parent_resource_id` int(11) default NULL,

`resource_name` varchar(50) default NULL,

`description` varchar(100) default NULL,

PRIMARY KEY  (`resource_id`)

);



#

# Structure for the `resource_role` table : 

#



DROP TABLE IF EXISTS `resource_role`;



CREATE TABLE `resource_role` (

`resource_role_id` int(11) NOT NULL auto_increment,

`resource_id` int(11) NOT NULL,

`role_id` int(11) NOT NULL,

PRIMARY KEY  (`resource_role_id`)

);

添加我们的类:

AcegiJdbcDefinitionSourceImpl.java

package org.security.acegi;



import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;



import javax.sql.DataSource;



import org.acegisecurity.ConfigAttributeDefinition;

import org.acegisecurity.SecurityConfig;

import org.acegisecurity.intercept.web.FilterInvocationDefinitionMap;

import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;

import org.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap;

import org.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;



import org.security.IResourceRole;

import org.security.ResourceRoleImpl;

import org.security.event.IPermissionListener;

import org.security.event.PermissionEventPublisher;



import org.springframework.beans.factory.InitializingBean;

import org.springframework.jdbc.core.support.JdbcDaoSupport;

import org.springframework.jdbc.object.MappingSqlQuery;



/**
 * 
 * The class <code>AcegiJdbcDefinitionSourceImpl</code> is proxy to
 * PathBasedFilterInvocationDefinitionMap or RegExpBasedFilterInvocationDefinitionMap, This class get the permission
 * settings from the database, the default sql script is: SELECT resource, role
 * FROM role_permission, if it doesn't match your needs, changed it in bean
 * setting. <br>
 * 
 * <br>
 * $log___FCKpd___11lt;br>
 * <br>
 * 
 * @author $Author: wade $
 * @see
 */
public class AcegiJdbcDefinitionSourceImpl extends JdbcDaoSupport implements

InitializingBean, FilterInvocationDefinitionSource{

private Log logger = LogFactory.getLog(this.getClass());



public static final String DEF_PERMISSIONS_QUERY = "SELECT resource, role FROM role_permission";



/** The Perl5 expression */
String PERL5_KEY = "PATTERN_TYPE_PERL5";



/** The ant path expression */
String ANT_PATH_KEY = "PATTERN_TYPE_APACHE_ANT";



/* Set default to Ant Path Expression*/
private String resourceExpression = ANT_PATH_KEY;



private boolean convertUrlToLowercaseBeforeComparison = false;



private FilterInvocationDefinitionMap definitionSource = null;



private String permissionsQuery;



private String rolePrefix = "";



public AcegiJdbcDefinitionSourceImpl() {

permissionsQuery = DEF_PERMISSIONS_QUERY;

}



public String getAuthoritiesByUsernameQuery() {

return permissionsQuery;

}



public String getRolePrefix() {

return rolePrefix;

}



/**
     * Allows the default query string used to retrieve permissions to be
     * overriden, if default table or column names need to be changed. The
     * default query is {@link #DEF_PERMISSIONS_QUERY}; when modifying this
     * query, ensure that all returned columns are mapped back to the same
     * column names as in the default query.
     * 
     * @param queryString
     *            The query string to set
     */
public void setPermissionsQuery(String queryString) {

permissionsQuery = queryString;

}



/**
     * Allows a default role prefix to be specified. If this is set to a
     * non-empty value, then it is automatically prepended to any roles read in
     * from the db. This may for example be used to add the <code>ROLE_</code>
     * prefix expected to exist in role names (by default) by some other Acegi
     * Security framework classes, in the case that the prefix is not already
     * present in the db.
     * 
     * @param rolePrefix
     *            the new prefix
     */
public void setRolePrefix(String rolePrefix) {

this.rolePrefix = rolePrefix;

}



/**
     * Init the permission list from db
     * 
     */
protected void initMap() {

// return if we have got the latest permission list
if (definitionSource != null) {

return;

}



logger.debug("getting permissions from db");

if (PERL5_KEY.equals(getResourceExpression())) {

definitionSource = new RegExpBasedFilterInvocationDefinitionMap();

} else if (ANT_PATH_KEY.equals(getResourceExpression())) {

definitionSource = new PathBasedFilterInvocationDefinitionMap();

} else {

throw new IllegalArgumentException("wrong resourceExpression value");

}



definitionSource.setConvertUrlToLowercaseBeforeComparison(isConvertUrlToLowercaseBeforeComparison());



MappingSqlQuery permissionsMapping = new PermissionsMapping(

getDataSource());

List<IResourceRole> resources = permissionsMapping.execute();



Map<String, String> map = new HashMap<String, String>();

for (int i = 0; i < resources.size(); i++) {

ConfigAttributeDefinition defn = new ConfigAttributeDefinition();



String resource = resources.get(i).getResource();

if (map.containsKey(resource)) {

continue;

} else {

map.put(resource, resource);

}



for (int j = i; j < resources.size(); j++) {

IResourceRole resourceRole = resources.get(j);

if (resource.equals(resourceRole.getResource())) {

defn.addConfigAttribute(new SecurityConfig(resourceRole

.getRole()));

// logger.debug("added role: " + resourceRole.getRole());
}
}
definitionSource.addSecureUrl(resources.get(i).getResource(), defn);
// logger.debug("added roles to :" +
// resources.get(i).getResource());
}
}
/**
     * Query object to look up a user's authorities.
     */
protected class PermissionsMapping extends MappingSqlQuery {

protected PermissionsMapping(DataSource ds) {

super(ds, permissionsQuery);

compile();

}



protected IResourceRole mapRow(ResultSet rs, int rownum)

throws SQLException {

String resource = rs.getString(1);

String role = rolePrefix + rs.getString(2);

IResourceRole resourceRole = new ResourceRoleImpl(resource, role);



return resourceRole;

}

}



public ConfigAttributeDefinition getAttributes(Object object)

throws IllegalArgumentException {

initMap();



if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {

return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).getAttributes(object);

}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {

return ((PathBasedFilterInvocationDefinitionMap) definitionSource).getAttributes(object);

}



throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class); 

}



public Iterator getConfigAttributeDefinitions() {

initMap();

if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {

return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).getConfigAttributeDefinitions();

}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {

return ((PathBasedFilterInvocationDefinitionMap) definitionSource).getConfigAttributeDefinitions();

}



throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class); 

}



public boolean supports(Class clazz) {

initMap();



if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {

return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).supports(clazz);

}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {

return ((PathBasedFilterInvocationDefinitionMap) definitionSource).supports(clazz);

}



throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class); 

}





public String getResourceExpression() {

return resourceExpression;

}



public void setResourceExpression(String resourceExpression) {

this.resourceExpression = resourceExpression;

}



public boolean isConvertUrlToLowercaseBeforeComparison() {

return convertUrlToLowercaseBeforeComparison;

}



public void setConvertUrlToLowercaseBeforeComparison(

boolean convertUrlToLowercaseBeforeComparison) {

this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;

}



}

修改spring配置, 使用我们新建立的类和对应的SQL:

    <bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">

<property name="authenticationManager"
ref="authenticationManager" />

<property name="accessDecisionManager">

<bean class="org.acegisecurity.vote.AffirmativeBased">

<property name="allowIfAllAbstainDecisions"
value="false" />

<property name="decisionVoters">

<list>

<bean class="org.acegisecurity.vote.RoleVoter" />

<bean

class="org.acegisecurity.vote.AuthenticatedVoter" />

</list>

</property>

</bean>

</property>



<property name="objectDefinitionSource">

<ref bean="rolePermissionService"/>

</property>

</bean>



<bean id="rolePermissionService"
class="org.security.acegi.AcegiJdbcDefinitionSourceImpl">

<property name="dataSource">

<ref bean="dataSource" />

</property>

<property name="permissionsQuery">

<value>

SELECT resource_name, role_name FROM resource_role rr, resource re, role ro

WHERE rr.role_id = ro.role_id and rr.resource_id = re.resource_id

</value>

</property>

<property name="convertUrlToLowercaseBeforeComparison" value="false"></property>

<property name="resourceExpression" value="PATTERN_TYPE_APACHE_ANT"></property>

</bean>

2.3 使用JUnit进行测试

AcegiPermissionTestCase.java

package org.security;



import java.io.IOException;

import java.util.Iterator;

import java.util.LinkedHashMap;

import java.util.Map;

import java.util.Set;



import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;



import org.acegisecurity.AccessDeniedException;

import org.acegisecurity.Authentication;

import org.acegisecurity.ConfigAttributeDefinition;

import org.acegisecurity.GrantedAuthority;

import org.acegisecurity.GrantedAuthorityImpl;

import org.acegisecurity.intercept.web.FilterInvocation;

import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;

import org.acegisecurity.intercept.web.FilterSecurityInterceptor;

import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.mock.web.MockHttpServletRequest;

import org.springframework.mock.web.MockHttpServletResponse;



import org.security.BaseSpringTestCase;

import org.security.IResourceRole;

import org.security.IUserDetails;

import org.security.ResourceRoleImpl;

import org.security.acegi.AcegiJdbcDaoImpl;



/**
 * 
 * The class <code>AcegiPermissionTestCase</code> test acegi permission settings<br><br>
 * $log___FCKpd___13lt;br><br>
 * @author $Author: wade $
 * @version $Revision: 1.0 $
 * @see
 */
public class AcegiPermissionTestCase extends BaseSpringTestCase {

@Autowired

private FilterInvocationDefinitionSource objectDefinitionSource;



@Autowired

private AcegiJdbcDaoImpl userDetailsService;



@Autowired

private FilterSecurityInterceptor filterInvocationInterceptor;



/**
     * Get Authentication Token by username
     * @param username
     * @return Authentication
     */
protected Authentication getAuthentication(String username){

IUserDetails userDetail = userDetailsService.loadUserByUsername(username);

Authentication authenticated;

if(userDetail.isEnabled()){

authenticated = new UsernamePasswordAuthenticationToken(userDetail, username, userDetail.getAuthorities());

}else{

//            authenticated = new AnonymousAuthenticationToken(username, userDetail, userDetail.getAuthorities());
authenticated = new UsernamePasswordAuthenticationToken(null, null, new GrantedAuthority[]{new GrantedAuthorityImpl("ROLE_ANONYMOUS")});

}



return authenticated;

}



/**
     * get FilterInvocation from the url 
     * @param url
     * @return FilterInvocation
     */
protected FilterInvocation getRequestedResource(String url){

MockHttpServletRequest request = new MockHttpServletRequest();

request.setServletPath(url);



MockHttpServletResponse response = new MockHttpServletResponse();

FilterChain filterchain = new FilterChain(){

public void doFilter(ServletRequest arg0, ServletResponse arg1)

throws IOException, ServletException {            

}};



FilterInvocation object = new FilterInvocation(request, response, filterchain);



return object;

}



/**
     * throws AccessDeniedException if no permission 
     * @param username
     * @param uri
     */
public void checkPermission(boolean shouldHasPermission, String username, String url){

Authentication authenticated = getAuthentication(username);

FilterInvocation object = getRequestedResource(url);



ConfigAttributeDefinition attr = objectDefinitionSource.getAttributes(object);

boolean hasPermission = false;



try{

filterInvocationInterceptor.getAccessDecisionManager().decide(authenticated, object, attr);

hasPermission = true;

}catch(AccessDeniedException e){

hasPermission = false;            

}



if(hasPermission){

assertTrue(username + " shouldn't be able to access " + url, shouldHasPermission);    

}else{

assertFalse(username + " should be able to access " + url, shouldHasPermission);    

}

}





public void testPermissionForAdmin(){

Map<IResourceRole, Boolean> map = new LinkedHashMap<IResourceRole, Boolean>();



map.put(new ResourceRoleImpl("/admin/index.jsp", "admin"    ), true);

map.put(new ResourceRoleImpl("/admin/index.jsp", "project"    ), false);

map.put(new ResourceRoleImpl("/admin/index.jsp", "dev"        ), false);

map.put(new ResourceRoleImpl("/admin/index.jsp", "disabled"    ), false);



map.put(new ResourceRoleImpl("/admin", "admin"    ), true);

map.put(new ResourceRoleImpl("/admin", "project"), false);

map.put(new ResourceRoleImpl("/admin", "dev"    ), false);

map.put(new ResourceRoleImpl("/admin", "disabled"), false);



map.put(new ResourceRoleImpl("/project/index.jsp", "admin"    ), true);

map.put(new ResourceRoleImpl("/project/index.jsp", "project"), true);

map.put(new ResourceRoleImpl("/project/index.jsp", "dev"    ), false);

map.put(new ResourceRoleImpl("/project/index.jsp", "disabled"), false);



map.put(new ResourceRoleImpl("/project", "admin"    ), true);

map.put(new ResourceRoleImpl("/project", "project"    ), true);

map.put(new ResourceRoleImpl("/project", "dev"        ), false);

map.put(new ResourceRoleImpl("/project", "disabled"    ), false);



map.put(new ResourceRoleImpl("/developer/index.jsp", "admin"    ), true);

map.put(new ResourceRoleImpl("/developer/index.jsp", "project"    ), true);

map.put(new ResourceRoleImpl("/developer/index.jsp", "dev"        ), true);

map.put(new ResourceRoleImpl("/developer/index.jsp", "disabled"    ), false);



map.put(new ResourceRoleImpl("/developer", "admin"        ), true);

map.put(new ResourceRoleImpl("/developer", "project"    ), true);

map.put(new ResourceRoleImpl("/developer", "dev"        ), true);

map.put(new ResourceRoleImpl("/developer", "disabled"    ), false);



map.put(new ResourceRoleImpl("/index.jsp", "admin"    ), true);

map.put(new ResourceRoleImpl("/index.jsp", "project"), true);

map.put(new ResourceRoleImpl("/index.jsp", "dev"    ), true);

map.put(new ResourceRoleImpl("/index.jsp", "disabled"), true);



map.put(new ResourceRoleImpl("/acegilogin.jsp", "admin"    ), true);

map.put(new ResourceRoleImpl("/acegilogin.jsp", "project"    ), true);

map.put(new ResourceRoleImpl("/acegilogin.jsp", "dev"    ), true);

map.put(new ResourceRoleImpl("/acegilogin.jsp", "disabled"    ), true);



Set<IResourceRole> keySet= map.keySet();

Iterator<IResourceRole> ita = keySet.iterator();

while(ita != null && ita.hasNext()){

IResourceRole resourceRole = ita.next();

boolean expectedPermission = map.get(resourceRole);



checkPermission(expectedPermission, resourceRole.getRole(), resourceRole.getResource());

}            

}

}

三. 集成之后

3.1 更改数据库中的权限

到目前为止, 一切顺利, 但是有一个问题, 用户如何修改权限, 修改后我们写的类如何能知道权限变了, 需要去重新加载呢? 看来我们需要再加一些代码以便于在权限被修改后能够得到消息, 然后去刷新权限.

为此, 我们使用Observe(观察者) 模式, 在改变权限后, 由改变权限的类通过调用PermissionEventPublisher.update(this.getClass())发出消息说权限变了.

IPermissionListener.java

public interface IPermissionListener {

public void updatePermission(Class eventSource);

}

PermissionEventPublisher.java

package org.security.event;



import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;



import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;





/**
 * The class PermissionEventPublisher provides a way to notify the IPermissionListener that the permission has been changed.
 * @author wade
 *
 */
public class PermissionEventPublisher {

private static Log logger = LogFactory.getLog(PermissionEventPublisher.class);



private static Map<IPermissionListener, IPermissionListener> observerList = 

new HashMap<IPermissionListener, IPermissionListener>();



/**
     * Attach a listener for permission event
     * 
     * @param subject
     * @param listener
     */
public static void attach(IPermissionListener listener){

observerList.put(listener, listener);



if(logger.isDebugEnabled()){

logger.debug("Added listener: " + listener.getClass().getName());

}

}



/**
     * Detatch from the event updater
     * @param listener
     */
public static void detatch(IPermissionListener listener){

observerList.remove(listener);



if(logger.isDebugEnabled()){

logger.debug("Removeded listener: " + listener.getClass().getName());

}

}



/**
     * send message to each listener.
     * @param eventSource
     */
public static void update(Class eventSource){

if(logger.isDebugEnabled()){

logger.debug("permission changed from "+eventSource.getName());

}



Iterator<IPermissionListener> ita = observerList.keySet().iterator();

while(ita.hasNext()){

IPermissionListener permissionListener = ita.next();

permissionListener.updatePermission(eventSource);



if(logger.isDebugEnabled()){

logger.debug("call update for listener=" + permissionListener.getClass().getName());

}

}

}

}

修改AcegiJdbcDefinitionSourceImpl.java, 增加updatePermission方法, 在权限变化后进行处理

public class AcegiJdbcDefinitionSourceImpl extends JdbcDaoSupport implements

InitializingBean, FilterInvocationDefinitionSource, IPermissionListener {



public AcegiJdbcDefinitionSourceImpl() {

permissionsQuery = DEF_PERMISSIONS_QUERY;



//attach to event publisher, so the class can get the notify when permission changes
PermissionEventPublisher.attach(this);

}



/**
     * Set definitionSource to null, so we can get a refreshed permission list from db
     */
public void updatePermission(Class eventSource) {

definitionSource = null;

}

}

3.2 在程序中获取当前用户

直接从Acegi中取用户信息不太方便, 为了简化获取用户的方法, 可以添加一个类封装对应的逻辑, 然后通过CurrentUser.getUser()直接取到用户信息.

CurrentUser.java

/**
     * Get current user which stored in session
     * You must set a user when using junit test
     * @return IUserDetails
     */
public static IUserDetails getUser(){

//if not in unit test environment, get the current user using acegi
if ((SecurityContextHolder.getContext() == null)

|| !(SecurityContextHolder.getContext() instanceof SecurityContext)

|| (((SecurityContext) SecurityContextHolder.getContext())

.getAuthentication() == null)) {

return null;

}



Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth.getPrincipal() == null) {

return null;

}



IUserDetails user = null;

if (auth.getPrincipal() instanceof IUserDetails) {

user = (IUserDetails)auth.getPrincipal(); 

}



return user;

}

3.3 使用Tag来判断用户是否具有某一种Role的权限

有一点一定要注意, 由于Filter的处理有顺序,所以需要将Acegi的Filter放在最前面.

<authz:authorize ifAnyGranted="ROLE_SUPERVISOR, ROLE_ADMINISTRATOR, ROLE_FULLACCESS">

Role in ROLE_SUPERVISOR, ROLE_ADMINISTRATOR, ROLE_FULLACCESS

</authz:authorize>

3.4 添加自己的Tag

Acegi 提供的Tag只能判断当前用户是不是具有某种Role, 不能判断当前用户对某一个URL有没有权限, 由于很多时候需要根据当前用户的权限来控制某些功能是否显示, 比如只有管理员才显示Add或Delete按钮

这是你可以自己写自己的Tag, 为了简单起见, 我们继承jstl的Tag, 比如下面实现两个条件的Tag, Tag的用法如下:

<auth:ifNotAuthrized url="/system/acl.action">如果当前用户没有指定url的权限,显示本部分内容</auth:ifNotAuthrized>

<auth:ifAuthrized url="/system/acl.action">如果当前用户有指定url的权限,显示本部分内容</auth:ifAuthrized>

AuthorizedTag.java

public class AuthorizedTag extends ConditionalTagSupport {

protected Log logger = LogFactory.getLog(this.getClass());



@Autowired

private FilterInvocationDefinitionSource objectDefinitionSource;



@Autowired    

private FilterSecurityInterceptor filterInvocationInterceptor;



private String url;



/**
     * Get Authentication Token from  IUserDetails object
     * @param user
     * @return Authentication
     */
protected Authentication getAuthentication(IUserDetails user){

IUserDetails userDetail = user;

Authentication authenticated;



if(userDetail == null){

authenticated = new UsernamePasswordAuthenticationToken(null, null, new GrantedAuthority[]{new GrantedAuthorityImpl("ROLE_ANONYMOUS")});            

}else{

if(userDetail.isEnabled()){

authenticated = new UsernamePasswordAuthenticationToken(userDetail, userDetail.getUsername(), userDetail.getAuthorities());

}else{

authenticated = new AnonymousAuthenticationToken(userDetail.getUsername(), userDetail, userDetail.getAuthorities());

}

}



return authenticated;

}    



/**
     * get FilterInvocation from the url 
     * @param url
     * @return FilterInvocation
     */
protected FilterInvocation getRequestedResource(String url){

MockHttpServletRequest request = new MockHttpServletRequest(pageContext.getServletContext());

request.setServletPath(url);



FilterChain filterchain = new FilterChain(){

public void doFilter(ServletRequest arg0, ServletResponse arg1)

throws IOException, ServletException {            

}};



FilterInvocation object = new FilterInvocation(request, pageContext.getResponse(), filterchain);



return object;

}



@Override

protected boolean condition() throws JspTagException {

boolean result = false;



IUserDetails user = CurrentUser.getUser();



ServletContext servletContext = pageContext.getServletContext();        

WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

wac.getAutowireCapableBeanFactory().autowireBeanProperties(this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);



ConfigAttributeDefinition attr = objectDefinitionSource.getAttributes(getRequestedResource(url));        

try{

filterInvocationInterceptor.getAccessDecisionManager().decide(getAuthentication(user), url, attr);

result = true;

}catch(AccessDeniedException e){

result = false;

if(user == null){

logger.debug("anonymous has no permission on :" + url);

}else{

logger.debug(user.getUsername() + " has no permission on :" + url);

}

}



return result;

}



public String getUrl() {

return url;

}



public void setUrl(String url) {

this.url = url;

}



}

添加Jsp页面测试新添加的Tag, 在文所附的例子程序中, 将Tag的测试代码放在index.jsp页面中, 任何人都可以访问该页面, 在页面上列出了全部地址的链接, 同时列出了当前用户有权限的地址, 这样可以方便地知道当前用户有哪些权限, 如果你想修改数据库中的权限, 然后再次测试, 可以点击页面右上侧的Reload Permission重新从数据库加载权限.

<auth:ifAuthrized url="/admin">
<p><a href="admin">Admin page</a></p>
</auth:ifAuthrized>

四. 参考文档

1. 更多深入介绍,可以根据Acegi官方提供的Suggested Steps (http://www.acegisecurity.org/suggested.html) 一步一步学习.

2. 如果要了解Acegi提供的各种功能, 可以参考http://www.acegisecurity.org/reference.html

3. 阅读本文需要对Spring有一定的了解, http://www.springframework.org/documentation

4. 扩展jstl的tag, 可以参看http://www.onjava.com/pub/a/onjava/2002/10/30/jstl3.html?page=1

5. 从https://sourceforge.net/project/platformdownload.php?group_id=216220下载本文附带的例子代码, 通过acegi.sql建立数据库, 然后将acegi-test.war放到Tomcat的webapps目录下, 或者你可以下载acegi-test.zip文件, 里面包含了完整的eclipse的项目以及sql文件.

访问http://youip:port/acegi-test, 列出全部地址的链接, 同时列出了当前用户有权限的地址链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值