2

时间:2017年07月09日星期日
说明:本文部分内容均来自慕课网。@慕课网:http://www.imooc.com
教学源码:无
学习源码:https://github.com/zccodere/s...

第一章:概述

1-1 课程概述

主要内容

验证码历史
课程内容
不同方案对比
设计与实现
总结

1-2 验证码历史

验证码历史

无验证码:垃圾骚扰
Luis von Ahn:Captcha
不断的升级
去验证码

常见验证码

clipboard.png

clipboard.png

1-3 项目介绍

完成类似最后一张图片的验证码设计与实现

对比方案
完成设计
编码实现
结果演示

结果演示

clipboard.png

不同方案对比(一)

浏览器请求验证码图片
服务器返回验证码图片及图片标识
浏览器提交验证码
服务器验证图片内容及标识

不同方案对比(二)

浏览器请求验证码图片
服务器返回验证码图片及图片标识
浏览器提交验证码
    图片文字/计算结果等
    坐标
服务器验证
    验证图片内容及标识
    验证坐标及标识

设计与实现

包结构
--controller、generator
主要类及作用
--Image:生成验证码图片核心类
--BufferedImageWrap:图片包装类
--ImageGroup:原始图片分组
--GenerateImageGroup:单次验证使用图片组
--Cache:单次验证数据缓存
--LoginController

程序设计:技术选择
教学使用

SpringMVC
JSP
Spring(4.0.5)

学习使用

SpringBoot
Freemarker

思路整理

每次显示几张图片:由8张小图组成的一张大图
答案图片位置
选中位置坐标
坐标验证
前后关联

第二章:图片生成及页面显示

2-1 页面结构及LoginController介绍

部分代码演示:源码请到我的github地址查看

login.html

<html>
<head>
<title>登录</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<script type="text/javascript">

    var index = 1;
    function addImg(e){
        var parentDiv = document.getElementById("insert");
        var topValue = 0,leftValue = 0;
        var obj = parentDiv;
        while(obj){
            leftValue += obj.offsetLeft;
            topValue += obj.offsetTop;
            obj = obj.offsetParent;
        }
        
        e = e || window.event;
        var left = e.clientX + document.body.scrollLeft - document.body.clientLeft - 10;
        var top = e.clientY + document.body.scrollTop - document.body.clientTop - 10;
        var imgDivId = "img_" + index++;
        
        var newDiv = document.createElement("div");
        parentDiv.appendChild(newDiv);
        
        newDiv.id = imgDivId;
        newDiv.style.position = "relative";
        
        newDiv.style.zIndex = index;
        newDiv.style.width = "20px"
        newDiv.style.height = "20px";
        newDiv.style.top = top - topValue - 150 + 10 + "px";
        newDiv.style.left = left - leftValue -300 + "px";
        newDiv.style.display = "inline";
        newDiv.setAttribute("onclick","removeSelf('"+imgDivId+"');");
        
        var img = document.createElement("img");
        newDiv.appendChild(img);
        
        img.src = "img/a.png";
        img.style.width = "20px";
        img.style.height = "20px";
        img.style.top = "0px";
        img.style.left = "0px";
        img.style.position = "absolute";
        img.style.zIndex = index;
    }
    
    function removeSelf(id){
        document.getElementById("insert").removeChild(document.getElementById(id));
    }
    
    function login(){
        var parentDiv = document.getElementById("insert");
        var nodes = parentDiv.childNodes;
        var result = "";
        for(var i=0;i<nodes.length;i++){
            var id = nodes[i].id;
            if(id && id.startsWith('img_')){
                var top = document.getElementById(id).style.top;
                var left = document.getElementById(id).style.left;
                result = result + top.replace('px','') + ',' + left.replace('px','') + ';';
            }
        }
        console.info(result.substr(0,result.length - 1));
        document.getElementById('location').value = result.substr(0,result.length - 1);
        document.getElementById('loginForm').submit();
    }

</script>

