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等中间件里面。