简介

  • 在实现登录功能时,大多数情况需要验证码支持,目的是防止机器人暴力破解密码。
  • 目前比较简单的验证码通过一些OCR工具就可以解析出来,复杂的验证码(一般通过扭曲、加线条或噪点等干扰)能够防止OCR识别。更复杂的如填字、算数等验证码则需要人工识别。
  • 目前比较可靠的是手机验证码,但是对于用户来说比普通验证码要麻烦得多。
  • 验证码图片可以通过Java提供的图片API实现,也可以借助如JCaptcha(Completely Automated Public Turing Test To Tell Computers adn Humans Apart)这种开源Java类库生成,它提供了常见的扭曲、加线条和噪点等干扰的支持。

示例

  • 基于第十六章,是一个权限管理系统,但本例中后台管理不是主要内容,现新增一个jcaptcha包,放置所有和验证码有关的类,用于实现用户登录时获取验证码和提交表单后校验验证码的功能。

  • 依赖:

    <!--jcaptcha核心-->
    <dependency>  
        <groupId>com.octo.captcha</groupId>  
        <artifactId>jcaptcha</artifactId>  
        <version>2.0-alpha-1</version>  
    </dependency>  
    <!--支持与servlet集成-->
    <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>
    
  • 实际能用的依赖如下:

      <!--jcaptcha验证码工具-->
      <dependency><!--jcaptcha核心-->
    <groupId>com.octo.captcha</groupId>
    <artifactId>jcaptcha</artifactId>
    <version>1.0</version>
    <exclusions> <!--不排除此包,tomcat启动出错-->
      <exclusion>
        <artifactId>servlet-api</artifactId>
        <groupId>javax.servlet</groupId>
      </exclusion>
    </exclusions>
    


    com.octo.captcha
    jcaptcha-api
    1.0
  • GMailEngine:验证码图片生成引擎,继承自ListImageCaptchaEngine,只需要简单设置以下图片和字体大小等,即可仿照jcaptcha2.0生成类似GMail验证码的样式。

public class GMailEngine extends ListImageCaptchaEngine {

    @Override
    protected void buildInitialFactories() {

        // 图片和字体大小设置
        int minWordLength = 4;
        int maxWordLength = 5;
        int fontSize = 20;
        int imageWidth = 100;
        int imageHeight = 36;

        WordGenerator dictionnaryWords = new ComposeDictionaryWordGenerator(
                new FileDictionary("toddlist"));

        // word2image components
        TextPaster randomPaster = new DecoratedRandomTextPaster(minWordLength,
                maxWordLength, new RandomListColorGenerator(new Color[]{
                new Color(23, 170, 27), new Color(220, 34, 11),
                new Color(23, 67, 172)}), new TextDecorator[]{});
        BackgroundGenerator background = new UniColorBackgroundGenerator(
                imageWidth, imageHeight, Color.white);
        FontGenerator font = new RandomFontGenerator(fontSize, fontSize,
                new Font[]{new Font("nyala", Font.BOLD, fontSize),
                        new Font("Bell MT", Font.PLAIN, fontSize),
                        new Font("Credit valley", Font.BOLD, fontSize)});

        ImageDeformation postDef = new ImageDeformationByFilters(
                new ImageFilter[]{});
        ImageDeformation backDef = new ImageDeformationByFilters(
                new ImageFilter[]{});
        ImageDeformation textDef = new ImageDeformationByFilters(
                new ImageFilter[]{});

        WordToImage word2image = new DeformedComposedWordToImage(font,
                background, randomPaster, backDef, textDef, postDef);

        addFactory(new GimpyFactory(dictionnaryWords, word2image));
    }

}
  • MyManageableImageCaptchaService:使用GMailEngine生成验证码图片、比对验证码(每验证一次无论成功与否都会删除该验证码)。
