集成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這個表反向生成的類吧????
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章