1. 程式人生 > 實用技巧 >拋磚引玉-使用Acegi實現多種使用者登入的一種方案

拋磚引玉-使用Acegi實現多種使用者登入的一種方案

為什麼80%的碼農都做不了架構師?>>> hot3.png

拋磚引玉-使用Acegi實現多種使用者登入的一種方案 摘要

Acegi提供了多種身份驗證方式(表單驗證,CAS等),但只允許一種使用者登入,而就個人瞭解,有一些系統是需要多種使用者登入的。比如企業的員工需要登入並使用系統,企業也允許客戶登入系統並使用有限的功能。以下嘗試剖析Acegi的表單驗證過程,並給出一種允許多種使用者登入的方案。本方案基本達到“能用”的目的,但不一定是最佳方案。希望這篇文章能起到拋磚引玉的作用,給各位朋友一點參考,也希望各位提出有益的建議。

Acegi的表單驗證方式簡要分析

一個使用Acegi的表單驗證的登入頁面通常需要在表單提交時request的j_username和j_password引數賦值,即使用者名稱和密碼,而表單則提交到Acegi設定到驗證地址。例如:

< form method ="post" id ="loginForm" action ="<c:urlvalue='/j_security_check'/>" >
< input type ="text" name ="j_username" id ="j_username" />

< input type ="password" name ="j_password" id ="j_password"
/>

< input type ="submit" name ="login" value ="Login" />
</ form >
伺服器的Servlet容器收到請求後會傳遞給Acegi的FilterToBeanProxy,這需要在web.xml中進行配置。例如:

< filter >
< filter-name > securityFilter </ 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 > securityFilter </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
FilterToBeanProxy基本上只起到呼叫轉發的作用。在它的doFilter方法中會找到型別為FilterChainProxy的bean,呼叫後者的doFilter方法,同時把request、response會chain引數都傳遞過去。程式碼如下:

public void doFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)
throws IOException,ServletException{
if ( ! initialized){
doInit();
}

delegate.doFilter(request,response,chain);
}

上面的程式碼中的delegate就是找到的型別FilterChainProxy的bean。FilterChainProxy的典型配置如下:

< bean id ="filterChainProxy" class ="org.acegisecurity.util.FilterChainProxy" >
< property name ="filterInvocationDefinitionSource" >
< value >
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,
</ value >
</ property >
</ bean >
對於上面的配置,引用一段Acegi聯機幫助中的說明來幫助理解:

Internally Acegi Security will use a PropertyEditor to convert the string presented in the above XML fragment into a FilterInvocationDefinitionSource object. What's important to note at this stage is that a series of filters will be run - in the order specified by the declaration - and each of those filters are actually the <bean id> of another bean inside the application context.

實際上,FilterChainProxy的doFilter方法會執行如下處理:
1.讀取配置,如果配置為空,則直接呼叫chain.doFilter,返回
2.如果配置不為空,則根據配置找到各個bean,放入Filter陣列中。如果配置中沒有配置任何bean,則直接呼叫chain.doFilter,返回
3.FilterChainProxy建立一個VirtualFilterChain物件,並將chain封裝為一個FilterInvocation物件,將它和Filter陣列一起傳遞給VirtualFilterChain的建構函式。VirtualFilterChain的建構函式初始化了一個指標currentPosition,指向Filter陣列的第一個元素additionalFilters[0]
4.FilterChainProxy呼叫VirtualFilterChain的doFilter方法,在該方法中將指標currentPosition前移,呼叫additionalFilters[0]的doFilter方法。注意這裡VirtualFilterChain把自身作為引數傳遞給additionalFilters[0]的doFilter方法,這樣additionalFilters[0]的doFilter方法最後會呼叫VirtualFilterChain的doFilter方法,這樣控制就又回到了VirtualFilterChain!於是VirtualFilterChain又將currentPosition前移,呼叫additionalFilters[1]的doFilter方法......
5.當additionalFilters中所有元素的doFilter都執行完畢,VirtualFilterChain執行fi.getChain().doFilter,而fi.getChain()的值就是FilterChainProxy的doFilter方法中的引數chain的值。這樣我們就理解了FilterChainProxy是怎樣讓呼叫兜了個圈,又傳遞出去的。