</head>
<body>
    
    <form id="loginForm" action="dologin" method="post">
        <input type="hidden" id="location" name="location" />
        
        <span>邮箱/用户名/手机号</span>
        <br />
        <input type="text" name="username" />
        <br />
        <span>密码</span>
        <br />
        <input type="password" name="password" />
        <br />
        <span>选出图片中的"${tip}"</span>
        <br />
        <div id="insert">
        <img src="../targetImage/${file}" height="150" width="300" onclick="addImg()" />
        </div>
        <br />
        <input type="button" value="登录" onclick="login()"/>
        
    </form>
    
</body>
</html>

LoginController类

package com.myimooc.identifying.controller;

import java.io.IOException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.myimooc.identifying.generator.Image;
import com.myimooc.identifying.generator.ImageResult;

/**
 * 登录控制器
 * @author ZhangCheng on 2017-07-09
 *
 */
@Controller
public class LoginController {
    
    /**
     * 登录主页
     * @param model
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/login")
    public String identify(Model model,HttpServletRequest request,HttpServletResponse response){
        try{
            ImageResult imageResult  = Image.generateImage();
            model.addAttribute("file", imageResult.getName());
            model.addAttribute("tip", imageResult.getTip());
            System.out.println(imageResult.getName() + imageResult.getTip());
            Cookie cookie = new Cookie("note",imageResult.getUniqueKey());
            response.addCookie(cookie);
            request.getSession().setAttribute(imageResult.getUniqueKey(), imageResult);
        }catch(Exception e){
            System.out.println("获取图片失败");
            e.printStackTrace();
        }
        return "login";
    }
    
     /**
     * 刷新图片
     *
     * @param request
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/getPng")
    @ResponseBody
    public String getPng(HttpServletRequest request) throws IOException{
        ImageResult imageResult = Image.generateImage();
        ((HttpServletRequest) request).getSession().setAttribute("imageResult", imageResult);
        return imageResult.getName() + "," + imageResult.getTip();
    }
    
    /**
     * 验证消息
     *
     * @param location
     * @param request
     * @param userName
     * @param password
     * @return
     */
    @PostMapping("/dologin")
    @ResponseBody
    public String doLogin(String location, HttpServletRequest request, String userName, String password, RedirectAttributes redirectAttributes) {
        System.out.println("验证坐标:"+ location);
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        Cookie note = null;
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("note")) {
                note = cookie;
                break;
            }
        }
        
        if(null == note){
            return "ERROR";
        }
        
        ImageResult imageResult = (ImageResult)request.getSession().getAttribute(note.getValue());
        
        if(validate(location,imageResult)){
            return "OK";
        }
        
        return "ERROR";
    }
    /**
     * 验证是否正确
     * @param locationString
     * @param imageResult
     * @return
     */
    private boolean validate(String locationString, ImageResult imageResult) {
        
        String[] resultArray = locationString.split(";");
        int[][] array = new int[resultArray.length][2];
        for (int i = 0; i<resultArray.length;i++) {
            String[] temp = resultArray[i].split(",");
            array[i][0] = Integer.parseInt(temp[0]) + 150 - 10;
            array[i][1] = Integer.parseInt(temp[1]) + 300;
        }
        
        for(int i=0;i<array.length;i++){
            int location = location(array[i][1],array[i][0]);
            System.out.println("解析后的坐标序号:" + location);
            if(!imageResult.getKeySet().contains(location)){
                return false;
            }
        }
        
        return true;
    }

    private int location(int x, int y) {
        if(y >=0 && y<75){
            return xLocation(x);
        }else if(y >=75 && y<=150){
            return xLocation(x)+4;
        }else{
            // 脏数据
            return -1;
        }
    }

    private int xLocation(int x) {
        if(x >=0 && x<75){
            return 0;
        }else if(x >=75 && x<150){
            return 1;
        }else if(x >=150 && x<225){
            return 2;
        }else if(x >=225 && x<=300){
            return 3;
        }else{
            // 脏数据
            return -1;
        }
    }
}

2-2 如何生成图片generateImage

Image类

package com.myimooc.identifying.generator;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import javax.imageio.ImageIO;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 生成验证码图片核心类
 * @author ZhangCheng on 2017-07-09
 *
 */
public class Image {
    
    private static final Logger log= LoggerFactory.getLogger(Image.class);

