基于react的滑动图片验证码组件

更新日期: 2019-02-25阅读: 3078标签: 验证
业务需求,需要在系统登陆的时候,使用“滑动图片验证码”,来验证操作的不是机器人。  

效果图


使用方式

在一般的页面组件引用即可。onReload这个函数一般是用来请求后台图片的。

class App extends Component {
    state = {
        url: ""
    }

    componentDidMount() {
        this.setState({ url: getImage() })
    }

    onReload = () => {
        this.setState({ url: getImage() })
    }
    render() {
        return (
            <div>
                <ImageCode
                    imageUrl={this.state.url}
                    onReload={this.onReload}
                    onMatch={() => {
                        console.log("code is match")
                    }}
                />
            </div>
        )
    }
}

代码

// index.js
/**
 * @name ImageCode
 * @desc 滑动拼图验证
 * @author darcrand
 * @version 2019-02-26
 *
 * @param {String} imageUrl 图片的路径
 * @param {Number} imageWidth 展示图片的宽带
 * @param {Number} imageHeight 展示图片的高带
 * @param {Number} fragmentSize 滑动图片的尺寸
 * @param {Function} onReload 当点击'重新验证'时执行的函数
 * @param {Function} onMath 匹配成功时执行的函数
 * @param {Function} onError 匹配失败时执行的函数
 */

import react from "react"

import "./styles.css"

const icoSuccess = require("./icons/success.png")
const icoError = require("./icons/error.png")
const icoReload = require("./icons/reload.png")
const icoSlider = require("./icons/slider.png")

const STATUS_LOADING = 0 // 还没有图片
const STATUS_READY = 1 // 图片渲染完成,可以开始滑动
const STATUS_MATCH = 2 // 图片位置匹配成功
const STATUS_ERROR = 3 // 图片位置匹配失败

const arrTips = [{ ico: icoSuccess, text: "匹配成功" }, { ico: icoError, text: "匹配失败" }]

// 生成裁剪路径
function createClipPath(ctx, size = 100, styleIndex = 0) {
    const styles = [
        [0, 0, 0, 0],
        [0, 0, 0, 1],
        [0, 0, 1, 0],
        [0, 0, 1, 1],
        [0, 1, 0, 0],
        [0, 1, 0, 1],
        [0, 1, 1, 0],
        [0, 1, 1, 1],
        [1, 0, 0, 0],
        [1, 0, 0, 1],
        [1, 0, 1, 0],
        [1, 0, 1, 1],
        [1, 1, 0, 0],
        [1, 1, 0, 1],
        [1, 1, 1, 0],
        [1, 1, 1, 1]
    ]
    const style = styles[styleIndex]

    const r = 0.1 * size
    ctx.save()
    ctx.beginPath()
    // left
    ctx.moveTo(r, r)
    ctx.lineTo(r, 0.5 * size - r)
    ctx.arc(r, 0.5 * size, r, 1.5 * Math.PI, 0.5 * Math.PI, style[0])
    ctx.lineTo(r, size - r)
    // bottom
    ctx.lineTo(0.5 * size - r, size - r)
    ctx.arc(0.5 * size, size - r, r, Math.PI, 0, style[1])
    ctx.lineTo(size - r, size - r)
    // right
    ctx.lineTo(size - r, 0.5 * size + r)
    ctx.arc(size - r, 0.5 * size, r, 0.5 * Math.PI, 1.5 * Math.PI, style[2])
    ctx.lineTo(size - r, r)
    // top
    ctx.lineTo(0.5 * size + r, r)
    ctx.arc(0.5 * size, r, r, 0, Math.PI, style[3])
    ctx.lineTo(r, r)

    ctx.clip()
    ctx.closePath()
}

class ImageCode extends React.Component {
    static defaultProps = {
        imageUrl: "",
        imageWidth: 500,
        imageHeight: 300,
        fragmentSize: 80,
        onReload: () => {},
        onMatch: () => {},
        onError: () => {}
    }

    state = {
        isMovable: false,
        offsetX: 0, //图片截取的x
        offsetY: 0, //图片截取的y
        startX: 0, // 开始滑动的 x
        oldX: 0,
        currX: 0, // 滑块当前 x,
        status: STATUS_LOADING,
        showTips: false,
        tipsIndex: 0
    }

    componentDidUpdate(prevProps) {
        // 当父组件传入新的图片后,开始渲染
        if (!!this.props.imageUrl && prevProps.imageUrl !== this.props.imageUrl) {
            this.renderImage()
        }
    }

