图形验证码的必要性
图形验证码是验证码的一种,有防止黑客对某一特定注册用户用程序暴力破解私人信息、恶意破解密码、刷票、论坛灌水的作用。
图形验证码是一种区分用户是计算机还是人的公共全自动程序。验证码是现在很多网站通行的方式,由计算机生成并评判,但是只有人类才能解答。
在常用的网站业务中我们不难看出很多登录注册业务上都采用了图形验证码的方式。今天记录一个图形验证码以后端的方式实现的逻辑。
实现效果
在登录注册业务上集成图片中验证码后的是效果如下图所示:
工具说明
这里主要推荐Hutool工具中的captcha包中的图形验证码来实现。
验证码功能位于cn.hutool.captcha包中,核心接口为ICaptcha,此接口定义了以下方法:
createCode 创建验证码,实现类需同时生成随机验证码字符串和验证码图片
getCode 获取验证码的文字内容
verify 验证验证码是否正确,建议忽略大小写
write 将验证码写出到目标流中
其中write方法只有一个OutputStream,ICaptcha实现类可以根据这个方法封装写出到文件等方法。
AbstractCaptcha为一个ICaptcha抽象实现类,此类实现了验证码文本生成、非大小写敏感的验证、写出到流和文件等方法,通过继承此抽象类只需实现createImage方法定义图形生成规则即可。
生成方式可参考Hutool官网给定的方式:
LineCaptcha 线段干扰的验证码
生成效果大致如下:
//定义图形验证码的长和宽LineCaptcha lineCaptcha =CaptchaUtil.createLineCaptcha(200,100);//图形验证码写出,可以写出到文件,也可以写出到流
lineCaptcha.write("d:/line.png");//输出codeConsole.log(lineCaptcha.getCode());//验证图形验证码的有效性,返回boolean值
lineCaptcha.verify("1234");//重新生成验证码
lineCaptcha.createCode();
lineCaptcha.write("d:/line.png");//新的验证码Console.log(lineCaptcha.getCode());//验证图形验证码的有效性,返回boolean值
lineCaptcha.verify("1234");
可以看出,这种方式特别简便,大部分实现逻辑已经被封装在工具类内部,并不需要我们了解具体的实现。并且可以验证图形中的验证码信息,非常方便该功能在登陆注册等业务上的验证码校验。
但是需要注意的是,这种方式一般用于输出一个png图片,如果后端处理生成图片后怎么在前端界面上展示也是我们需要考虑的问题。
思路说明
不过Hutool也给出了另外的输入输出流的方式,因此我们可以在后端利用前端的请求,调用图形验证码生成逻辑相应的图形输出流,然后在前端通过指定的流解析,将图形验证码展示在前端界面上。另外在处理逻辑上,在生成图片输出流的同时去获取图心中的验证码并将验证码给信息存入Redis缓存中,当用户在登陆或注册模块中输入相应信息时,将前端界面的数据代入后端一起校验,如果某个值有错,将对应的错误信息反馈给用户。
实现方法
1,导入依赖
首先需要导入Hutool的工具包,至于项目构建的SpringBoot依赖并不在这里一一列举。这里主要是需要使用captcha的依赖,怕麻烦的也可以将Hutool所有的项目依赖hutool-all直接导入:
<!-- hutool工具依赖-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.19</version>
</dependency>
依赖加载完毕后,就可以使用CaptchaUtil这个工具类了。
2,构建业务接口
然后就通过工具类构建图形验证码构建接口
/**
* 获取验证码
*
* @return
*/
@GetMapping("getCode")
public Result<Map<String, String>> getCode() {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
String verify = IdUtil.simpleUUID();
//图形验证码写出,可以写出到文件,也可以写出到流
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
lineCaptcha.write(os);
String code = lineCaptcha.getCode();
// 缓存一分钟的验证码
redisTemplate.opsForValue().set(verify, code, Duration.ofMinutes(1));
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(5);
//验证码对应的redis上的uuid
map.put("uuid", verify);
//图片上的验证码
map.put("code", code);
//将图片转换成输出流传到前端上
map.put("img",Base64.encode(os.toByteArray()));
return Result.ok(map);
}
这里我们将图形验证码写入到输出流,并通过Base64进行编码转换,当前端获取后端请求后的数据后,将“img”对应的输出流以"data:image/gif;base64,"的方式解析出来,这样图片就能成功显示在界面上了。
为了方便校验,这里也为每个请求生成的图形验证码指定一个与之对应的随机id,并为其设置一定的缓存时间,以便在业务中校验。这样在前端界面我们每次点击以及验证码图片就相当于向后端请求一次。(所以这只是一种实现方式,但是并不太可取,毕竟大量请求会消耗服务器)。
3,后端登录功能上集成图形验证码
@ApiOperation(value = "管理员登录接口")
@PostMapping("/login")
@PassToken
public Result<? extends Object> login(@RequestBody @Validated LoginDto loginDto, BindingResult result) {
if (result.hasErrors()) {
return Result.fail(null).message("信息输入不正确");
}
//获取uuid
String uuid = loginDto.getUuid();
//获取验证码
String code = loginDto.getCode();
// //验证验证码是否正确
if (!Objects.equals(redisTemplate.opsForValue().get(uuid), code)) {
log.info("判断结果{}",!Objects.equals(redisTemplate.opsForValue().get(uuid), code));
//错误则返回前端并提示
return Result.fail(null).message("验证码错误,请重新验证");
}
//验证成功则清除redis内的验证码缓存
redisTemplate.delete(uuid);
Admin admin = adminService.login(loginDto.getUsername(), loginDto.getPassword());
if (!StringUtils.isEmpty(admin)) {
return Result.ok(admin).message("欢迎管理员~!");
}
return Result.fail(null).message("用户名或密码错误,请重新登录!");
}
当用户登录时,需要在前端输入指定的登录信息,请求登录业务接口后,此时后端首先获取图形验证码的随机id和用户填写的验证码信息,并通过uuid去redis中找到存储缓存的后端生成的验证码信息,并与之对比。如果验证码信息不对,返回给前端提示信息-》"验证码错误,请重新验证",只有通过后即可验证用户的账号密码的准确性并反馈对应的提示信息。
4,部分前端参考
这里也给出前端登录页面的具体信息可供大家参考。
<template>
<div class="login-wrap">
<div class="ms-login">
<div class="title">FP&Net后台管理系统</div>
<el-form
ref="ruleForm"
class="demo-ruleForm"
:model="loginForm"
:rules="rules"
>
<!-- 账号-->
<div style="display: flex">
<el-icon class="icon-mine" style="margin-top: 10px" size="large">
<user-filled/>
</el-icon>
<el-form-item prop="username">
<el-input
size="large"
class="input-wid"
v-model="loginForm.username"
placeholder="请输入用户名"
></el-input>
</el-form-item>
</div>
<!-- 密码-->
<div style="display: flex">
<el-icon class="icon-mine" style="margin-top: 10px">
<Lock/>
</el-icon>
<el-form-item prop="password">
<el-input
size="large"
class="input-wid"
type="password"
placeholder="请输入密码"
v-model="loginForm.password"
@keyup.enter="submitForm('loginForm')"
></el-input>
</el-form-item>
</div>
<!-- 验证码-->
<div style="display: flex">
<el-icon class="icon-mine" style="margin-top: 10px">
<Stamp/>
</el-icon>
<el-form-item prop="code" style="width: 160px">
<el-input
size="large"
class="input-wid"
v-model="loginForm.code"
placeholder="点击图片刷新"
></el-input>
</el-form-item>
<div class="w-18px"/>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</div>
<div style="font-size: 18px;margin: 0 10px">
<el-checkbox size="large" v-model="remMe">记住我</el-checkbox>
<el-link type="warning" style="font-size: 14px; line-height: 20px;margin-left: 150px" @click="forget()">
忘记密码?
</el-link>
</div>
<div class="login-btn">
<el-button type="primary" size="large" style="margin: 0 10px" @click="submitForm" @keyup="submitForm">登录</el-button>
<el-button type="warning" size="large" style="margin-left: 100px" @click="handleSignUp()">注册</el-button>
</div>
<p style="font-size: 12px; line-height: 30px; color: #999">
Tips : 请保护好个人信息哦~
</p>
</el-form>
</div>
</div>
</template>
<script>
import Cookies from "js-cookie";
import request from "@/util/postreq";
import {mixin} from "../../mixins/index";
import {UserFilled, Lock, Stamp} from '@element-plus/icons-vue'
export default {
name: "LoginPage",
mixins: [mixin],
components: {
UserFilled,
Lock,
Stamp,
},
data() {
//用户名校验
const validateName = (rule, value, callback) => {
if (!value) {
return callback(new Error("用户名不能为空"));
} else {
callback();
}
};
//验证码校验
const codeRule = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入验证码"));
} else {
callback();
}
}
//密码校验
const validatePassword = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else {
callback();
}
};
return {
wrap: {
flexWrap: "nowrap",
},
codeUrl: "",
remMe: false,
loginForm: {
// 登录用户名密码
username: "",
password: "",
code: "",
uuid: "",
avatar: "",
cid:"",
roleId:""
},
rules: {
username: [
{validator: validateName, message: "用户名不能为空", trigger: "blur"},
],
code: [
{
validator: codeRule,
message: "验证码不能为空",
trigger: "blur",
}
],
password: [
{
validator: validatePassword,
message: "密码不能为空",
trigger: "blur",
},
],
},
};
},
created() {
this.getCode()
this.rememberMes()
},
methods: {
getCode() {
request.get('/getCode').then(res => {
if (res.code === 200) {
this.codeUrl = "data:image/gif;base64," + res.data.img;
this.loginForm.uuid = res.data.uuid;
this.code = res.data.code
}
});
},
rememberMes() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe');
const cid = Cookies.get("cid");
const roleId = Cookies.get("roleId")
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : password,
cid: cid === undefined ? this.loginForm.cid : cid,
roleId: roleId === undefined ? this.loginForm.roleId : roleId,
};
this.remMe = {
remMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
},
submitForm() {
request.post('/login', this.loginForm)
.then((res) => {
console.log(res.data);
if (res.code === 200) {
this.$message({
message: res.message,
type: res.type,
});
this.setUserInfo(res.data)
//cookie存储个人信息
if (this.loginForm.remMe) {
Cookies.set("username", this.loginForm.username, {expires: 30});
Cookies.set("password", this.loginForm.password, {expires: 30});
Cookies.set('rememberMe', this.loginForm.remMe, {expires: 30});
Cookies.set('roleId', this.loginForm.roleId, {expires: 30});
Cookies.set('cid', this.loginForm.cid, {expires: 30});
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
Cookies.remove('cid');
Cookies.remove('roleId');
}
//将个人信息存入vuex
this.setUserInfo(res.data);
setTimeout(() => {
if (res.data != null) {
localStorage.setItem("user", JSON.stringify(res.data)); //存储用户信息到浏览器
this.$router.push("homePage/FlowerEcharts");
}
}, 500);
} else {
this.$notify({
title: res.message,
type: "error",
});
}
})
.catch((error) => {
console.error(error);
});
},
setUserInfo(item) {
this.$store.commit("setUserId", item.id);
this.$store.commit("setUserName", item.username);
this.$store.commit("setUserPic", item.avatar);
this.$store.commit("setNickName", item.nickname);
this.$store.commit("setCid", item.cid);
this.$store.commit("setRoleId", item.roleId);
},
handleSignUp() {
this.$router.push("/register");
},
// 后面添加找回密码功能
forget() {
this.$router.push("/forget");
},
},
};
</script>
<style scoped>
.login-code-img {
height: 38px;
}
.login-code {
width: 33%;
height: 38px;
float: right;
}
img {
cursor: pointer;
vertical-align: middle;
}
/*#captcha {*/
/* width: 200px;*/
/* height: 70px;*/
/* margin: 10px auto;*/
/*}*/
.w-18px {
width: 18px;
}
.title {
margin-bottom: 50px;
text-align: center;
font-size: 30px;
font-weight: 600;
color: rgb(3, 19, 11);
}
.login-wrap {
position: relative;
background: url("../../assets/images/plant.png") fixed center;
background-size: cover;
width: 100%;
height: 100%;
}
.input-wid {
width: 250px;
}
.icon-mine {
margin-right: 5px;
}
/*.ms-title {*/
/* position: absolute;*/
/* top: 50%;*/
/* width: 100%;*/
/* margin-top: -230px;*/
/* text-align: center;*/
/* font-size: 30px;*/
/* font-weight: 600;*/
/* color: rgb(240, 241, 231);*/
/*}*/
.ms-login {
position: absolute;
left: 50%;
top: 30%;
width: 320px;
height: 380px;
margin: -150px 0 0 -190px;
padding: 40px;
border-radius: 5px;
background: #fff;
}
.login-btn {
text-align: center;
display: flex;
}
.login-btn button {
height: 36px;
width: 100%;
}
</style>
前端主要是采用vue框架搭配Axios使用实现前后端请求数据交互,界面利用ElementPlus搭建,这里不做详述。重点是处理请求后,将验证码的图形输出流在前端以字符拼接的形式进行处理。
request.get('/getCode').then(res => {
if (res.code === 200) {
//处理Base64编码后的图片输出流
this.codeUrl = "data:image/gif;base64," + res.data.img;
this.loginForm.uuid = res.data.uuid;
this.code = res.data.code
}
});
至此,图形验证码功能就实现了。因为主要是通过后端实现的,因此可能存在一定的性能消耗,仅作为一个学习参考。