    private static Map<String,ImageGroup> imageGroupMap=new HashMap<>();
    private static Map<Integer,Map<String,ImageGroup>> countGroupMap=new HashMap<>();
    
    /**
     * 功能:由小图生成一种大图
     * @return
     * @throws IOException
     */
    public static ImageResult generateImage()throws IOException{
        // 初始化
        initImageGroup();
        log.debug("初始化完成");
        GenerateImageGroup generateImageGroup = randomImageGroups();
        List<BufferedImageWrap> images = new ArrayList<BufferedImageWrap>();
        // 找到图片干扰项
        for (ImageGroup group : generateImageGroup.getGroups()) {
            for (String imgName : group.getImages()) {
                images.add(new BufferedImageWrap(false,getBufferedImage(imgName)));
            }
        }
        // 找到图片答案项
        for(String imgName : generateImageGroup.getKeyGroup().getImages()){
            images.add(new BufferedImageWrap(true,getBufferedImage(imgName)));
        }
        return mergeImage(images,generateImageGroup.getKeyGroup().getName());
    }

    /**
     * 功能:根据图片名称获得图片缓冲流
     * @param imgName
     * @return
     * @throws IOException
     */
    private static BufferedImage getBufferedImage(String imgName)throws IOException {
        String rootPath = Image.class.getClassLoader().getResource("sourceImage/").getPath();
        String imgPath = rootPath + imgName;
        File file = new File(imgPath);
        return ImageIO.read(file);
    }
    
    /**
     * 功能:将小图合并成一种大图
     * @param images
     * @param name
     * @return
     */
    private static ImageResult mergeImage(List<BufferedImageWrap> imageWraps, String tip) {
        Collections.shuffle(imageWraps);
        // 原始图片宽200像素,高200像素
        int width = 200;
        int high = 200;
        int totalWidth = width * 4;
        
        BufferedImage destImage = new BufferedImage(totalWidth,400,BufferedImage.TYPE_INT_RGB);
        int x1 = 0;
        int x2 = 0;
        int order = 0;
        List<Integer> keysOrderList = new ArrayList<Integer>();
        StringBuilder keysOrder = new StringBuilder();
        Set<Integer> keySet = new HashSet<Integer>();
        for(BufferedImageWrap image : imageWraps){
            int[] rgb = image.getBufferedImage().getRGB(0, 0, width, high, null, 0, width);
            if(image.isKey()){
                keysOrderList.add(order);
                int x = (order % 4) * 200;
                int y = order < 4 ? 0:200;
                keySet.add(order);
                keysOrder.append(order).append("(").append(x).append(",").append(y).append(")|");
            }
            if(order < 4 ){
                // 设置上半部分的RGB
                destImage.setRGB(x1, 0, width,high,rgb,0,width);
                x1 += width;
            }else{
                destImage.setRGB(x2, high, width,high,rgb,0,width);
                x2 += width;
            }
            order++;
        }
        
        keysOrder.deleteCharAt(keysOrder.length() - 1);
        System.out.println("答案位置:" + keysOrder);
        String fileName = UUID.randomUUID().toString().replaceAll("-", "") + ".jpeg";
        String rootPath = Image.class.getClassLoader().getResource("static/targetImage/").getPath();
        //String rootPath = Image.class.getClassLoader().getResource("sourceImage/").getPath();
        log.info("根路径:{}",rootPath);
        String fileUrl = rootPath + fileName;
        // 保存图片
        saveImage(destImage,fileUrl,"png");
        
        ImageResult ir = new ImageResult();
        ir.setName(fileName);
        ir.setKeySet(keySet);
        ir.setUniqueKey(fileName);
        ir.setTip(tip);
        return ir;
    }
    
    /**
     * 功能:将图片写入指定的路径
     * @param destImage
     * @param fileUrl
     * @param string
     */
    private static void saveImage(BufferedImage destImage, String fileUrl, String format) {
        File file=new File(fileUrl);
        log.debug(file.getAbsolutePath());
        try {
            ImageIO.write(destImage,format,file);
        } catch (IOException e) {
            log.info("图片写入失败");
            e.printStackTrace();
        }
    }

