前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到教程。
在做用户登录功能时,很多时候都需要验证码支持,验证码的目的是为了防止机器人模拟真实用户登录而恶意访问,如暴力破解用户密码/恶意评论等。目前也有一些验证码比较简单,通过一些OCR工具就可以解析出来;另外还有一些验证码比较复杂(一般通过如扭曲、加线条/噪点等干扰)防止OCR工具识别;但是在中国就是人多,机器干不了的可以交给人来完成,所以在中国就有很多打码平台,人工识别验证码;因此即使比较复杂的如填字、算数等类型的验证码还是能识别的。所以验证码也不是绝对可靠的,目前比较可靠还是手机验证码,但是对于用户来说相对于验证码还是比较麻烦的。
对于验证码图片的生成,可以自己通过如Java提供的图像API自己去生成,也可以借助如JCaptcha这种开源Java类库生成验证码图片;JCaptcha提供了常见的如扭曲、加噪点等干扰支持。
一、添加JCaptcha依赖
- <dependency>
- <groupId>com.octo.captcha</groupId>
- <artifactId>jcaptcha</artifactId>
- <version>2.0-alpha-1</version>
- </dependency>
- <dependency>
- <groupId>com.octo.captcha</groupId>
- <artifactId>jcaptcha-integration-simple-servlet</artifactId>
- <version>2.0-alpha-1</version>
- <exclusions>
- <exclusion>
- <artifactId>servlet-api</artifactId>
- <groupId>javax.servlet</groupId>
- </exclusion>
- </exclusions>
- </dependency>
com.octo.captcha . jcaptcha 提供了jcaptcha 核心;而jcaptcha-integration-simple-servlet提供了与Servlet集成。
二、GMailEngine
来自https://code.google.com/p/musicvalley/source/browse/trunk/musicvalley/doc/springSecurity/springSecurityIII/src/main/java/com/spring/security/jcaptcha/GMailEngine.java?spec=svn447&r=447(目前无法访问了),仿照JCaptcha2.0编写类似GMail验证码的样式;具体请参考com.github.zhangkaitao.shiro.chapter22.jcaptcha.GMailEngine。
三、MyManageableImageCaptchaService
提供了判断仓库中是否有相应的验证码存在。
- public class MyManageableImageCaptchaService extends
- DefaultManageableImageCaptchaService {
- public MyManageableImageCaptchaService(
- com.octo.captcha.service.captchastore.CaptchaStore captchaStore,
- com.octo.captcha.engine.CaptchaEngine captchaEngine,
- int minGuarantedStorageDelayInSeconds,
- int maxCaptchaStoreSize,
- int captchaStoreLoadBeforeGarbageCollection) {
- super(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds,
- maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection);
- }
- public boolean hasCapcha(String id, String userCaptchaResponse) {
- return store.getCaptcha(id).validateResponse(userCaptchaResponse);
- }
- }
四、JCaptcha工具类
提供相应的API来验证当前请求输入的验证码是否正确。
- public class JCaptcha {
- public static final MyManageableImageCaptchaService captchaService
- = new MyManageableImageCaptchaService(new FastHashMapCaptchaStore(),
- new GMailEngine(), 180, 100000, 75000);
- public static boolean validateResponse(
- HttpServletRequest request, String userCaptchaResponse) {
- if (request.getSession(false) == null) return false;
- boolean validated = false;
- try {
- String id = request.getSession().getId();
- validated =
- captchaService.validateResponseForID(id, userCaptchaResponse)
- .booleanValue();
- } catch (CaptchaServiceException e) {
- e.printStackTrace();
- }
- return validated;
- }
- public static boolean hasCaptcha(
- HttpServletRequest request, String userCaptchaResponse) {
- if (request.getSession(false) == null) return false;
- boolean validated = false;
- try {
- String id = request.getSession().getId();
- validated = captchaService.hasCapcha(id, userCaptchaResponse);
- } catch (CaptchaServiceException e) {
- e.printStackTrace();
- }
- return validated;
- }
- }
validateResponse():验证当前请求输入的验证码否正确;并从CaptchaService中删除已经生成的验证码;
hasCaptcha():验证当前请求输入的验证码是否正确;但不从CaptchaService中删除已经生成的验证码(比如Ajax验证时可以使用,防止多次生成验证码);
五、JCaptchaFilter
用于生成验证码图片的过滤器。
- public class JCaptchaFilter extends OncePerRequestFilter {
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- response.setDateHeader("Expires", 0L);
- response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
- response.addHeader("Cache-Control", "post-check=0, pre-check=0");
- response.setHeader("Pragma", "no-cache");
- response.setContentType("image/jpeg");
- String id = request.getRequestedSessionId();
- BufferedImage bi = JCaptcha.captchaService.getImageChallengeForID(id);
- ServletOutputStream out = response.getOutputStream();
- ImageIO.write(bi, "jpg", out);
- try {
- out.flush();
- } finally {
- out.close();
- }
- }
- }
CaptchaService使用当前会话ID当作key获取相应的验证码图片;另外需要设置响应内容不进行浏览器端缓存。
- <!-- 验证码过滤器需要放到Shiro之后 因为Shiro将包装HttpSession 如果不,可能造成两次的sesison id 不一样 -->
- <filter>
- <filter-name>JCaptchaFilter</filter-name>
- <filter-class>
- com.github.zhangkaitao.shiro.chapter22.jcaptcha.JCaptchaFilter
- </filter-class>
- </filter>
- <filter-mapping>
- <filter-name>JCaptchaFilter</filter-name>
- <url-pattern>/jcaptcha.jpg</url-pattern>
- </filter-mapping>
这样就可以在页面使用/jcaptcha.jpg地址显示验证码图片。
六、JCaptchaValidateFilter
用于验证码验证的Shiro过滤器。
- public class JCaptchaValidateFilter extends AccessControlFilter {
- private boolean jcaptchaEbabled = true;//是否开启验证码支持
- private String jcaptchaParam = "jcaptchaCode";//前台提交的验证码参数名
- private String failureKeyAttribute = "shiroLoginFailure"; //验证失败后存储到的属性名
- public void setJcaptchaEbabled(boolean jcaptchaEbabled) {
- this.jcaptchaEbabled = jcaptchaEbabled;
- }
- public void setJcaptchaParam(String jcaptchaParam) {
- this.jcaptchaParam = jcaptchaParam;
- }
- public void setFailureKeyAttribute(String failureKeyAttribute) {
- this.failureKeyAttribute = failureKeyAttribute;
- }
- protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- //1、设置验证码是否开启属性,页面可以根据该属性来决定是否显示验证码
- request.setAttribute("jcaptchaEbabled", jcaptchaEbabled);
- HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
- //2、判断验证码是否禁用 或不是表单提交(允许访问)
- if (jcaptchaEbabled == false || !"post".equalsIgnoreCase(httpServletRequest.getMethod())) {
- return true;
- }
- //3、此时是表单提交,验证验证码是否正确
- return JCaptcha.validateResponse(httpServletRequest, httpServletRequest.getParameter(jcaptchaParam));
- }
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- //如果验证码失败了,存储失败key属性
- request.setAttribute(failureKeyAttribute, "jCaptcha.error");
- return true;
- }
- }
七、MyFormAuthenticationFilter
用于验证码验证的Shiro拦截器在用于身份认证的拦截器之前运行;但是如果验证码验证拦截器失败了,就不需要进行身份认证拦截器流程了;所以需要修改下如FormAuthenticationFilter身份认证拦截器,当验证码验证失败时不再走身份认证拦截器。
- public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
- protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- if(request.getAttribute(getFailureKeyAttribute()) != null) {
- return true;
- }
- return super.onAccessDenied(request, response, mappedValue);
- }
- }
即如果之前已经错了,那直接跳过即可。
八、spring-config-shiro.xml
- <!-- 基于Form表单的身份验证过滤器 -->
- <bean id="authcFilter"
- class="com.github.zhangkaitao.shiro.chapter22.jcaptcha.MyFormAuthenticationFilter">
- <property name="usernameParam" value="username"/>
- <property name="passwordParam" value="password"/>
- <property name="rememberMeParam" value="rememberMe"/>
- <property name="failureKeyAttribute" value="shiroLoginFailure"/>
- </bean>
- <bean id="jCaptchaValidateFilter"
- class="com.github.zhangkaitao.shiro.chapter22.jcaptcha.JCaptchaValidateFilter">
- <property name="jcaptchaEbabled" value="true"/>
- <property name="jcaptchaParam" value="jcaptchaCode"/>
- <property name="failureKeyAttribute" value="shiroLoginFailure"/>
- </bean>
- <!-- Shiro的Web过滤器 -->
- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- <property name="securityManager" ref="securityManager"/>
- <property name="loginUrl" value="/login"/>
- <property name="filters">
- <util:map>
- <entry key="authc" value-ref="authcFilter"/>
- <entry key="sysUser" value-ref="sysUserFilter"/>
- <entry key="jCaptchaValidate" value-ref="jCaptchaValidateFilter"/>
- </util:map>
- </property>
- <property name="filterChainDefinitions">
- <value>
- /static/** = anon
- /jcaptcha* = anon
- /login = jCaptchaValidate,authc
- /logout = logout
- /authenticated = authc
- /** = user,sysUser
- </value>
- </property>
- </bean>
九、login.jsp登录页面
- <c:if test="${jcaptchaEbabled}">
- 验证码:
- <input type="text" name="jcaptchaCode">
- <img class="jcaptcha-btn jcaptcha-img"
- src="${pageContext.request.contextPath}/jcaptcha.jpg" title="点击更换验证码">
- <a class="jcaptcha-btn" href="javascript:;">换一张</a>
- <br/>
- </c:if>
根据jcaptchaEbabled来显示验证码图片。
十、测试
输入http://localhost:8080/chapter22将重定向到登录页面;输入正确的用户名/密码/验证码即可成功登录,如果输入错误的验证码,将显示验证码错误页面:
示例源代码:https://github.com/zhangkaitao/shiro-example;
十一、另附讲解:
先说明错误原因:
用<a href="http://lib.csdn.net/base/javaee" class="replace_word" title="Java EE知识库" target="_blank" >spring</a>安全拦截器进行验证码的验证的时候抛出异常。
throw new RuntimeException("captcha validation failed due to exception", cse);前台提交数据后跳转到如下方法:
- 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>
copy
仔细看了一下这个方法的作用:
- 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这个类的源码如下:
传入的参数都有相应的说明,其中这个类继承了AbstractManageableImageCaptchaService
继续深入到这个类中看个究竟:
这个类中果然有我们想要的方法:
相应的通过store.getCaptcha(ID)通过这个ID获得和这个sessionId匹配的验证码,再调用vilidateResponse方法进行验证,如果和输入的验证码相同就验证通过了。
验证通过后就把这个sessionId删除了,如果你再次登录,输入验证码的时候是同一个逻辑,之所以删除了这个ID我想是有好处的:
原因如下,如果不进行删除,随着的登录访问用户的过多,hashMap中的值会越来越多,这样以后再进行验证的时候速度和效率都会受到印象,如果删除了这个sessionId,这样这个store中的hashMap只是存储了当前正在准备登录的sessionId和相应的验证码!这样效率就大大提高了,如果有10万个人同时登录,都不是问题!
通过这个方法的调用我们就知道了sessionId是怎么和验证码绑定存储在hashMap中的!让我们进入源码验证一下:
上面就是CaptchaStore接口的实现类MapCaptchaStore,其中定义了一个hashMap,通过storeCaptcha(String id,Captcha captcha)方法来存储sessionId和captcha的键值对,这是进入登录页面生成的时候调用的方法,当进行验证的时候就需要hasCaptcha(String ID)方法和
但是我们是调用了
- MapCaptchaStore 的子类FastHashMapCaptchaStore来存储信息的:同样看FastHashMapCaptchaStore这个类:
- 17 public class FastHashMapCaptchaStore extends MapCaptchaStore {
- 18 public FastHashMapCaptchaStore() {
- 19 this.store = new FastHashMap();
- 20 }
- 21 }
- 这就是这个类的全部了,再看一下FastHashMap类:
- 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 }
- 这个类是HashMap的一个扩展,里面有两种方式操作,一种是快速的不同步,一种是同步的操作!
- 显然FastHashMapCaptchaStore就是一个HashMap。验证码的实现在这个类中:
- 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="white-space:pre"><img src="https://img-my.csdn.net/uploads/201211/23/1353676134_4969.png" alt=""> </span>
- 内存中清楚显示了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,验证码就无法验证。就报错了。
- 学习中应该多去看源码,分析源码设计理念。