安全永遠是WEB應用系統必須面對的頭等大事, 也是最頭疼的事, 其實安全系統就只包括兩個問題: 認證和授權.
以前做些網站系統, 安全檢測邏輯都在放在須要安全控制的代碼前面, 這樣做有很多不好的地方, 重復多次的編碼就不用說了, 代碼移植性, 重用性都得不到體現, 安全檢測邏輯要永遠和業務邏輯放在一起.
那麼, 能不能夠在進入方法前就調用一些安全檢測? 其實Spring AOP就是這個思想, 那麼又如何實現安全檢測呢? Spring Acegi Security 框架就是做這個事情.
本文主要是討論下在已有的SSH系統中, 如何使用Acegi作為安全框架實現基於角色的權限控制(Role Based Access Control RBAC) , 本文主要是以Java 5注解的形式來配置安全框架, 大大減化配置和操作.
本文的主要參考資料: <Spring 2.0 核心技術與最佳實踐> 第10章 (Spring Acegi 安全框架)
<精通Spring 2.X -- 企業應用開發詳解> 第17章 (使用Acegi 實施應用系統安全)
acegi-security-1.0.6 官方文檔
說明: 本文介紹的是RBAC, 在官方文檔的基礎上有所擴展或改動, 以更適合WEB應用系統. 其實我覺得大多數的網站基於角色已經足夠了, 一般都沒必要基於權限.
文章開始:
一. 下載所要的軟件或JAR包:
我的相關配置是: Java 5, Tomcat 5.5.26, Struts 2.0.11, Spring 2.5.1, Hibernate 3.2, Acegi 1.0.6
二. 建立相關的數據庫:
數據表: 用戶信息表User: id, enable, user_name, user_pass, email_box
角色信息表RoleInfo: id, role_name, role_title, descp
用戶與角色關聯表(用戶與角色是多對多關系)UserRole: user_id, user_name, role_id, role_name
並在這三個表中插入相關的數據, 我是定義了兩種角色(role_name): ROLE_USER, ROLE_ADMIN
和三個用戶, 一個用戶角色為: ROLE_USER, ROLE_ADMIN
另一個用戶角色為: ROLE_USER
第三個沒有角色.
二. 修改配置文件:
其實對Acegi框架的應用難點就在配置文件, 所以要特別注意了:
在 src 建立Acegi的配置文件: acegi-security.xml 當然這個文件的名稱是可以任意的.
acegi-security.xml 說白了就是配置: 安全攔截器, 認證管理器, 決策管理器.
其內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- ========================= 認證管理器 ========================= -->
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider" />
<ref bean="rememberMeAuthenticationProvider" />
</list>
</property>
</bean>
<!-- 基於DAO驗證的AuthenticationProvider -->
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService" />
</bean>
<bean id="userDetailsService" class="org.ymcn.security.AcegiUserDeitailsService">
<property name="userDao" ref="userDao" />
<property name="userRoleDao" ref="userRoleDao" />
</bean>
<bean id="rememberMeAuthenticationProvider" class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
<property name="key" value="[email protected]" />
</bean>
<bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="userDetailsService" />
<property name="parameter" value="j_remember_me" />
<property name="key" value="[email protected]" />
<property name="tokenValiditySeconds" value="31536000" />
</bean>
<!-- ========================= 決策管理器 ========================= -->
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="decisionVoters">
<list>
<ref bean="roleVoter" />
</list>
</property>
<!-- 是否全部棄權就通過 -->
<property name="allowIfAllAbstainDecisions" value="false" />
</bean>
<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter">
<property name="rolePrefix" value="ROLE_" />
</bean>
<!-- ========================= 過濾器鏈 ========================= -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,rememberMeFilter,exceptionFilter,securityInterceptor
</value>
</property>
</bean>
<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" />
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<!-- 登錄退出後的URL -->
<constructor-arg value="/" />
<constructor-arg>
<list>
<ref bean="rememberMeServices" />
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
</list>
</constructor-arg>
<!-- 登錄退出的URL -->
<property name="filterProcessesUrl" value="/j_logout.j" />
</bean>
<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<!-- 登錄失敗後的URL -->
<property name="authenticationFailureUrl" value="/login.jsp?msg=%E6%97%A0%E6%95%88%E7%9A%84%E7%94%A8%E6%88%B7%E5%90%8D%E6%88%96%E5%8F%A3%E4%BB%A4" />
<!-- 登錄成功後的URL -->
<property name="defaultTargetUrl" value="/user/cmd.jsp" />
<!-- 登錄的URL -->
<property name="filterProcessesUrl" value="/j_login.j" />
<property name="rememberMeServices" ref="rememberMeServices" />
</bean>
<bean id="rememberMeFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="rememberMeServices" ref="rememberMeServices" />
</bean>
<bean id="exceptionFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<!-- 出現AuthenticationException時的登錄入口 -->
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/login.jsp" />
<property name="forceHttps" value="false" />
</bean>
</property>
<!-- 出現AccessDeniedException時的Handler -->
<property name="accessDeniedHandler">
<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/denied.jsp" />
</bean>
</property>
</bean>
<bean id="securityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/admin/**=ROLE_ADMIN
/user/**=ROLE_USER
/cart/previeworder*=ROLE_USER
</value>
</property>
</bean>
</beans>
在上面的配置文件中, 紅色部分要特別注意, 其余的內容都差不多了.
<bean id="userDetailsService" class="org.ymcn.security.AcegiUserDeitailsService">
<property name="userDao" ref="userDao" />
<property name="userRoleDao" ref="userRoleDao" />
</bean>
在整個應用的安全控制中, 我們唯一要編寫代碼的類就是: org.ymcn.security.AcegiUserDeitailsService
就連登錄和登出的代碼也不要了.
三. 修改 web.xml, 增加安全控制過濾鏈.
<filter>
<filter-name>acegiFilterChain</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>acegiFilterChain</filter-name>
<url-pattern>*.j</url-pattern>
</filter-mapping>
注意: 這個過濾器一定要在MVC轉發過濾器的前面!!!!
四. 在 applicationContext.xml 中增加 Acegi安全控制攔截器 和 Spring的自動代理功能實現AOP代理
<!-- Acegi安全控制攔截器 -->
<bean id="serviceSecurityInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="validateConfigAttributes" value="true" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<bean class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes">
<bean class="org.acegisecurity.annotation.SecurityAnnotationAttributes" />
</property>
</bean>
</property>
</bean>
<!-- 利用Spring的自動代理功能實現AOP代理 -->
<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
<value>serviceSecurityInterceptor</value>
</list>
</property>
<property name="beanNames">
<list>
<value>userService</value>
<value>mailService</value>
</list>
</property>
</bean>
五. 編寫在利用Acegi框架唯一要我們編寫的類 AcegiUserDeitailsService.java
package org.ymcn.security;
import java.util.List;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.ymcn.dao.UserDao;
import org.ymcn.dao.UserRoleDao;
import org.ymcn.model.User;
import org.ymcn.model.UserRole;
public class AcegiUserDeitailsService implements UserDetailsService {
private final Log LOG = LogFactory.getLog(AcegiUserDeitailsService.class);
/* 依賴注入 */
private UserDao userDao;
private UserRoleDao userRoleDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setUserRoleDao(UserRoleDao userRoleDao) {
this.userRoleDao = userRoleDao;
}
/* 用戶所有的權限 */
//private final List<GrantedAuthority> grantedAuthList = new ArrayList<GrantedAuthority>(6);
private GrantedAuthority[] grantedAuthArray;
public UserDetails loadUserByUsername(String userName)
throws UsernameNotFoundException, DataAccessException {
if(LOG.isDebugEnabled()) {
LOG.debug("Loading UserDetails of userName: " + userName);
}
/* 取得用戶 */
User user = userDao.getUserByName(userName);
if(user == null) {
LOG.warn("UserDetails load failed: No such UserRole with userName: " + userName);
throw new UsernameNotFoundException("User name is not found.");
}
/* 取得所有用戶權限 */
List<UserRole> userRoleList = userRoleDao.getUserRoleByUserName(userName);
if(userRoleList == null || userRoleList.size() == 0) {
LOG.warn("UserRole load failed: No such UserRole with userName: " + userName);
throw new UsernameNotFoundException("UserRole is not found.");
}
/* 取得用戶的所有角色 */
int size = userRoleList.size();
grantedAuthArray = new GrantedAuthority[size];
int j = 0;
for(int i = 0; i < size; i++) {
UserRole userRole = userRoleList.get(i);
if(userRole != null) {
this.grantedAuthArray[j++] = new GrantedAuthorityImpl(userRole.getRoleName().toUpperCase());
}
}
LOG.info("UserName: " + userName + " loaded successfully.");
return new org.acegisecurity.userdetails.User(userName, user.getUserPass(),
true, true, true, true, this.grantedAuthArray);
}
}
六. 在業務邏輯代碼中利用Java 5注釋實現安全控制
@Secured({"ROLE_USER"})
void sendSimpleMail(Long userId);
@Secured({"ROLE_ADMIN"})
void sendAttachmentMail() throws Exception;
其實就是在需要安全控制的方法前加上: @Secured({"角色名"}), 非常的簡單
七. 整個工作完成
Acegi框架完全是一種可插拔式的, 完全可以在原有的系統中加個一個配置文件, 和在每個方法前加上: @Secured({"角色名"}) 就可完成.
上面的 AcegiUserDeitailsService.java 中的有 UserDao, UserRoleDao, 我想一看就知道它們是干什麼的了, 這完全取決於個人的實現, 與Acegi無關, 它僅僅只要返回一個 return new org.acegisecurity.userdetails.User(userName, user.getUserPass(),
true, true, true, true, this.grantedAuthArray) 就可以了.