    /**
     * 功能:随机生成图片答案和干扰组
     * @return
     */
    private static GenerateImageGroup randomImageGroups(){
        
        List<ImageGroup> result = new ArrayList<ImageGroup>();
        int num = random(0, imageGroupMap.size() - 1);
        
        String name = new ArrayList<String>(imageGroupMap.keySet()).get(num);
        ImageGroup keyGroup = imageGroupMap.get(name);
        
        Map<Integer,Map<String,ImageGroup>> thisCountGroupMap = new HashMap<>(countGroupMap);
        thisCountGroupMap.get(keyGroup.getCount()).remove(name);
        
        // 假设总量8个,每种名称图片只有2个或4个,为了逻辑简单些
        int leftCount = 8 - keyGroup.getCount();
        if(leftCount == 4){
            if(new Random().nextInt() % 2 == 0){
                List<ImageGroup> groups = new ArrayList<ImageGroup>(thisCountGroupMap.get(4).values());
                if(groups.size() > 1){
                    num = random(0, groups.size() - 1);
                }else{
                    num = 0;
                }
                result.add(groups.get(num));
            }else{
                List<ImageGroup> groups = new ArrayList<ImageGroup>(thisCountGroupMap.get(2).values());
                int num1 = random(0, groups.size() - 1);
                result.add(groups.get(num1));
                
                int num2 = random(0, groups.size() - 1,num1);
                result.add(groups.get(num2));
            }
        }else if(leftCount == 6){
            if(new Random().nextInt() % 2 == 0){
                List<ImageGroup> groups1 = new ArrayList<ImageGroup>(thisCountGroupMap.get(4).values());
                int num1 = random(0, groups1.size() - 1);
                result.add(groups1.get(num1));
                
                List<ImageGroup> groups2 = new ArrayList<ImageGroup>(thisCountGroupMap.get(2).values());
                int num2 = random(0, groups2.size() - 1);
                result.add(groups2.get(num2));
            }else{
                List<ImageGroup> groups = new ArrayList<ImageGroup>(thisCountGroupMap.get(2).values());
                int num1 = random(0, groups.size() - 1);
                result.add(groups.get(num1));
                
                int num2 = random(0, groups.size() - 1,num1);
                result.add(groups.get(num2));
                
                int num3 = random(0, groups.size() - 1,num1,num2);
                result.add(groups.get(num3));
            }
        }
        
        return new GenerateImageGroup(keyGroup, result);
        
    }
    
    /**
     * 功能:初始化图片组。后期优化可从数据库获取
     */
    private static void initImageGroup(){
        ImageGroup group1 = new ImageGroup("包包",4,"bao/1.jpg","bao/2.jpg","bao/3.jpg","bao/4.jpg");
        ImageGroup group2 = new ImageGroup("老虎",4,"laohu/1.jpg","laohu/2.jpg","laohu/3.jpg","laohu/4.jpg");
        ImageGroup group3 = new ImageGroup("糖葫芦",4,"tanghulu/1.jpg","tanghulu/2.jpg","tanghulu/3.jpg","tanghulu/4.jpg");
        ImageGroup group4 = new ImageGroup("小慕",4,"xiaomu/1.jpg","xiaomu/2.jpg","xiaomu/3.jpg","xiaomu/4.jpg");
        ImageGroup group5 = new ImageGroup("柚子",4,"youzi/1.jpg","youzi/2.jpg","youzi/3.jpg","youzi/4.jpg");
        ImageGroup group6 = new ImageGroup("订书机",2,"dingshuji/1.jpg","dingshuji/2.jpg");
        ImageGroup group7 = new ImageGroup("蘑菇",2,"mogu/1.jpg","mogu/2.jpg");
        ImageGroup group8 = new ImageGroup("磁铁",2,"citie/1.jpg","citie/2.jpg");
        ImageGroup group9 = new ImageGroup("土豆",4,"tudou/1.jpg","tudou/2.jpg","tudou/3.jpg","tudou/4.jpg");
        ImageGroup group10 = new ImageGroup("兔子",4,"tuzi/1.jpg","tuzi/2.jpg","tuzi/3.jpg","tuzi/4.jpg");
        ImageGroup group11 = new ImageGroup("仙人球",4,"xianrenqiu/1.jpg","xianrenqiu/2.jpg","xianrenqiu/3.jpg","xianrenqiu/4.jpg");
        
        initMap(group1,group2,group3,group4,group5,group6,group7,group8,group9,group10,group11);
    }
    