    renderImage = () => {
        // 初始化状态
        this.setState({ status: STATUS_LOADING })

        // 创建一个图片对象,主要用于canvas.context.drawImage()
        const objImage = new Image()

        objImage.addEventListener("load", () => {
            const { imageWidth, imageHeight, fragmentSize } = this.props

            // 先获取两个ctx
            const ctxShadow = this.refs.shadowCanvas.getContext("2d")
            const ctxFragment = this.refs.fragmentCanvas.getContext("2d")

            // 让两个ctx拥有同样的裁剪路径(可滑动小块的轮廓)
            const styleIndex = Math.floor(Math.random() * 16)
            createClipPath(ctxShadow, fragmentSize, styleIndex)
            createClipPath(ctxFragment, fragmentSize, styleIndex)

            // 随机生成裁剪图片的开始坐标
            const clipX = Math.floor(fragmentSize + (imageWidth - 2 * fragmentSize) * Math.random())
            const clipY = Math.floor((imageHeight - fragmentSize) * Math.random())

            // 让小块绘制出被裁剪的部分
            ctxFragment.drawImage(objImage, clipX, clipY, fragmentSize, fragmentSize, 0, 0, fragmentSize, fragmentSize)

            // 让阴影canvas带上阴影效果
            ctxShadow.fillStyle = "rgba(0, 0, 0, 0.5)"
            ctxShadow.fill()

            // 恢复画布状态
            ctxShadow.restore()
            ctxFragment.restore()

            // 设置裁剪小块的位置
            this.setState({ offsetX: clipX, offsetY: clipY })

            // 修改状态
            this.setState({ status: STATUS_READY })
        })

        objImage.src = this.props.imageUrl
    }

    onMoveStart = e => {
        if (this.state.status !== STATUS_READY) {
            return
        }

        // 记录滑动开始时的绝对坐标x
        this.setState({ isMovable: true, startX: e.clientX })
    }

    onMoving = e => {
        if (this.state.status !== STATUS_READY || !this.state.isMovable) {
            return
        }
        const distance = e.clientX - this.state.startX
        let currX = this.state.oldX + distance

        const minX = 0
        const maxX = this.props.imageWidth - this.props.fragmentSize
        currX = currX < minX ? 0 : currX > maxX ? maxX : currX

        this.setState({ currX })
    }

    onMoveEnd = () => {
        if (this.state.status !== STATUS_READY || !this.state.isMovable) {
            return
        }
        // 将旧的固定坐标x更新
        this.setState(pre => ({ isMovable: false, oldX: pre.currX }))

        const isMatch = Math.abs(this.state.currX - this.state.offsetX) < 5
        if (isMatch) {
            this.setState(pre => ({ status: STATUS_MATCH, currX: pre.offsetX }), this.onShowTips)
            this.props.onMatch()
        } else {
            this.setState({ status: STATUS_ERROR }, () => {
                this.onReset()
                this.onShowTips()
            })
            this.props.onError()
        }
    }

    onReset = () => {
        const timer = setTimeout(() => {
            this.setState({ oldX: 0, currX: 0, status: STATUS_READY })
            clearTimeout(timer)
        }, 1000)
    }

    onReload = () => {
        if (this.state.status !== STATUS_READY && this.state.status !== STATUS_MATCH) {
            return
        }
        const ctxShadow = this.refs.shadowCanvas.getContext("2d")
        const ctxFragment = this.refs.fragmentCanvas.getContext("2d")

        // 清空画布
        ctxShadow.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize)
        ctxFragment.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize)

        this.setState(
            {
                isMovable: false,
                offsetX: 0, //图片截取的x
                offsetY: 0, //图片截取的y
                startX: 0, // 开始滑动的 x
                oldX: 0,
                currX: 0, // 滑块当前 x,
                status: STATUS_LOADING
            },
            this.props.onReload
        )
    }

    onShowTips = () => {
        if (this.state.showTips) {
            return
        }

        const tipsIndex = this.state.status === STATUS_MATCH ? 0 : 1
        this.setState({ showTips: true, tipsIndex })
        const timer = setTimeout(() => {
            this.setState({ showTips: false })
            clearTimeout(timer)
        }, 2000)
    }

    render() {
        const { imageUrl, imageWidth, imageHeight, fragmentSize } = this.props
        const { offsetX, offsetY, currX, showTips, tipsIndex } = this.state
        const tips = arrTips[tipsIndex]

        return (
            <div className="image-code" style={{ width: imageWidth }}>
                <div className="image-container" style={{ height: imageHeight, backgroundImage: `url("${imageUrl}")` }}>
                    <canvas
                        ref="shadowCanvas"
                        className="canvas"
                        width={fragmentSize}
                        height={fragmentSize}
                        style={{ left: offsetX + "px", top: offsetY + "px" }}
                    />
                    <canvas
                        ref="fragmentCanvas"
                        className="canvas"
                        width={fragmentSize}
                        height={fragmentSize}
                        style={{ top: offsetY + "px", left: currX + "px" }}
                    />

                    <div className={showTips ? "tips-container--active" : "tips-container"}>
                        <i className="tips-ico" style={{ backgroundImage: `url("${tips.ico}")` }} />
                        <span className="tips-text">{tips.text}</span>
                    </div>
                </div>

                <div className="reload-container">
                    <div className="reload-wrapper" onClick={this.onReload}>
                        <i className="reload-ico" style={{ backgroundImage: `url("${icoReload}")` }} />
                        <span className="reload-tips">刷新验证</span>
                    </div>
                </div>

                <div className="slider-wrpper" onMouseMove={this.onMoving} onMouseLeave={this.onMoveEnd}>
                    <div className="slider-bar">按住滑块,拖动完成拼图</div>
                    <div
                        className="slider-button"
                        onMouseDown={this.onMoveStart}
                        onMouseUp={this.onMoveEnd}
                        style={{ left: currX + "px", backgroundImage: `url("${icoSlider}")` }}
                    />
                </div>
            </div>
        )
    }
}