public class MyManageableImageCaptchaService 
    extends DefaultManageableImageCaptchaService {

    /**
     * @Author haien
     * @Description 构造器,生成验证码图片
     * @Date 16:08 2019/4/19
     * @Param [captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds,
        maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection]
     **/
    public MyManageableImageCaptchaService(CaptchaStore captchaStore,
                                           CaptchaEngine captchaEngine,
                                           int minGuarantedStorageDelayInSeconds,
                                           int maxCaptchaStoreSize,
                                           int captchaStoreLoadBeforeGarbageCollection)
                                           {
        super(captchaStore,captchaEngine,minGuarantedStorageDelayInSeconds,
                maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection);
    }

    /**
     * @Author haien
     * @Description 根据会话id获取当前会话的验证码,与用户输入的比对
     * @Date 2019/4/11
     * @Param [id会话id, userCaptchaResponse用户输入的验证码]
     * @return boolean
     **/
    public boolean validate(String id,String userCaptchaResponse){
        //验证但不删除
        return store.getCaptcha(id).validateResponse(userCaptchaResponse); 
    }
}
  • JCaptcha:jcaptcha工具类,利用MyManageableImageCaptchaService生成和验证验证码。
public class JCaptcha {
    public static final MyManageableImageCaptchaService captchaService=
            new MyManageableImageCaptchaService(new FastHashMapCaptchaStore(),
                    new GMailEngine(),180,
                    100000,75000);

    /**
     * @Author haien
     * @Description 比对当前请求输入的验证码是否正确,
                    并从CaptchaService中移除已使用过的验证码
     * @Date 2019/4/11
     * @Param [request, userCaptchaResponse用户输入的验证码]
     * @return boolean
     **/
    public static boolean validateResponse(HttpServletRequest request,
            String userCaptchaResponse){
        //为true应该是没有session则新建
        if(request.getSession(false)==null) return false;

        boolean validated=false;
        try {
            //获取会话id
            String id = request.getSession().getId();
            //将用户输入的验证码与库存比对,并移除验证码与id(无论正确与否)
            validated = captchaService.validateResponseForID(id, userCaptchaResponse)
                    .booleanValue();
        }catch (CaptchaServiceException e){
            e.printStackTrace();
        }

        return validated;
    }

    /**
     * @Author haien
     * @Description 比对,但不移除验证码,适用于Ajax异步验证,
                    此时不是真的验证,还不能删除验证码
     * @Date 2019/4/11
     * @Param [request, userCaptchaResponse]
     * @return boolean
     **/
    public static boolean validate(HttpServletRequest request,
            String userCaptchaResponse){
        if (request.getSession(false)==null) return false;

        boolean validated=false;
        try {
            String id=request.getSession().getId();
            validated=captchaService.validate(id,userCaptchaResponse);
        }catch (CaptchaServiceException e){
            e.printStackTrace();
        }

        return validated;
    }
}
  • JCaptchaFilter:拦截前端的验证码请求,调用JCaptcha工具类生成并向前端写验证码图片。
//保证只执行一次,注:继承的是spring的Filter
public class JCaptchaFilter extends OncePerRequestFilter { 

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response,FilterChain chain) 
            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();
        //captchaService使用当前会话的id作为key获取相应的验证码图片
        BufferedImage bi=JCaptcha.captchaService.getImageChallengeForID(id);
        ServletOutputStream out=response.getOutputStream();
        ImageIO.write(bi,"jpg",out);
        try {
            out.flush();
        }finally {
            out.close();
        }
    }
}
  • 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>
    
    <script>
        $(function() {
            $(".jcaptcha-btn").click(function() {
                $(".jcaptcha-img").attr("src",
                '${pageContext.request.contextPath}/jcaptcha.jpg?' //按钮对应url,
                                                                   //触发JCaptchaFilter
                    +new Date().getTime());
            });
        });
    </script>
    
  • web.xml:分配验证码请求给JCaptchaFilter拦截。

    <!--验证码过滤器需要放到ShiroFilter之后,因为Shiro将包装HttpSession;
    否则可能造成两次的session id不同-->
      <filter>
        <filter-name>JCaptchaFilter</filter-name>
        <filter-class>com.haien.chapter22.jcaptcha.JCaptchaFilter</filter-class>
        <async-supported>true</async-supported>
      </filter>
      <filter-mapping>
        <filter-name>JCaptchaFilter</filter-name>
        <!--前台登录页面点击“获取验证码”,其url为此,则执行该过滤链,
        生成验证码图片并写到客户端-->
        <url-pattern>/jcaptcha.jpg</url-pattern>
      </filter-mapping>
    
  • JCaptchaValidateFilter:拦截验证码验证请求的拦截器,主要是调用JCaptcha工具类进行验证。