    /**
     * 功能:初始化所有图片组
     * @param groups
     */
    private static void initMap(ImageGroup... groups) {
        for (ImageGroup group : groups) {
            imageGroupMap.put(group.getName(),group);
            if(!countGroupMap.containsKey(group.getCount())){
                countGroupMap.put(group.getCount(),new HashMap<String,ImageGroup>());
            }
            countGroupMap.get(group.getCount()).put(group.getName(),group);
        }
    }
    
    /**
     * 功能:生成随机整数
     * @param min
     * @param max
     * @return
     */
    private static int random(int min,int max){
        Random random = new Random();
        return random.nextInt(max - min + 1) + min;
    }
    
    /**
     * 功能:生成随机整数不在指定整数数组里
     * @param min
     * @param max
     * @param not
     * @return
     */
    private static int random(int min,int max,Integer... not){
        int num = random(min,max);
        List<Integer> notList = Arrays.asList(not);
        while(notList.contains(num)){
            num = random(min,max);
        }
        return num;
    }
}

2-3 如何将图片融合mergeImage

/**
     * 功能:将小图合并成一种大图
     * @param images
     * @param name
     * @return
     */
    private static ImageResult mergeImage(List<BufferedImageWrap> imageWraps, String tip) {
        Collections.shuffle(imageWraps);
        // 原始图片宽200像素,高200像素
        int width = 200;
        int high = 200;
        int totalWidth = width * 4;
        
        BufferedImage destImage = new BufferedImage(totalWidth,400,BufferedImage.TYPE_INT_RGB);
        int x1 = 0;
        int x2 = 0;
        int order = 0;
        List<Integer> keysOrderList = new ArrayList<Integer>();
        StringBuilder keysOrder = new StringBuilder();
        Set<Integer> keySet = new HashSet<Integer>();
        for(BufferedImageWrap image : imageWraps){
            int[] rgb = image.getBufferedImage().getRGB(0, 0, width, high, null, 0, width);
            if(image.isKey()){
                keysOrderList.add(order);
                int x = (order % 4) * 200;
                int y = order < 4 ? 0:200;
                keySet.add(order);
                keysOrder.append(order).append("(").append(x).append(",").append(y).append(")|");
            }
            if(order < 4 ){
                // 设置上半部分的RGB
                destImage.setRGB(x1, 0, width,high,rgb,0,width);
                x1 += width;
            }else{
                destImage.setRGB(x2, high, width,high,rgb,0,width);
                x2 += width;
            }
            order++;
        }
        
        keysOrder.deleteCharAt(keysOrder.length() - 1);
        System.out.println("答案位置:" + keysOrder);
        String fileName = UUID.randomUUID().toString().replaceAll("-", "") + ".jpeg";
        String rootPath = Image.class.getClassLoader().getResource("static/targetImage/").getPath();
        //String rootPath = Image.class.getClassLoader().getResource("sourceImage/").getPath();
        log.info("根路径:{}",rootPath);
        String fileUrl = rootPath + fileName;
        // 保存图片
        saveImage(destImage,fileUrl,"png");
        
        ImageResult ir = new ImageResult();
        ir.setName(fileName);
        ir.setKeySet(keySet);
        ir.setUniqueKey(fileName);
        ir.setTip(tip);
        return ir;
    }
    
    /**
     * 功能:将图片写入指定的路径
     * @param destImage
     * @param fileUrl
     * @param string
     */
    private static void saveImage(BufferedImage destImage, String fileUrl, String format) {
        File file=new File(fileUrl);
        log.debug(file.getAbsolutePath());
        try {
            ImageIO.write(destImage,format,file);
        } catch (IOException e) {
            log.info("图片写入失败");
            e.printStackTrace();
        }
    }

第三章:验证过程及总结

3-1 验证验证码过程及总结

总结

验证码历史
不同方案对比
设计与实现
总结

妙手空空
1.3k 声望368 粉丝

博观而约取,厚积而薄发