export default ImageCode
// styles.css

.image-code {
    padding: 10px;
    user-select: none;
}

.image-container {
    position: relative;
    background-color: #ddd;
}

.canvas {
    position: absolute;
    top: 0;
    left: 0;
}

.reload-container {
    margin: 20px 0;
}

.reload-wrapper {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}

.reload-ico {
    width: 20px;
    height: 20px;
    margin-right: 10px;
    background: center/cover no-repeat;
}

.reload-tips {
    font-size: 14px;
    color: #666;
}

.slider-wrpper {
    position: relative;
    margin: 10px 0;
}

.slider-bar {
    padding: 10px;
    font-size: 14px;
    text-align: center;
    color: #999;
    background-color: #ddd;
}

.slider-button {
    position: absolute;
    top: 50%;
    left: 0;
    width: 50px;
    height: 50px;
    border-radius: 25px;
    transform: translateY(-50%);
    cursor: pointer;
    background: #fff center/80% 80% no-repeat;
    box-shadow: 0 2px 10px 0 #333;
}

/* 提示信息 */
.tips-container,
.tips-container--active {
    position: absolute;
    top: 50%;
    left: 50%;
    display: flex;
    align-items: center;
    padding: 10px;
    transform: translate(-50%, -50%);
    transition: all 0.25s;
    background: #fff;
    border-radius: 5px;

    visibility: hidden;
    opacity: 0;
}

.tips-container--active {
    visibility: visible;
    opacity: 1;
}

.tips-ico {
    width: 20px;
    height: 20px;
    margin-right: 10px;
    background: center/cover no-repeat;
}

.tips-text {
    color: #666;
}

链接: https://www.fly63.com/article/detial/2132

Validate表单验证插件之常用参数介绍

Validate常用的一些参数和方法:errorElement,errorClass,errorPlacement,errorLabelContainer,errorContainer,wrapper,success,debug

vue短信验证性能优化写入localstorage中

当点击完按钮时,倒计时还没到60s过完时,刷新浏览器,验证码按钮又可以重新点击;就需要把时间都写到localstorage里面去,当打开页面的时候,就去localstorage里面去取,我这里就贴上我的解决方法,因为前几天有个vue的项目用到该方法

登陆时短信验证码的原理,实现

陆时需要发送短信验证码或者其他的验证方式来校验是否是本人操作,达到安全性的目的。点击获取验证码时,倒计时,暂时设定倒计时的时间为180秒....

网页如何实现拼图滑块的验证码_纯js的实现

滑动解锁应该是有两张图片,一张正常的,一张上面有解锁区域的(后端给),然后前端只用把用户释放鼠标后,滑动模块在图片上的xy轴传给后端,后端做成功与否的判断。如果只是纯前端js验证,不具备高安全性。

风火云短信验证码接收平台, 接码快速,平台稳定,简单易学

目前的社会发展得非常快,互联网产业的发展更是让很多人惊奇,平台越来越多也导致了搭载程序的要求必须高,服务平台完善性还体现于短信验证码是否具备,只有在具备的前提下才能有着更大的保证

验证码的分类_ 网页验证码有哪些方式?

早期的互联网是没有验证码的,随着后来计算机程序的发展,黑客编写了模仿登录、恶意破解密码、刷票、论坛灌水等恶意程序,破坏了整个网络的平衡性。介绍目前常用验证码的分类有哪些:Gif动画验证码、手机短信验证码、手机语音验证码、视频验证码、滑动验证码

滑动验证码原理实现

滑动验证码在很多网站逐步流行起来,一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。常见验证码是需要输入图中字符的,是因为机器识别字符比较困难,以此来防止机器自动的行为。

Vue中的验证登录状态

Vue项目中实现用户登录及token验证,先说一下我的实现步骤:使用easy-mock新建登录接口,模拟用户数据;使用axios请求登录接口,匹配账号和密码,账号密码验证后, 拿到token,将token存储到sessionStorage中,并跳转到首页

canvas实现随机验证码

canvas生成背景图和文字 设置字体样式和大小,String的fromCharCode(code码)生成大小写字母和数字 str.toLowerCase()转小写,随机抽取不重复的6位数字组成验证码字符串

javascript如何验证是否为11位有效电话号码?

JavaScript中可以使用正则表达式判断是否为11为有效电话号码:这个表达式的意思是:1--以1为开头;2--第二位可为3,4,5,7,8,中的任意一位;3--最后以0-9的9个整数结尾。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!