重新回到FilterChainProxy的配置,看到它呼叫了authenticationProcessingFilter這個Filter。讓我們看看它的配置:

< bean id ="authenticationProcessingFilter"
class
="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter" >
< property name ="authenticationManager" ref ="authenticationManager" />
< property name ="authenticationFailureUrl" value ="/login.jsp?error=true" />
< property name ="defaultTargetUrl" value ="/" />
< property name ="filterProcessesUrl" value ="/j_security_check" />
< property name ="rememberMeServices" ref ="rememberMeServices" />
</ bean >
authenticationProcessingFilter的其中一個作用就是獲取客戶端提交的使用者名稱和密碼,將它們封裝為一個Token,傳遞給authenticationManager的authenticate方法,由後者負責驗證。

看看authenticationManager的配置:

< bean id ="authenticationManager" class ="org.acegisecurity.providers.ProviderManager" >
< property name ="providers" >
< list >
< ref local ="daoAuthenticationProvider" />
< ref local ="anonymousAuthenticationProvider" />
< ref local ="rememberMeAuthenticationProvider" />
</ list >
</ property >
</ bean >
authenticationManager依次呼叫每個provider的authenticate方法。如果某個provider驗證成功則返回;如果所有的驗證都不成功,則丟擲異常。

讓我們看看daoAuthenticationProvider的配置:

< bean id ="daoAuthenticationProvider" class ="org.acegisecurity.providers.dao.DaoAuthenticationProvider" >
< property name ="userDetailsService" ref ="userDao" />
< property name ="passwordEncoder" ref ="passwordEncoder" />
</ bean >
daoAuthenticationProvider在authenticate方法中呼叫retrieveUser方法取得使用者資訊,執行基本的驗證,然後呼叫additionalAuthenticationChecks執行附加的驗證(比如驗證密碼是否正確)。在retrieveUser方法中呼叫userDetailsService的loadUserByUsername方法取得使用者資訊,而userDetailsService是一個名為userDao的bean。讓我們看看userDao的配置:

< bean id ="userDao" class ="cn.net.cogent.summer.extension.appfuse.dao.hibernate.EmployeeDaoHibernate" >
< property name ="sessionFactory" ref ="sessionFactory" />
</ bean >
userDao實現了Acegi的UserDetailsService介面,該介面只有loadUserByUsername方法。loadUserByUsername方法根據傳入的username取得相應的Employee物件(Employee實現了UserDetails介面),該物件返回給daoAuthenticationProvider,由它和authenticationManager聯合完成驗證的任務。

以上對Acegi對錶單驗證過程進行了簡單對分析,限於篇幅,無法深入分析原始碼。但從配置可以畫出驗證過程的物件圖如下:



從圖中可以看出,儘管Acegi呼叫了多個Filter來完成驗證過程,關鍵點卻在三處:
1.在客戶端輸入身份驗證資訊,包括使用者名稱和密碼
2.AuthenticationProcessingFilter取出使用者名稱和密碼,封裝為一個Token往後傳遞
3.DaoAuthenticationProvider從系統中找出使用者資料,並和ProviderManager一起執行驗證

實現多種使用者登入

很明顯,要讓系統識別不同種類的使用者,必須設立一個使用者型別標誌。問題就轉化為:
1.使用者在客戶端輸入身份資訊時系統就必須設立相應的標誌
2.該標誌如何傳遞到DaoAuthenticationProvider
3.DaoAuthenticationProvider如何識別該標誌,並從相應型別的使用者中找到指定使用者

