一. 簡單介紹
1.1 本文目的
集成Acegi到自己的項(xiàng)目中, 并且將用戶信息和權(quán)限放到數(shù)據(jù)庫, 提供方法允許權(quán)限動(dòng)態(tài)變化,變化后自動(dòng)加載最新的權(quán)限
本文介紹Acegi例子的時(shí)候采用的是acegi-security-samples-tutorial-1.0.6.war
閱讀本文需要對Spring有一定的了解, 如果你還沒有接觸過, 有些地方可能不容易理解, 這時(shí)候可能需要參考本文后附的Spring地址, 先了解一下Spring的基本知識(shí).
本文使用的是Mysql數(shù)據(jù)庫, 如果你使用其他的數(shù)據(jù)庫, 可能需要修改相應(yīng)的SQL.
本文及所附的全部代碼放在http://acegi-test.sourceforge.net/
1.2 安裝與配置
項(xiàng)目主頁: http://www.acegisecurity.org/
下載地址: http://sourceforge.net/project/showfiles.php?group_id=104215
解壓文件后, 將acegi-security-samples-tutorial-1.0.6.war復(fù)制Your_Tomcat_Path/webapps/
啟動(dòng)Tomcat, 訪問http://localhost:8080/acegi-security-samples-tutorial-1.0.6/
點(diǎn)擊頁面上任何一個(gè)鏈接,都需要用戶登錄后訪問, 可以在頁面上看到可用的用戶名和密碼.
二. 開始集成到自己的程序中
2.1 將用戶和角色放在數(shù)據(jù)庫中
可能是為了演示方便, 簡單的展示Acegi如何控制權(quán)限, 而不依賴于任何數(shù)據(jù)庫, ACEGI給出的例子采用InMemoryDaoImpl獲取用戶信息, 用戶和角色信息放在WEB-INF/users.properties 文件中, InMemoryDaoImpl 一次性的從該配置文件中讀出用戶和角色信息, 格式是: 用戶名=密碼, 角色名, 如第一行是:
marissa=koala,ROLE_SUPERVISOR
就是說marissa的密碼是koala, 并且他的角色是ROLE_SUPERVISOR
對這個(gè)文件的解析是通過applicationContext-acegi-security.xml中如下的設(shè)置進(jìn)行的:
<!-- 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的支持, 由于使用數(shù)據(jù)庫進(jìn)行驗(yàn)證比較常見, 下面僅就jdbc實(shí)現(xiàn)做出介紹.
不管是InMemoryDaoImpl還是JdbcDaoImpl都是實(shí)現(xiàn)了UserDetailsService接口, 而這個(gè)接口里只定義了一個(gè)方法: UserDetails loadUserByUsername(String username) 就是根據(jù)用戶名加載UserDetails對象, UserDetails也是一個(gè)接口, 定義了一個(gè)用戶所需要的基本信息, 包括: username, password, authorities等信息
2.1.1 直接使用JdbcDaoImpl 訪問數(shù)據(jù)庫中的用戶信息
如果ACEGI提供的信息滿足你的需要, 也就是說你只需要用戶的username, password等信息, 你可以直接使用ACEGI提供的Schema, 這樣, 不需要任何變動(dòng), JdbcDaoImpl就可以使用了.
如果你的數(shù)據(jù)庫已經(jīng)定義好了, 或者不想使用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 擴(kuò)展JdbcDaoImpl獲取更多用戶信息
如果上面提到的定制查詢SQL語句不能提供足夠的靈活性, 那么你可能就需要定義一個(gè)JdbcDaoImpl的子類, 如果變動(dòng)不大, 通過覆蓋initMappingSqlQueries方法重新定義MappingSqlQuery的實(shí)例. 而如果你需要獲取更多信息, 比如userId, companyId等, 那就需要做更多的改動(dòng), 第一種改動(dòng)不大, 所以不具體介紹, 下面以第二種改動(dòng)為例,介紹如何實(shí)現(xiàn)這種需求.
我們需要三張表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接口中只定義了一個(gè)方法: UserDetails loadUserByUsername(String username), UserDetails中不存在我們需要的userId 和companyId等信息, 所以我們首先需要擴(kuò)展UserDetails接口, 并擴(kuò)展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;
}
}
到此為止, 我們已經(jīng)準(zhǔn)備好了存放用戶信息的類, 下面就開始動(dòng)手修改取用戶數(shù)據(jù)的代碼.
假設(shè)我們用下面的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方法, 修改了返回的對象類型,和很少的內(nèi)部代碼.
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>
好了, 如果再有用戶登錄,就會(huì)調(diào)用我們的loadUserByUsername, 從數(shù)據(jù)庫中讀取用戶數(shù)據(jù)了, 那用戶的權(quán)限都有什么呢? 一個(gè)用戶又對應(yīng)著哪些ROLE呢? 下面先講一下ACEGI 例子中的權(quán)限設(shè)置
2.2 將權(quán)限放在數(shù)據(jù)庫中
截止到1.0.6版, Acegi沒有提供直接從數(shù)據(jù)庫讀取權(quán)限的方法, 而是采用通過如下的配置設(shè)置權(quán)限:
<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>
而對大部分項(xiàng)目, 將權(quán)限放在數(shù)據(jù)庫中可能是更靈活的, 為此, 我們需要寫一個(gè)類去讀取權(quán)限, 為了使這個(gè)類盡量簡單, 我們把它做成PathBasedFilterInvocationDefinitionMap和RegExpBasedFilterInvocationDefinitionMap的代理類, PathBasedFilterInvocationDefinitionMap 采用的是Ant Path 風(fēng)格的匹配方式, 而RegExpBasedFilterInvocationDefinitionMap采用的是Perl5風(fēng)格的匹配方式. 用戶可以通過在配置文件中設(shè)置來選擇具體比較方式, 默認(rèn)的比較方式是Ant Path 風(fēng)格的匹配方式.
這樣我們需要做的就是讀取權(quán)限列表, 并放到相應(yīng)的代理類里面, 而具體的比較則由代理類進(jìn)行.
需要的表結(jié)構(gòu): 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配置, 使用我們新建立的類和對應(yīng)的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進(jìn)行測試
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 更改數(shù)據(jù)庫中的權(quán)限
到目前為止, 一切順利, 但是有一個(gè)問題, 用戶如何修改權(quán)限, 修改后我們寫的類如何能知道權(quán)限變了, 需要去重新加載呢? 看來我們需要再加一些代碼以便于在權(quán)限被修改后能夠得到消息, 然后去刷新權(quán)限.
為此, 我們使用Observe(觀察者) 模式, 在改變權(quán)限后, 由改變權(quán)限的類通過調(diào)用PermissionEventPublisher.update(this.getClass())發(fā)出消息說權(quán)限變了.
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方法, 在權(quán)限變化后進(jìn)行處理
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 在程序中獲取當(dāng)前用戶
直接從Acegi中取用戶信息不太方便, 為了簡化獲取用戶的方法, 可以添加一個(gè)類封裝對應(yīng)的邏輯, 然后通過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的權(quán)限
有一點(diǎn)一定要注意, 由于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只能判斷當(dāng)前用戶是不是具有某種Role, 不能判斷當(dāng)前用戶對某一個(gè)URL有沒有權(quán)限, 由于很多時(shí)候需要根據(jù)當(dāng)前用戶的權(quán)限來控制某些功能是否顯示, 比如只有管理員才顯示Add或Delete按鈕
這是你可以自己寫自己的Tag, 為了簡單起見, 我們繼承jstl的Tag, 比如下面實(shí)現(xiàn)兩個(gè)條件的Tag, Tag的用法如下:
<auth:ifNotAuthrized url="/system/acl.action">如果當(dāng)前用戶沒有指定url的權(quán)限,顯示本部分內(nèi)容</auth:ifNotAuthrized>
<auth:ifAuthrized url="/system/acl.action">如果當(dāng)前用戶有指定url的權(quán)限,顯示本部分內(nèi)容</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頁面中, 任何人都可以訪問該頁面, 在頁面上列出了全部地址的鏈接, 同時(shí)列出了當(dāng)前用戶有權(quán)限的地址, 這樣可以方便地知道當(dāng)前用戶有哪些權(quán)限, 如果你想修改數(shù)據(jù)庫中的權(quán)限, 然后再次測試, 可以點(diǎn)擊頁面右上側(cè)的Reload Permission重新從數(shù)據(jù)庫加載權(quán)限.
<auth:ifAuthrized url="/admin">
<p><a href="admin">Admin page</a></p>
</auth:ifAuthrized>
四. 參考文檔
1. 更多深入介紹,可以根據(jù)Acegi官方提供的Suggested Steps (http://www.acegisecurity.org/suggested.html) 一步一步學(xué)習(xí).
2. 如果要了解Acegi提供的各種功能, 可以參考http://www.acegisecurity.org/reference.html
3. 閱讀本文需要對Spring有一定的了解, http://www.springframework.org/documentation
4. 擴(kuò)展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建立數(shù)據(jù)庫, 然后將acegi-test.war放到Tomcat的webapps目錄下, 或者你可以下載acegi-test.zip文件, 里面包含了完整的eclipse的項(xiàng)目以及sql文件.
訪問http://youip:port/acegi-test, 列出全部地址的鏈接, 同時(shí)列出了當(dāng)前用戶有權(quán)限的地址鏈接