JCaptcha做验证码遇到的问题引出的思考

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

原文地址:https://www.cnblogs.com/shihuc/p/5694636.html