我不打算改動Acegi的原始碼,只打算擴展出我需要的功能。

首先在登入頁面中加入使用者型別標誌j_userkind。在登入頁面中加入如下程式碼:

< input type ="hidden" name ="j_userkind" id ="j_userkind" value ="0" >
其中0程式碼員工,1程式碼客戶。可以考慮在登入頁面中增加一個選項,如果使用者要以員工身份登入,則把j_userkind置為0;如果使用者要以客戶身份登入,則把j_userkind置為1。也可以提供兩個登入頁面,其中一個員工專用(j_userkind被強制置為0),另一個客戶專用(j_userkind被強制置為1)

系統如何根據收到的使用者型別標誌去讀取指定的使用者呢?如果在程式碼中寫死(比如當用戶型別標誌=0時,讀取員工;當用戶型別標誌=1時,讀取客戶)非常不好,還是通過配置來確定比較靈活。首先編寫UserKindComparisonAware介面:

package cn.net.cogent.summer.extension.acegisecurity.providers;

public
interface UserKindComparisonAware{

public void setExpectedUserKind(StringexpectedUserKind);
public void setCurrentUserKind(StringcurrentUserKind);

}

該介面說明實現類需要實現兩個方法,setExpectedUserKind用於接受一個期望的使用者型別標誌(通常該標誌通過配置來設定),setCurrentUserKind用於接受當前登入使用者的使用者型別標誌(系統在執行時捕獲,並傳遞給實現類)

編寫MKUDaoAuthenticationProvider類:

package cn.net.cogent.summer.extension.acegisecurity.providers.dao;

import cn.net.cogent.summer.extension.acegisecurity.BadUserKindException;
import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.acegisecurity.AuthenticationException;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;

import cn.net.cogent.summer.util.LoggerUtil;

public class MKUDaoAuthenticationProvider extends DaoAuthenticationProvider implements
UserKindComparisonAware{

private StringexpectedUserKind;
private StringcurrentUserKind;

public StringgetExpectedUserKind(){
return expectedUserKind;
}
public void setExpectedUserKind(StringexpectedUserKind){
this .expectedUserKind = expectedUserKind;
}

public StringgetCurrentUserKind(){
return currentUserKind;
}
public void setCurrentUserKind(StringcurrentUserKind){
this .currentUserKind = currentUserKind;
}

protected void additionalAuthenticationChecks(UserDetailsuserDetails,
UsernamePasswordAuthenticationTokenauthentication)
throws AuthenticationException{
LoggerUtil.getLogger().debug(
" expectedUserKind=' " + expectedUserKind + " ',currentUserKind=' " + currentUserKind + " ' " );
if (currentUserKind.equals(expectedUserKind))
super .additionalAuthenticationChecks(userDetails,authentication);
else
throw new BadUserKindException(
" FlagUserKinddoesnotmatch " );
}
}

該類繼承自DaoAuthenticationProvider並實現UserKindComparisonAware介面,在additionalAuthenticationChecks方法中判斷當前登入使用者的使用者型別標誌與期望的使用者型別標誌是否一致,如果一致則執行父類的additionalAuthenticationChecks,完成驗證;否則丟擲一個BadUserKindException異常,表明驗證失敗。BadUserKindException繼承自org.acegisecurity.AuthenticationException,具體的程式碼略

在applicationContext.xml中刪除daoAuthenticationProvider相關的配置,增加如下配置:

< bean id ="customerDaoAuthenticationProvider" class ="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider" >
< property name ="userDetailsService" ref ="customerDao" />
< property name ="passwordEncoder" ref ="passwordEncoder" />
< property name ="expectedUserKind" value ="1" />
</ bean >

< bean id ="userDaoAuthenticationProvider" class ="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider" >
< property name ="userDetailsService" ref ="userDao" />
< property name ="passwordEncoder" ref ="passwordEncoder" />
< property name ="expectedUserKind" value ="0" />
</ bean >

