JCaptcha用来做用户登录时期的验证码的,但是今天将开放的应用系统部署到生产环境的时候,遇到了问题,总是提示验证码不对。后台报出来下面的错误:
com.octo.captcha.service.CaptchaServiceException: Invalid ID, could not validate unexisting or already validated captcha
at com.octo.captcha.service.AbstractCaptchaService.validateResponseForID(AbstractCaptchaService.java:)
at com.octo.captcha.service.AbstractManageableCaptchaService.validateResponseForID(AbstractManageableCaptchaService.java:)
at com.tk.cms.core.shiro.JCaptcha.validateResponse(JCaptcha.java:)
at com.tk.cms.core.shiro.JCaptchaValidateFilter.isAccessAllowed(JCaptchaValidateFilter.java:)
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:)
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:)
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:)
at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:)
at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:)
at org.apache.shiro.web.servlet.AbstractShiroFilter$.call(AbstractShiroFilter.java:)
at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:)
at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:)
at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:)
at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:)
at java.lang.Thread.run(Thread.java:)
网上也有很多种方案,其中就说到过,如下代码处有问题(红色部分代码执行的不是时候):
/**
* Method to validate a response to the challenge corresponding to the given ticket and remove the coresponding
* captcha from the store.
*
* @param ID the ticket provided by the buildCaptchaAndGetID method
* @return true if the response is correct, false otherwise.
* @throws CaptchaServiceException if the ticket is invalid
*/
public Boolean validateResponseForID(String ID, Object response)
throws CaptchaServiceException {
if (!store.hasCaptcha(ID)) {
throw new CaptchaServiceException("Invalid ID, could not validate unexisting or already validated captcha");
} else {
Boolean valid = store.getCaptcha(ID).validateResponse(response);
store.removeCaptcha(ID);
return valid;
}
}
其实,我也仔细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丢失的问题了。
upstream cms {
server 10.130.14.51:;
server 10.130.14.53:;
ip_hash;
}
加上了上面的红色部分,问题解决!
思考:
1. 负载均衡环境下,session的管理是个问题,忽视这个问题,会造成登录都不可能完成。 遇到登录或者类似我这里验证码总是不对的情况,可以想想,是不是session的管理不到位!
2. 常见的session管理,比较靠谱的可控的方式有类似我这里的方案,基于ip_hash的策略,还有,就是专门的开发接口来管理session,不用tomcat容器的那一套。比如将session放在mysql里面,或者redis等中间件里面。