集成Acegi到自己的项目

一. 简单介绍

1.1 本文目的

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

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

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

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

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$<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$<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, 列出全部地址的链接, 同时列出了当前用户有权限的地址链接


转自:http://acegi-test.sourceforge.net/



绿色通道:好文要顶关注我收藏该文与我联系


eafy.ye
关注 - 0
粉丝 - 1



+加关注


0

0


(请您对文章做出评价)


« 上一篇:关于远程调用(XFire/HttpInvoker/Hessian etc.)及远程服务管理的一些随想
» 下一篇:Acegi+hibernate 动态实现基于角色的权限管理


posted @ 2008-03-04 17:30 eafy.ye 阅读(3398) 评论(11)编辑 收藏


发表评论



 回复 引用 
#1楼2008-03-13 11:41 | winie[未注册用户]
我正在找这方面的资料````````呵呵`````写的good


 回复 引用 
#2楼2008-04-22 18:12 | QQ:23390520[未注册用户]
哥们你的文章很好,你一定是个令人佩服之人

但是我在进行 登陆的时候,选中复选框没有问题。
可是当我 关闭浏览器(没有退出)的时候,再次登陆 就不能重建session了

哥们,麻烦你解决下哦


 回复 引用 
#3楼2008-07-06 17:27 | hunterk[未注册用户]
你的文章很详细,非常不错~~


 回复 引用 
#4楼2008-07-28 11:50 | 树的回忆[未注册用户]
你的文章写得很清楚,谢谢楼主咯!


 回复 引用 
#5楼2008-10-16 11:14 | 世玉[未注册用户]
请帮我解决个问题:

http://topic.csdn.net/u/20081014/20/c3562b08-8d0f-41fa-aa7f-59ef63b63513.html
不胜感激!!!


 回复 引用 
#6楼2008-12-03 10:32 | Ivan's Blog[未注册用户]
Actually there is no need such complex to make the authrozation supports DB, all things only to do is to extends AbstractObjectDefinitionSource and override the lookAttribute method to fetchs security definitions from db or cache(for better performance).


 回复 引用 
#7楼2009-06-21 23:24 | 张志juney[未注册用户]
您的文章写的非常好

请问一下,在监听resource改动时,重新加载权限这个地方,definitionSource这个变量是哪个类里面的啊?我怎么就是找不到这个属性,如果方便的话请加我qq:75492100,请帮我一个忙。非常感谢。


 回复 引用 查看 
#8楼2009-07-04 19:00 | acegi
好乱
没有条理,越看越复杂
而且里面你用到得类或接口(自己定义的),根本就没交代
会让初学的人看了想死
因为你给的东西不全
你自己写的有些类,接口都没贴出来

写教程不是这么写滴
写教程要把配置文件,代码等完完全全贴出来
不是从中抽出一些贴出来

看的我头昏脑胀
一步一步按你的配下来
结果。。。有些类都不知道是啥
你也没贴出来
。。。


 回复 引用 查看 
#9楼2009-07-04 19:06 | acegi
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`)
);

String resource = rs.getString(1);
String role = rolePrefix + rs.getString(2);
IResourceRole resourceRole = new ResourceRoleImpl(resource, role);
return resourceRole;

上面建表的时候明明是int(11)
怎么到下面的时候resource,role是String了???????

完全没交代IResourceRole和ResourceRoleImpl是啥
不过我猜应该是resource_role这个表反向生成的类吧????
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值