可以看出customerDaoAuthenticationProvider僅用於驗證客戶(其expectedUserKind被指定為1),而userDaoAuthenticationProvider僅用於驗證員工(其expectedUserKind被指定為0)。customerDao的配置如下:

< bean id ="customerDao" class ="cn.net.cogent.summer.extension.appfuse.dao.hibernate.CustomerDaoHibernate" >
< property name ="sessionFactory" ref ="sessionFactory" />
</ bean >
CustomerDaoHibernate的程式碼如下:

package cn.net.cogent.summer.extension.appfuse.dao.hibernate;

import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;

import cn.net.cogent.summer.model.Customer;
import org.appfuse.dao.hibernate.GenericDaoHibernate;
import org.springframework.dao.DataAccessException;

import java.util.List;

public class CustomerDaoHibernate extends GenericDaoHibernate < Customer,Long > implements UserDetailsService{

public CustomerDaoHibernate(){
super (Customer. class );
}

public UserDetailsloadUserByUsername(Stringusername)
throws UsernameNotFoundException,DataAccessException{
List
< Customer > users = getHibernateTemplate().find( " fromCustomerwhereusername=? " ,username);
if (users == null || users.isEmpty()){
throw new UsernameNotFoundException( " Customer' " + username + " 'notfound " );
}
else {
return (UserDetails)users.get( 0 );
}
}
}


可以看出CustomerDaoHibernate是取得一個Customer物件(實現了UserDetails介面),而不是Employee。

修改authenticationManager的配置如下:

< bean id ="authenticationManager" class ="org.acegisecurity.providers.ProviderManager" >
< property name ="providers" >
< list >
< ref local ="customerDaoAuthenticationProvider" />
< ref local ="userDaoAuthenticationProvider" />
< ref local ="anonymousAuthenticationProvider" />
< ref local ="rememberMeAuthenticationProvider" />
</ list >
</ property >
</ bean >
在哪裡捕獲當前登入使用者的使用者型別標誌,並傳遞給MKUDaoAuthenticationProvider呢?我決定增加一個名為PreAuthenticationProcessingFilter的Filter,放在AuthenticationProcessingFilter之前,程式碼如下:

package cn.net.cogent.summer.extension.acegisecurity.ui.webapp;

import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.io.IOException;

import java.util.Iterator;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class PreAuthenticationProcessingFilter implements Filter,ApplicationContextAware{

public static final StringACEGI_SECURITY_FORM_USERKIND = " j_userkind " ;

private FilterConfigfilterConfig;
private boolean initialized = false ;
private MaptargetBeans;
private StringtargetClass;
private ApplicationContextapplicationContext;

public StringgetTargetClass(){
return targetClass;
}
public void setTargetClass(StringtargetClass){
this .targetClass = targetClass;
}

public void setApplicationContext(ApplicationContextapplicationContext){
this .applicationContext = applicationContext;
}

public void destroy(){
}

public void init(FilterConfigfilterConfig) throws ServletException{
this .filterConfig = filterConfig;
}

public void doFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain) throws IOException,
ServletException{
if ( ! (request instanceof HttpServletRequest)){
throw new ServletException( " CanonlyprocessHttpServletRequest " );
}

if ( ! initialized){
doInit();
}

StringuserKind
= obtainUserKind((HttpServletRequest)request);
for (Iteratorit = targetBeans.values().iterator();it.hasNext();){
UserKindComparisonAwarecomparison
= (UserKindComparisonAware)it.next();
comparison.setCurrentUserKind(userKind);
}

chain.doFilter(request,response);
}

private synchronized void doInit() throws ServletException{
if ((targetClass == null ) || "" .equals(targetClass)){
throw new ServletException( " targetClassmustbespecified " );
}

Class_targetClass;

try {
_targetClass
= Thread.currentThread().getContextClassLoader().loadClass(targetClass);
}
catch (ClassNotFoundExceptionex){
throw new ServletException( " Classoftype " + targetClass + " notfoundinclassloader " );
}

targetBeans
= BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,_targetClass, true , true );