public class JCaptchaValidateFilter extends AccessControlFilter {
    //是否开启验证码支持
    private boolean jcaptchaEnabled=true;
    //前台提交的验证码参数名
    private String jcaptchaParam="jcaptchaCode";
    //验证失败后存储到的属性名
    private String failureKeyAttribute="shiroLoginFailure";

    public void setJcaptchaEnabled(boolean jcaptchaEnabled) {
        this.jcaptchaEnabled = jcaptchaEnabled;
    }
    public void setJcaptchaParam(String jcaptchaParam) {
        this.jcaptchaParam = jcaptchaParam;
    }
    public void setFailureKeyAttribute(String failureKeyAttribute) {
        this.failureKeyAttribute = failureKeyAttribute;
    }

    /**
     * @Author haien
     * @Description 验证验证码
     * @Date 2019/4/19
     * @Param [request, response, mappedValue]
     * @return boolean
     **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
                                      Object mappedValue) throws Exception {

        //1. 设置验证码是否开启属性,页面据此决定是否显示验证码
        request.setAttribute("jcaptchaEnabled",jcaptchaEnabled);
        //将HttpServlet的request转为HttpServlet类型
        HttpServletRequest httpServletRequest=WebUtils.toHttp(request);

        //2. 判断验证码是否禁用|不是由表单提交的
        if(jcaptchaEnabled==false || 
            ! "post".equalsIgnoreCase(httpServletRequest.getMethod())){
            //则不用验证,直接结束
            return true;
        }

        //3. 验证 验证码,正确则返回true,否则false
        return JCaptcha.validateResponse(httpServletRequest,
                httpServletRequest.getParameter(jcaptchaParam));
    }

    /**
     * @Author haien
     * @Description 验证失败处理
     * @Date 2019/4/19
     * @Param [request, response]
     * @return boolean
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
            throws Exception {
        //若验证失败,则存储failureKeyAttribute属性;可通过getFailureKeyAttribute()获取
        request.setAttribute(failureKeyAttribute,"jCaptcha.error");
        return true;
    }
}
  • MyFormAuthenticationFilter:继承FormAuthenticationFilter,自定义身份验证拦截器,主要是增加验证码错误情况下不验证身份的条件。
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
            Object mappedValue) {

        //若为登录请求,无论登录与否,直接进入onAccessDenied
        if(isLoginRequest(request,response)) return false;

        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    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.haien.chapter22.jcaptcha.MyFormAuthenticationFilter">
        <property name="usernameParam" value="username"/>
        <property name="passwordParam" value="password"/>
        <property name="rememberMeParam" value="rememberMe"/>
        <property name="failureKeyAttribute" value="shiroLoginFailure"/><!--值同JCaptchaValidateFilter-->
    </bean>
    <bean id="JCaptchaValidateFilter" class="com.haien.chapter22.jcaptcha.JCaptchaValidateFilter">
        <property name="jcaptchaEnabled" value="true"/>
        <property name="jcaptchaParam" value="jcaptchaCode"/>
        <property name="failureKeyAttribute" value="shiroLoginFailure"/>
    </bean>
    <bean id="sysUserFilter"
          class="com.haien.chapter22.web.shiro.filter.SysUserFilter"/>
    
    <!-- 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">
            <!--其实第一行可以不配置,因为spring-mvc.xml已经映射了静态资源?-->
            <value>
                /static/**=anon
                /jcaptcha*=anon
                /login = JCaptchaValidate,authc
                /logout = logout
                /authenticated = authc
                /** = user,sysUser
            </value>
        </property>
    </bean>
    
  • 测试:访问http://localhost:8080/chapter22,将重定向到登录页面,显示验证码图片,点击可换一张。输入验证码,错误则返回登录页面,并提示验证码错误。