vue中使用验证码
1、思路分析
后端分析:
- nodejs的第三方express 开启服务器
- 数据接口 —— 包括登录接口、图形验证接口
- 图形验证码的处理封装
- 前后端跨域处理(这里处理的是vue和静态资源服务器端口不同导致验证码跨域问题)
前端分析:
- vue框架 登录视图页面
- axios 发送请求(这里是封装了axios) 获取数据
2、npm安装必要的包
npm i express 开启静态资源服务器
npm i mongodb 连接数据库
npm i express-session 会话session
npm i svg-captcha 图形验证码
npm i jsonwebtoken 用于令牌的token
注意以下以 nodejs 和 mongodb 来后端和数据库开发使用,以 nodejs 的第三方 express 开启静态资源服务器使用
3、express开启服务器和封装必要的请求函数
server.js
const express = require('express');
const {
PORT
} = require('./config.json');
const rootRouter = require('./router');
const path = require('path')
//创建一个服务器
const app = express();
// ./ 是当前目录
// / 是根目录
// 启用静态资源服务器
app.use(express.static(path.join(__dirname, './public'), {
// maxAge:60*60*1000*24
}));
// 数据接口
app.use('/api', rootRouter);
app.listen(PORT, () => {
console.log('server is running on port ' + PORT)
})
router文件下的index.js
const {
Router,
urlencoded,
json
} = require('express');
// express.json===bodyParse.json, ....
const session = require('express-session')
const token = require('../utils/token');
const cors = require('../utils/cors')
const router = Router();
const userRouter = require('./user');
const goodsRouter = require('./goods');
const regRouter = require('./reg');
const loginRouter = require('./login');
const vcodeRouter = require('./vcode');
const uploadRouter = require('./upload');
const {
formatData
} = require('../utils/tools');
// 跨域请求处理
router.use(cors)
// 数据格式化中间件
router.use(urlencoded({
extended: false
}), json())
// 使用session会话
// 通过req.session获取存入会话的数据
router.use(session({
secret: 'fqniu',
resave: false,
saveUninitialized: true,
cookie: {
// 设置cookie有效期
maxAge: 1000 * 60 * 60 * 2
}
}))
// /api/user
router.use('/user', userRouter);
// /api/goods
router.use('/goods', goodsRouter);
// 注册
router.use('/reg', regRouter);
// 登录
router.use('/login', loginRouter);
// 上传
router.use('/upload', uploadRouter);
// 校验token
router.get('/jwtverify', (req, res) => {
const {
authorization
} = req.query;
console.log('test', authorization)
// verify方法校验成功:得到一个对象
// verify方法校验不通过:直接抛出错误
// try{
// var decoded = jwt.verify(authorization, 'laoxie');
// res.send(formatData())
// }catch(err){
// res.send(formatData({code:0}))
// }
if (token.verify(authorization)) {
res.send(formatData())
} else {
res.send(formatData({
code: 0
}))
}
});
// 验证码
router.use('/vcode', vcodeRouter);
module.exports = router;
router文件下的mongodb.js
/**
* MongoDB操作封装
*/
const {
MongoClient,
ObjectId
} = require('mongodb');
// mongodb数据库地址
const url = 'mongodb://localhost:27017';
// 数据库名称
const dbName = 'user';
async function connect() {
const client = await MongoClient.connect(url);
const db = client.db(dbName);
return {
client,
db
}
}
// 增
async function insert(colName, data) {
// 1. 连接数据库
const {
db,
client
} = await connect();
// 2. 添加数据
// 根据传入的集合名称获取数据库中的某个集合
const collection = db.collection(colName);
const result = await collection[Array.isArray(data) ? 'insertMany' : 'insertOne'](data)
// 3. 关闭连接
client.close()
return result;
}
// 删
async function remove(colName, query) { // query{_id:'5c128cdbd1233ce12c878a32'}
const {
db,
client
} = await connect();
if (query._id && typeof query._id === 'string') {
query._id = ObjectId(query._id);
}
const collection = db.collection(colName);
const result = await collection.deleteMany(query);
client.close();
return result;
}
// 改
async function update(colName, query, newData) { // newData{$set:{price:200,qty:2},$inc:{view:1}}
const {
db,
client
} = await connect();
const collection = db.collection(colName);
if (query._id && typeof query._id === 'string') {
query._id = ObjectId(query._id);
}
const result = await collection.updateMany(query, newData);
return result;
}
// 查 query 查询语句
async function find(colName, query = {}, options = {}) { // options={litmit:10,skip:0}
const {
client,
db
} = await connect();
// console.log("query=", query)
const collection = db.collection(colName);
if (query._id && typeof query._id === 'string') {
query._id = ObjectId(query._id);
}
// // 查询多少条数据
let datalength = await collection.count().then(data => data)
// console.log("数据长度=", datalength) // 50
// 添加条数
let total = datalength
// console.log("这个1=", result)
// 如果存在query查询到的 name 则执行下面语句
if (query.name) {
const font = query.name
let result = collection.find({
'name': {
'$regex': font,
// "$options": 'i'
}
});
result = await result.toArray();
client.close();
return {
result,
total
}
}
// 查询到数据集合
let result = collection.find(query); // 50->10
// 判断是否要跳过记录
if (options.skip) {
result = result.skip(options.skip)
}
if (options.limit) {
result = result.limit(options.limit);
}
// 排序
// console.log('sort',options.sort);
if (options.sort) { //['price'],['price','1']
let key, val;
key = options.sort[0];
if (options.sort.length > 1) {
val = options.sort[1] * 1;
} else {
val = -1;
}
result = result.sort({
[key]: val
})
}
result = await result.toArray();
// console.log(result)
client.close();
// 把数据和数据长度返回出去
return {
result,
total
}
}
module.exports = {
insert,
remove,
update,
find
}
router文件下的vcode.js
const express = require('express');
const router = express.Router();
const svgCaptcha = require('svg-captcha');
const {
formatData
} = require('../utils/tools');
// 生成验证码
router.get('/', async (req, res) => {
// 生成图像验证码:svg-captcha
const options = {
// size: 10,
noise: 3,
ignoreChars: '0o1il',
background: '#58bc58',
color: true,
fontSize: 50,
height: 54
}
// 验证码在这里生成
const captcha = svgCaptcha.create(options); // {data:'<svg/>',text:'abcd'}
// console.log('vcode.session=',req.session);
// 把验证码存入会话Session
req.session.vcode = captcha.text.toLowerCase();
res.send(formatData({
data: captcha.data
}));
})
module.exports = router;
router文件下的token.js
const jwt = require('jsonwebtoken');
const privateKey = 'fqniu';
function create(data = {}, expiresIn = '2h') {
const token = jwt.sign({
...data
}, privateKey, {
// token有效期
expiresIn
});
return token;
}
function verify(token) {
let result;
try {
jwt.verify(token, privateKey);
result = true;
} catch (err) {
result = false
}
return result;
}
module.exports = {
create,
verify
}
router文件下的login.js
const express = require('express');
const router = express.Router();
// 引入 封装的 token 模块
const token = require('../utils/token');
const {
formatData,
md5
} = require('../utils/tools');
const mongo = require('../utils/mongo');
// 登录 get请求
router.get('/', async (req, res) => {
let { username, password, vcode, mdl } = req.query;
// 其中vcode是前端输入的验证码
// 从会话中获取验证码
// 校验验证码 这里的req.session 是已经存有vcode的 Session 对象
// console.log('login.session=', req.session)
// vcode 是前端输入验证码之后,传过来的参数
// 而 req.session.vcode 是Session中的存的 vcode
if (vcode !== req.session.vcode) {
res.send(formatData({ code: 10 }))
return;
}
// 加密后进行查询
// password = md5(password)
let {result} = await mongo.find('user', { username, password }); //[{}]
console.log("result=",result)
// 判断如果 find 找到 有result, 则返回这个数组数据
// 如果find 找不到 ,则result为 空数组
if (result.length > 0) {
// 用户名、密码、验证码都校验通过后,判断是否有免登陆选项
// console.log('req.query=', req.query);
let authorization;
if (mdl === 'true') {
// token的操作
// 1. 生成token
// const token = jwt.sign({ username }, 'laoxie' ,{
// // token有效期
// expiresIn: 20//1000 * 60 * 60 * 24 * 7
// });
//封装的 token 函数
authorization = token.create({ username }, '7d')
}else{
authorization = token.create({ username })
}
// console.log('token=', authorization);
result = result[0];
result.authorization = authorization
res.send(formatData({ data: result }));
} else {
res.send(formatData({ code: 0 }))
}
})
module.exports = router;
router文件下的cors.js
用于跨域处理
const allow_origin = ['localhost:3000', 'www.xxxxxx.com']
function cors(req, res, next) {
// 设置响应头
// Access-Control-Allow-Origin
// Access-Control-Allow-Methods
// Access-Control-Allow-Headers
// res.header("Access-Control-Allow-Origin", "*");
// res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
// res.header("Access-Control-Allow-Methods","PUT,POST,GET,PATCH,DELETE,OPTIONS");
// console.log('Origin:',req.get('host'));
// 获取请求者的域名
if (allow_origin.includes(req.get('host'))) {
res.set({
"Access-Control-Allow-Origin": "http://localhost:8080",
"Access-Control-Allow-Headers": "Content-Type,Content-Length, Authorization, Accept,X-Requested-With",
"Access-Control-Allow-Methods": "PUT,POST,GET,PATCH,DELETE,OPTIONS",
"Access-Control-Allow-Credentials": true
})
// 跨域请求CORS中的预请求
if (req.method == "OPTIONS") {
res.sendStatus(200); /*让options请求快速返回*/
} else {
next();
}
} else {
res.send(401);
}
}
module.exports = cors;
router文件下的tools.js
用于对请求数据formatData的封装
const crypto = require('crypto');
function formatData({code=1, data=[], msg='success'}={}){
if(code === 0){
msg = 'fail';
}
return {
code,
data,
msg
}
}
// 封装加密函数
function md5(data,privateKey='fqniu'){
const hash = crypto.createHash('md5');
hash.update(data + privateKey); // 加盐 盐值
const result = hash.digest('hex');
return result;
}
module.exports = {
formatData,
md5
}
4、前端准备工作
<template>
<div class="login-wrap">
<el-form class="login-form" :model="ruleForm" status-icon ref="ruleForm" label-width="100px">
<h1>登录账号</h1>
<el-form-item label="用户名" prop="username">
<el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="验证码" class="vcode-input">
<el-input placeholder="请输入验证码" v-model="ruleForm.vcode">
<!-- 验证码html结构 -->
<template v-slot:append>
<div v-html="vcodeSvg" @click="vcode()" class="vcode"></div>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="submitForm()">登录</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="gotoLogin">立即注册</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
// from表单 参数 需要发请求到后端进行处理, 包括用户名、密码、验证码
ruleForm: {
username: "admin",
password: "123456",
vcode: "",
},
// 定义变量接收请求后的图形验证码
vcodeSvg: "",
};
},
methods: {
// 登录请求
async submitForm() {
const { data } = await this.$request.get("/login", {
// 因为后端需要 query 来接收参数,所以这里需要传入params { 里面放出去的参数 }
// 这是对象需要结构
params: {
...this.ruleForm,
},
});
console.log(data);
if (data.code === 1) {
// 登录成功
// 保存token
localStorage.setItem("token", data.data.authorization);
this.$message({
type: "success",
message: "登录成功",
});
// 登录成功跳转到home
this.$router.push("/home");
} else if (data.code === 10) {
// 登录失败
this.$message({
type: "error",
message: "验证码有误",
});
} else {
this.$message({
type: "error",
message: "账号和密码有误",
});
}
},
// 重置表单数据
resetForm() {
this.ruleForm.username = "";
this.ruleForm.password = "";
},
// 点击注册按钮, 跳转到注册
gotoLogin() {
this.$router.push("/reg");
},
// 点击更新验证码
async vcode() {
const { data } = await this.$request.get("/vcode");
// console.log(data);
this.vcodeSvg = data.data;
},
},
// vue生命周期中created函数, 一加载页面时就发请求 显示图形验证码 并把返回的数据渲染到页面上
// 因为后端返回的是图形验证码的html
created() {
this.vcode();
},
};
</script>
<style>
.login-wrap {
height: 100%;
background: #ccc;
display: flex;
justify-content: center;
align-items: center;
}
.login-form {
width: 520px;
padding: 20px;
background: #fff;
border-radius: 10px;
}
.el-input .el-input-group__append {
padding: 0;
border: 0;
}
.vcode svg {
width: 110px;
height: 40px;
}
</style>
效果图:
5、遇到问题
但是虽然验证码获取数据渲染页面成功,发送登录请求,但是后端却返回验证码有误,经过多次验证和查资料,才发现的问题如下:
问题如图:
5、问题处理
这里是处理图形验证码后端session中获取不到的问题(跨域)也就是 跨域导致 set-cookie 无效
推荐博客跨域set-cookie无效
后端处理cors跨域:
前端处理axios发送请求
还有一种是前后端已经设置了,但是还是拿不到vcode怎么办?
最终发现是浏览器因为安全策略给禁用了cookie,比如谷歌的 chrome://flags/ 里面的设置