JCaptcha用來做用戶登錄時期的驗證碼的,但是今天將開放的應用系統部署到生產環境的時候,遇到了問題,總是提示驗證碼不對。后台報出來下面的錯誤:
1 com.octo.captcha.service.CaptchaServiceException: Invalid ID, could not validate unexisting or already validated captcha 2 at com.octo.captcha.service.AbstractCaptchaService.validateResponseForID(AbstractCaptchaService.java:146) 3 at com.octo.captcha.service.AbstractManageableCaptchaService.validateResponseForID(AbstractManageableCaptchaService.java:367) 4 at com.tk.cms.core.shiro.JCaptcha.validateResponse(JCaptcha.java:19) 5 at com.tk.cms.core.shiro.JCaptchaValidateFilter.isAccessAllowed(JCaptchaValidateFilter.java:44) 6 at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162) 7 at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203) 8 at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178) 9 at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131) 10 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) 11 at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66) 12 at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449) 13 at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365) 14 at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90) 15 at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83) 16 at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383) 17 at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362) 18 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) 19 at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:343) 20 at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:260) 21 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) 22 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) 23 at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88) 24 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:106) 25 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) 26 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) 27 at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220) 28 at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) 29 at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505) 30 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170) 31 at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) 32 at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:956) 33 at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) 34 at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423) 35 at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079) 36 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:625) 37 at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316) 38 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 39 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) 40 at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) 41 at java.lang.Thread.run(Thread.java:745)
網上也有很多種方案,其中就說到過,如下代碼處有問題(紅色部分代碼執行的不是時候):
1 /** 2 * Method to validate a response to the challenge corresponding to the given ticket and remove the coresponding 3 * captcha from the store. 4 * 5 * @param ID the ticket provided by the buildCaptchaAndGetID method 6 * @return true if the response is correct, false otherwise. 7 * @throws CaptchaServiceException if the ticket is invalid 8 */ 9 public Boolean validateResponseForID(String ID, Object response) 10 throws CaptchaServiceException { 11 if (!store.hasCaptcha(ID)) { 12 throw new CaptchaServiceException("Invalid ID, could not validate unexisting or already validated captcha"); 13 } else { 14 Boolean valid = store.getCaptcha(ID).validateResponse(response); 15 store.removeCaptcha(ID); 16 return valid; 17 } 18 }
其實,我也仔細debug過,這個驗證碼錯誤的問題,其實是在store這個FastHashMap中通過sessionId為key去找是否存在這么一個jcaptcha實例,若沒有就報exception了,最后就是驗證碼錯誤。 其實,這不是我應用中的問題。
針對這個問題,我一開始,懷疑是自己的代碼寫的出了問題,總在分析代碼的流程,但是疑點是,我直接訪問tomcat所在的機器,沒有出現驗證碼的錯誤。但是一旦上到nginx的負載均衡環境,就遇到這個問題。想想,為何session不對???看看應用中的日志,sessionID和瀏覽器中cookie中的sessionId,總是不一樣。。。這個就是問題的表象,根源在什么地方呢???
后來仔細看了看我們服務器的nginx的反向代理配置,發現upstream部分,沒有配置負載均衡的策略,什么都沒有配置呀。。。。我倒,什么都沒有指定,那就是default的roundrobin啊,輪詢啊。。。。我的神,這一個應用服務會有多少次http請求到達后端啊,每次輪詢,那session對於后端的服務來說,豈不是沒有地方hold了。。。不行。這個就是問題的根源。。。
我們的session沒有專門的共享方案,所以,為了改動最小化,最好不改代碼的情況下,我選擇將upstream部分添加ip_hash策略,這樣子能保證同一個IP請求的所有http都鎖定在后端的一個服務上,這樣子就不存在session丟失的問題了。
1 upstream cms { 2 server 10.130.14.51:8080; 3 server 10.130.14.53:8080; 4 ip_hash; 5 }
加上了上面的紅色部分,問題解決!
思考:
1. 負載均衡環境下,session的管理是個問題,忽視這個問題,會造成登錄都不可能完成。 遇到登錄或者類似我這里驗證碼總是不對的情況,可以想想,是不是session的管理不到位!
2. 常見的session管理,比較靠譜的可控的方式有類似我這里的方案,基於ip_hash的策略,還有,就是專門的開發接口來管理session,不用tomcat容器的那一套。比如將session放在mysql里面,或者redis等中間件里面。