if (targetBeans.size() == 0 ){
throw new ServletException( " Beancontextmustcontainatleastonebeanoftype " + targetClass);
}

for (Iteratorit = targetBeans.entrySet().iterator();it.hasNext();){
Map.Entryentry
= (Map.Entry)it.next();
if ( ! (entry.getValue() instanceof UserKindComparisonAware)){
throw new ServletException( " Bean' " + entry.getKey() +
" 'doesnotimplementcn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware " );
}
}

// Setinitializedtotrueattheendofthesynchronizedmethod,so
// thatinvocationsofdoFilter()beforethismethodhascompletedwillnot
// causeNullPointerException
initialized = true ;
}

protected StringobtainUserKind(HttpServletRequestrequest){
return request.getParameter(ACEGI_SECURITY_FORM_USERKIND);
}
}

PreAuthenticationProcessingFilter需要在初始化引數中指定targetClass,該引數的值是一個類,該類實現了UserKindComparisonAware介面。PreAuthenticationProcessingFilter找到容器中所有該類的例項,並把捕獲的當前登入使用者的使用者型別標誌賦值給它們。PreAuthenticationProcessingFilter的配置如下:

< bean id ="preAuthenticationProcessingFilter"
class
="cn.net.cogent.summer.extension.acegisecurity.ui.webapp.PreAuthenticationProcessingFilter" >
< property name ="targetClass"
value
="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider" />
</ bean >
還需要把preAuthenticationProcessingFilter加入到filterChainProxy的配置中:

< bean id ="filterChainProxy" class ="org.acegisecurity.util.FilterChainProxy" >
< property name ="filterInvocationDefinitionSource" >
< value >
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=,preAuthenticationProcessingFilter,authenticationProcessingFilter,
</ value >
</ property >
</ bean >

注意把它放在authenticationProcessingFilter的前面

至此我們初步實現了使用Acegi實現多種使用者登入 posted on 2007-09-18 22:19 雨奏閱讀(2341) 評論(8) 編輯 收藏 146275.html?webview=1
FeedBack: # re: 拋磚引玉-使用Acegi實現多種使用者登入的一種方案2007-09-19 12:02 千里冰封 就為了一個登入,這樣配置有點複雜了吧:) 回覆 更多評論
# re: 拋磚引玉-使用Acegi實現多種使用者登入的一種方案2007-09-19 13:25 雨奏 @千里冰封
請問怎樣配置會更好呢?能簡要說說你的辦法嗎? 回覆 更多評論
# re: 拋磚引玉-使用Acegi實現多種使用者登入的一種方案2007-09-19 16:39 西濱 實現多種使用者登入倒不難,難的是有了多種使用者(像本文的員工和客戶)之後,怎麼處理不同使用者的角色、許可權? 回覆 更多評論
# re: 拋磚引玉-使用Acegi實現多種使用者登入的一種方案2007-09-19 21:39 雨奏 @西濱
我倒是覺得處理角色和許可權不難。原本系統中員工的角色、許可權是如何授予的,客戶的角色、許可權可以用類似的方法處理 回覆 更多評論
# re: 拋磚引玉-使用Acegi實現多種使用者登入的一種方案2007-09-20 11:30 Java初心 acegi的dao驗證本來就支援USERROLE的吧

<bean id="jdbcDaoImpl"
class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="usersByUsernameQuery">
<value>
SELECT USERID, PASSWORD,1 FROM T_USER_ROLE
WHERE USERID=?
</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>
SELECT USERID,USERROLE FROM T_USER_ROLE WHERE
USERID=?
</value>
</property>
</bean> 回覆 更多評論

轉載於:https://my.oschina.net/baobao/blog/15720