您的位置 首页 java

前后端分离模式下验证码实现

一、验证码

验证码,主要用于防刷,特别是注册页面。如果没有防刷机制,攻击者可以通过爬虫等技术,批量注册空虚的用户。

二、思路

传统单体应用,前后端融合为一体,彼此之间的交互通过 Session 来桥接,而目前前后端分离开发是主流,此方法便不再适用。这时我们需要一个与之类似的、可以独立出来的存储空间,来存储相关信息。 redis 的启用,使这一切成为可能,它是一个基于内存的 key-value 类型存储结构的数据库,同时存储的数据带有有效期,和 Session 可以说是神似呀,故笔者决定采用它来解决这个需求。

具体落地步骤如下:

  1. 用户进入 web 页面,前端请求后端生成一幅带验证码图片给前端。
  2. 后端生成图片的同时,将图片中的正确验证码 rightCode 存到 Redis 中, key 为用户的 ip, value 为正确码 rightCode , 有效时间 几分钟。
  3. 用户的输入码 tryCode ,提交表单时传递给后端。
  4. 后端接受用户的 tryCode,根据用户的 ip,通过 key = ip 为条件查 value,获取 rightCode,与 tryCode 比对,返回验证结果。

三、代码实现

  • 前端:使用 Vue,并借助 ElementUI 简单美化页面、axios 发送 ajax 请求。
  • 后端:使用 SpringBoot,并借助 Kaptcha 生成验证码。

(1)前端

 <template>
    <div id="app">
        <el-form :inline="true">
            <el-form-item>
                <el-image src="#34; onclick="this.src='#39;+new Date()*1"></el-image>
            </el-form-item>
            <el-form-item>
                <el-input v-model="code" style="width: 150px"></el-input>
            </el-form-item>
            <el-form-item>
                <el-button @click="checkCode" type="primary">Check</el-button>
            </el-form-item>
        </el-form>


    </div>
</template>

<script>
    export default {
        data() {
            return {
                code: ""
            }
        },
        methods: {
            async checkCode() {
                const {
                    data: res
                } = await this.$http.get(`{this.code}`);
                if (res.code === 200) {
                    this.$notify({
                        title: 'It works!',
                        type: 'success',
                         message : '验证码校验正确',
                        duration: 2000
                    })
                } else {
                    this.$notify({
                        title: 'It do not works!',
                        type: 'error',
                        message: '校验失败',
                        duration: 2000
                    })
                }
            }
        }
    }
</script>

<style>
    #app {
        font-family: Helvetica, sans-serif;
        text-align: center;
    }
</style>  

(2)后端

 package com.cun.kaptcha.controller;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax. servlet .ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.cun.kaptcha.utils.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.code.kaptcha.impl.DefaultKaptcha;

@Controller
@RequestMapping("kaptcha")
public class KaptchaController {

    private final Logger logger = LoggerFactory.getLogger(KaptchaController.class);

    /**
     * 验证码工具
     */    @Autowired
    DefaultKaptcha defaultKaptcha;

    /**
     * redis工具
     */    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 生成验证码
     *
     * @param httpServletRequest  获取ip
     * @param httpServletResponse 传输图片
     * @throws Exception
     */    @RequestMapping("/img")
    public void img(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {
        byte[] captchaChallengeAs jpeg  = null;
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            String createText = defaultKaptcha.createText();
            // 生产验证码字符串并保存到 Redis 中,ip-rightCode,有效期为 1 小时
            String ip = httpServletRequest.getRemoteAddr();
            logger.info("ip:" + ip + ",rightCode = " + createText);
            redisTemplate.opsForValue().set(ip, createText, 1, TimeUnit.HOURS);
            // 使用生产的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
            BufferedImage challenge = defaultKaptcha.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (IllegalArgumentException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-store");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }

    /**
     * 校对验证码
     *
     * @param httpServletRequest 获取ip
     * @return
     */    @ResponseBody
    @RequestMapping("/check/{tryCode}")
    public R check(HttpServletRequest httpServletRequest, @PathVariable String tryCode) {
        String ip = httpServletRequest.getRemoteAddr();
        logger.info("ip:" + ip + ",tryCode = " + tryCode);
        // 从 Redis 中校验
        String rightCode = redisTemplate.opsForValue().get(ip);
        if (rightCode != null && rightCode.equals(tryCode)) {
            return R.ok("校验成功", rightCode);
        }
        return R.error("校验失败");
    }
}  

(3)效果

四、其他

近年来,以往的这一套防刷机制,还是有风险的,比如爬虫可以先把图片爬取下来,通过图片识别API识别出里边 code,再进行操作,依旧是可以通过机器对系统制造垃圾数据。

目前主流防刷机制有手滑、拼图等。

本文相关代码已经上传至 GitHub了:

前端:

后端:

文章来源:智云一二三科技

文章标题:前后端分离模式下验证码实现

文章地址:https://www.zhihuclub.com/201074.shtml

关于作者: 智云科技

热门文章

网站地图