<p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px;">先說明錯誤原因:用spring安全攔截器進行驗證碼的驗證的時候拋出異常。</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px;">throw new RuntimeException("captcha validation failed due to exception", cse);</p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px;">前台提交數據后跳轉到如下方法:</p>
-
package com.davidstudio.gbp.core.security.jcaptcha;
-
-
import org.acegisecurity.captcha.CaptchaServiceProxy;
-
-
import org.apache.log4j.Logger;
-
-
import com.octo.captcha.service.CaptchaService;
-
import com.octo.captcha.service.CaptchaServiceException;
-
-
/**
-
* 調用CaptchaService類,完jcaptcha的驗證過程
-
*
-
*
-
*
-
*
-
*/
-
public
class JCaptchaServiceProxyImpl implements CaptchaServiceProxy {
-
-
/**
-
* Logger for this class
-
*/
-
private
static
final Logger logger = Logger.getLogger(JCaptchaServiceProxyImpl.class);
-
-
private CaptchaService jcaptchaService;
-
-
public boolean validateReponseForId(String id, Object response) {
-
if (logger.isDebugEnabled()) {
-
logger.debug(
"validating captcha response");
-
}
-
-
try {
-
boolean isHuman =
false;
-
-
isHuman = jcaptchaService.validateResponseForID(id, response).booleanValue();
-
-
if (isHuman) {
-
if (logger.isDebugEnabled()) {
-
logger.debug(
"captcha passed");
-
}
-
}
else {
-
if (logger.isDebugEnabled()) {
-
logger.debug(
"captcha failed");
-
}
-
}
-
return isHuman;
-
-
}
catch (CaptchaServiceException cse) {
-
// fixes known bug in JCaptcha
-
logger.warn(
"captcha validation failed due to exception", cse);
-
throw
new RuntimeException(
"captcha validation failed due to exception", cse);
-
}
-
}
-
-
public void setJcaptchaService(CaptchaService jcaptchaService) {
-
this.jcaptchaService = jcaptchaService;
-
}
-
}
設置斷點debug改語句不能順利執行
jcaptchaService.validateResponseForID(id, response).booleanValue();
查了網上的資料,這個方法的作用是: 根據HttpSession的 sessionId進行驗證碼的驗證,原理是這樣的,頁面生成的驗證碼是通過Spring中的配置生成的,查了一下配置:
-
<bean id="security.filter.manager" class="org.acegisecurity.util.FilterChainProxy">
-
<property name="filterInvocationDefinitionSource">
-
<value>
-
PATTERN_TYPE_APACHE_ANT
-
/**=security.filter.channel,security.filter.sessionIntegration,security.filter.logout,security.filter.thsso,security.filter.jcaptcha,security.filter.jcaptchachannel,security.filter.formAuth,security.filter.requestWrap,security.filter.exceptionTranslation,security.filter.filterInvocation
-
</value>
-
</property>
-
</bean>
這是一個過濾器鏈,其中登錄的時候會進行如下過濾操作,
security.filter.channel,security.filter.sessionIntegration,security.filter.logout,security.filter.thsso,security.filter.jcaptcha,security.filter.jcaptchachannel,security.filter.formAuth,security.filter.requestWrap,security.filter.exceptionTranslation,security.filter.filterInvocation
一般配置的順序不能變,因為這是這些配置定義了用戶登錄的一套認證機制。
看了一下命名還算規范,其中涉及到驗證碼的過濾:security.filter.jcaptcha
查了一下這個驗證碼的引用配置:
-
<!-- jcaptacha過濾器 -->
-
<bean id="security.filter.jcaptcha"
-
class=
"org.acegisecurity.captcha.CaptchaValidationProcessingFilter">
-
<property name="captchaService" ref="security.captcha.serviceproxy" />
-
<property name="captchaValidationParameter" value="j_captcha_response" />
-
</bean>
-
<bean id="security.captcha.serviceproxy"
-
class=
"com.davidstudio.gbp.core.security.jcaptcha.JCaptchaServiceProxyImpl">
-
<property name="jcaptchaService" ref="security.captcha.service" />
-
</bean>
-
<bean id="security.captcha.service"
-
class=
"com.octo.captcha.service.image.DefaultManageableImageCaptchaService">
-
<constructor-arg type="com.octo.captcha.service.captchastore.CaptchaStore" index="0">
-
<bean class="com.octo.captcha.service.captchastore.FastHashMapCaptchaStore" />
-
</constructor-arg>
-
<constructor-arg type="com.octo.captcha.engine.CaptchaEngine" index="1">
-
<bean class="com.davidstudio.gbp.core.security.jcaptcha.CaptchaEngine" />
-
</constructor-arg>
-
<constructor-arg index="2">
-
<value>180
</value>
-
</constructor-arg>
-
<constructor-arg index="3">
-
<value>100000
</value>
-
</constructor-arg>
-
<constructor-arg index="4">
-
<value>75000
</value>
-
</constructor-arg>
-
</bean>
通過bean配置反復引用。
剛開始以為SecurityContext沒有創建,查了一下配置也創建了:
-
<!-- session整合過濾器。自動將用戶身份信息存放在session里。 -->
-
<bean id="security.filter.sessionIntegration"
-
class=
"org.acegisecurity.context.HttpSessionContextIntegrationFilter">
-
<property name="context" value="org.acegisecurity.captcha.CaptchaSecurityContextImpl" />
-
</bean>
仔細看了一下這個方法的作用:
jcaptchaService.validateResponseForID(id, response).booleanValue();
id就是httpSession的Id,response是從頁面獲得的輸入的驗證碼,當調用這個方法的時候,根據httpSession的id找到相應的驗證碼,如果有sessionId並且sessionId對應的驗證碼和輸入的驗證碼(這里就是response)一致的時候返回true,也就是用戶通過了驗證。
有一個疑問,驗證碼是怎么生成的?又怎么和httpSession進行綁定的?其實這套理論是可行的,當用戶第一次訪問頁面的時候會生成一個sessionId,頁面生成有驗證碼,關於驗證碼的生成,下面會進行介紹。就是畫一個圖片以留的方式顯示到頁面而已。用戶訪問的時候有一個對應的驗證碼和sessionId相對應。
如果驗證碼不清楚,點擊換一張,因為瀏覽器沒有關閉,sessionId依然是那個sessionId,只需要更新生成的驗證碼的值即可,這樣就做到了一個sessionId和一個驗證碼進行綁定了,這個過程在生成驗證碼的過程中就發生了。
如果用戶再次提交登錄信息,其中的sessionId沒有變,驗證碼是最新生成的驗證碼並且和sessionId進行了綁定,這樣就可以調用:
jcaptchaService.validateResponseForID(id, response).booleanValue(); 這個條件進行驗證碼的驗證了,當然了驗證碼驗證前面還可以有很多過濾器認證,比如說對用戶名和密碼的驗證等等。形成一套的鏈式認證!
然而還有一個疑惑,這個sessionId是怎么和驗證碼進行綁定的呢?又是怎樣進行存儲的呢?
我們看一下內存:
調用這段代碼的時候內存中有sessionId和response驗證碼的值:
下面是驗證碼生成的線程中內存的狀態:
由內存的狀態可以看出和配置文件是一致的,首先調用了com.davidstudio.gbp.core.security.jcaptcha.JCaptchaServiceProxyImpl
這個代理實現,這個代理實現類 又去調用com.octo.captcha.service.image.DefaultManageableImageCaptchaService
這個類才是生成驗證碼的類:查下spring這個類的源碼如下:
-
23
public
class DefaultManageableImageCaptchaService extends AbstractManageableImageCaptchaService
-
24
implements
ImageCaptchaService {
-
25
/**
-
26 * Construct a new ImageCaptchaService with a {@link FastHashMapCaptchaStore} and a {@link DefaultGimpyEngine}
-
27 * minGuarantedStorageDelayInSeconds = 180s
-
28 * maxCaptchaStoreSize = 100000
-
29 * captchaStoreLoadBeforeGarbageCollection=75000
-
30 */
-
31
public DefaultManageableImageCaptchaService() {
-
32
super(
new FastHashMapCaptchaStore(),
new DefaultGimpyEngine(),
180,
-
33
100000,
75000);
-
34 }
傳入的參數都有相應的說明,其中這個類繼承了
AbstractManageableImageCaptchaService
繼續深入到這個類中看個究竟:
這個類中果然有我們想要的方法:
-
127
/**
-
128 * Method to validate a response to the challenge corresponding to the given ticket and remove the coresponding
-
129 * captcha from the store.
-
130 *
-
131 * @param ID the ticket provided by the buildCaptchaAndGetID method
-
132 * @return true if the response is correct, false otherwise.
-
133 * @throws CaptchaServiceException if the ticket is invalid
-
134 */
-
135
public Boolean validateResponseForID(String ID, Object response)
-
136
throws CaptchaServiceException {
-
137
if (!store.hasCaptcha(ID)) {
-
138
throw
new CaptchaServiceException(
"Invalid ID, could not validate unexisting or already validated captcha");
-
139 }
else {
-
140 Boolean valid = store.getCaptcha(ID).validateResponse(response);
-
141 store.removeCaptcha(ID);
-
142
return valid;
-
143 }
-
144 }
這個就是判斷有沒有驗證碼,如果store中沒有相應的sessionId那么就拋出異常:
throw new CaptchaServiceException("Invalid ID, could not validate unexisting or already validated captcha");
根本就沒有這個sessionId,如果有這個sessionId就走了另一個邏輯:
Boolean valid = store.getCaptcha(ID).validateResponse(response);
相應的通過store.getCaptcha(ID)通過這個ID獲得和這個sessionId匹配的驗證碼,再調用vilidateResponse方法進行驗證,如果和輸入的驗證碼相同就驗證通過了。
驗證通過后就把這個sessionId刪除了,如果你再次登錄,輸入驗證碼的時候是同一個邏輯,之所以刪除了這個ID我想是有好處的:
原因如下,如果不進行刪除,隨着的登錄訪問用戶的過多,hashMap中的值會越來越多,這樣以后再進行驗證的時候速度和效率都會受到印象,如果刪除了這個sessionId,這樣這個store中的hashMap只是存儲了當前正在准備登錄的sessionId和相應的驗證碼!這樣效率就大大提高了,如果有10萬個人同時登錄,都不是問題!
通過這個方法的調用我們就知道了sessionId是怎么和驗證碼綁定存儲在hashMap中的!讓我們進入源碼驗證一下:
-
18
/**
-
19 * Simple store based on a HashMap
-
20 */
-
21
public
class MapCaptchaStore implements CaptchaStore {
-
22
-
23 Map store;
-
24
-
25
public MapCaptchaStore() {
-
26
this.store =
new HashMap();
-
27 }
-
28
-
29
/**
-
30 * Check if a captcha is stored for this id
-
31 *
-
32 * @return true if a captcha for this id is stored, false otherwise
-
33 */
-
34
public boolean hasCaptcha(String id) {
-
35
return store.containsKey(id);
-
36 }
-
37
-
38
/**
-
39 * Store the captcha with the provided id as key. The key is assumed to be unique, so if the same key is used twice
-
40 * to store a captcha, the store will return an exception
-
41 *
-
42 * @param id the key
-
43 * @param captcha the captcha
-
44 *
-
45 * @throws CaptchaServiceException if the captcha already exists, or if an error occurs during storing routine.
-
46 */
-
47
public void storeCaptcha(String id, Captcha captcha) throws CaptchaServiceException {
-
48
// if (store.get(id) != null) {
-
49
// throw new CaptchaServiceException("a captcha with this id already exist. This error must " +
-
50
// "not occurs, this is an implementation pb!");
-
51
// }
-
52 store.put(id,
new CaptchaAndLocale(captcha));
-
53 }
上面就是CaptchaStore接口的實現類MapCaptchaStore,其中定義了一個hashMap,通過storeCaptcha(String id,Captcha captcha)方法來存儲sessionId和captcha的鍵值對,這是進入登錄頁面生成的時候調用的方法,當進行驗證的時候就需要hasCaptcha(String ID)方法和
-
69
/**
-
70 * Retrieve the captcha for this key from the store.
-
71 *
-
72 * @return the captcha for this id
-
73 *
-
74 * @throws CaptchaServiceException if a captcha for this key is not found or if an error occurs during retrieving
-
75 * routine.
-
76 */
-
77
public Captcha getCaptcha(String id) throws CaptchaServiceException {
-
78 Object captchaAndLocale = store.get(id);
-
79
return captchaAndLocale!=
null?((CaptchaAndLocale) captchaAndLocale).getCaptcha():
null;
-
80 }
但是我們是調用了
MapCaptchaStore 的子類<pre name="code" class="java" style="white-space: pre-wrap; word-wrap: break-word;">FastHashMapCaptchaStore來存儲信息的:同樣看<pre name="code" class="java" style="white-space: pre-wrap; word-wrap: break-word;">FastHashMapCaptchaStore這個類:
-
<pre name=
"code"
class=
"java" style=
"white-space: pre-wrap; word-wrap: break-word;">
17
public
class FastHashMapCaptchaStore extends MapCaptchaStore {
-
18
public FastHashMapCaptchaStore() {
-
19
this.store =
new FastHashMap();
-
20 }
-
21 }
這就是這個類的全部了,再看一下FastHashMap類:
-
<pre name=
"code"
class=
"java" style=
"white-space: pre-wrap; word-wrap: break-word;">
public
class FastHashMap extends HashMap {
-
67
-
68
/**
-
69 * The underlying map we are managing.
-
70 */
-
71
protected HashMap map =
null;
-
72
-
73
/**
-
74 * Are we currently operating in "fast" mode?
-
75 */
-
76
protected
boolean fast =
false;
-
77
-
78
// Constructors
-
79
// ----------------------------------------------------------------------
-
80
-
81
/**
-
82 * Construct an empty map.
-
83 */
-
84
public FastHashMap() {
-
85
super();
-
86
this.map =
new HashMap();
-
87 }
-
88
這個類是HashMap的一個擴展,里面有兩種方式操作,一種是快速的不同步,一種是同步的操作!
顯然<pre name="code" class="java" style="white-space: pre-wrap; word-wrap: break-word;">FastHashMapCaptchaStore就是一個HashMap
驗證碼的實現在這個類中:
-
<pre name=
"code"
class=
"java" style=
"white-space: pre-wrap; word-wrap: break-word;">
18 * Base implementation of the ImageCaptchaService.
-
19 *
-
20 *
@author <a href=
"mailto:mag@jcaptcha.net">Marc-Antoine Garrigue</a>
-
21 *
@version
1.0
-
22 */
-
23
public
abstract
class AbstractManageableImageCaptchaService extends AbstractManageableCaptchaService
-
24
implements
ImageCaptchaService {
-
25
-
26
protected AbstractManageableImageCaptchaService(CaptchaStore captchaStore,
-
27 com.octo.captcha.engine.CaptchaEngine captchaEngine,
-
28
int minGuarantedStorageDelayInSeconds,
-
29
int maxCaptchaStoreSize,
-
30
int captchaStoreLoadBeforeGarbageCollection) {
-
31
super(captchaStore, captchaEngine,
-
32 minGuarantedStorageDelayInSeconds, maxCaptchaStoreSize,
-
33 captchaStoreLoadBeforeGarbageCollection);
-
34 }
-
73
protected Object getChallengeClone(Captcha captcha) {
-
74 BufferedImage challenge = (BufferedImage) captcha.getChallenge();
-
75 BufferedImage clone =
new BufferedImage(challenge.getWidth(), challenge.getHeight(), challenge.getType());
-
76
-
77 clone.getGraphics().drawImage(challenge,
0,
0, clone.getWidth(), clone.getHeight(),
null);
-
78 clone.getGraphics().dispose();
-
79
-
80
-
81
return clone;
-
82 }
在這個類中,只是定義了一種,Captcha也是一種接口。
可以到內存中看一看有木有那個hashMap
<span style="margin: 0px; padding: 0px; white-space: pre;"><img src="http://img.my.csdn.net/uploads/201211/23/1353676134_4969.png" alt="" style="border: none; max-width: 100%;" /> </span>
</pre><pre name="code" class="java" style="white-space: pre-wrap; word-wrap: break-word;">內存中清楚顯示了hashTable中的key和value,這樣就證明驗證碼生成成功。
但是為什么每次驗證都是報錯呢?
后來無奈看了看發送到 sessionId在hashMap中是否有,結果是不一樣,就是再hashMap中沒有,為什么?不是每一次在驗證碼生成的時候都把sessionId放進去了嗎?
為什么會不一樣呢?原因其實很簡單,就是當點擊登陸的時候服務器又給分配了一個sessionId,這樣就和以前的sessionId不一樣了,在hashMap中就找不到對應的驗證碼了。
原則上講服務器在第一次訪問的時候會給用戶分配一個不重復的sessionId,如果服務器的session不超時就不會再給用戶分配sessionId了,減少給服務器的壓力,也帶來了友好的體驗。但是我的兩次sessiId為什么不一樣呢? 后來通過fiddler2這個軟件(這個軟件好強大可以獲得發送到form表單的內容,甚至可以修改),可以看到本地存儲的cookie,但是cookie是空的,就是nodata,汗啊,難怪每次都分配不同的sessionId,服務器怎么判斷每次提交過去的是同一個用戶呢?通過sessionId,服務器會在客戶端把sessionId寫在Cookie中,這樣用戶再次提交請求的時候,服務器如果在內存中找到用戶cookie中的sessionId而且沒有超時,就不再重新分配sessionId,我看了下IE瀏覽器,cookie被禁止了,難怪每次都是一個新的sessionId,驗證碼就無法驗證。就報錯了。
學習中應該多去看源碼,分析源碼設計理念。最好的參考就是源碼了,要養成多看源碼的習慣,即使有的看不懂。我暈寫的太長了。
</pre><pre style="white-space: pre-wrap; word-wrap: break-word;">