实际开发过程中经常遇到要实现图片验证码来防止外部使用脚本刷接口,所以说图片验证码是很有必要的一个小功能。
html
<!--- 注册页面整增加图形验证码功能,这里为了更贴近企业级业务,我们在注册页面整增加图形验证码功能-->
<div class="user-phone">
<label for="imageCode"><i class="am-icon-check am-icon-sm"></i></label>
<!--这里v-model="phoneUserForm.imageCode" 双向绑定 用户根据图形验证码输入的验证码 style="width: 180px;"设置文本的输入长度 -->
<input type="text" name="" style="width: 140px;" v-model="phoneUserForm.base64ImageCode"
id="imageCode" placeholder="图片验证码">
<!--@click="getImageCode":src="base64ImageCode" 点击发送axios异步请求到后端,获取到图片过后直接显示在imageCode-->
<!--@click="getImageCode" vue语法 ==v-on:click-->
<img id="captcha-image-temp" @click="getImageCode" :src="base64ImageCode"
class="captcha-image-temp" alt="点击换图" title="点击换图"
style="vertical-align: middle; cursor: pointer;">
<button type="button" style="font-size: 10px;color: purple" @click="getImageCode">看不清
</button>
</div>
模型数据
new Vue({
data: { /*模型数据*/
/**
* 图形验证码的两个参数
* */
base64ImageCode: "", // 图片验证码的图片 获取后端经过Base64编码加密后的string字符串,获取到了就会显示在前端,后端传递就会显示
imageCodePrefix: "data:image/jpeg;base64,", // 图形验证码显示的前缀,需要加上这个才能显示经过base64编译加密后的String在前端
/**
* 手机号注册
* */
phoneUserForm: {
base64ImageCode: "", // 点击获取【手机验证码】按钮的时候将 用户输入的图片验证码传递到后端
phoneNum: "15555555555", // 用户输入的手机号
imageCodeKey: "", // 存储图形验证码的key 即传递到后端的UUID,点击获取验证码的时候进行赋值 【唯一的--避免产生大量无用的验证码】,后端redis将其当作key 生成的图形验证码为value
password: "", // 页面输入的密码
},
})
methods
methods: {
/**
* 获取UUID,为了获取图片验证码,不会产生大量无用的图片验证码,放置恶意攻击
* */
createUuid() { // 获取UUID 通用唯一识别码(Universally Unique Identifier)
var s = [];
var hexDigits = "0123456789abcdefghi";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
},
/**
* 获取图片验证码
* 页面一加载就会产生UUID-key执行该方法发送异步请求并携带UUID去后端获取图片验证码
* */
getImageCode() {
// 通过localStorage【存储key和value-UUID】通过自定义的键registerImageCodeKey 去获取是否有imageCodeKey【即UUID】
// 这个key【registerImageCodeKey】其实一开始是没有,对应的imageCodeKey【UUID】也是没有的。是在下面判断后设定的。
let imageCodeKey = localStorage.getItem("registerImageCodeKey");
// 如果为false获取这为空字符串就创建UUID
if (!imageCodeKey) {
// 调用方法创建一个imageCodeKey【UUID】,这里才是真正的创建UUID,一开始是没有的
imageCodeKey = this.createUuid();
console.log("前端生成的UUID:" + imageCodeKey); // 判断是否生成UUID*/
// 将UUID通过键值对的方式存储到localStorage中 以后通过registerImageCodeKey 去获取属性值imageCodeKey【UUID】
localStorage.setItem("registerImageCodeKey", imageCodeKey);
}
/** 前端页面一加载就发送异步请求去后端 携带imageCodeKey【UUID】
* 这里传入的【imageCodeKey】UUID会作为Redis的key进行存储,value就是生成的图形验证码经过base64编码加密后的String
* Redis采用键值对的方式进行存储数据在内存中
*必须传入到后端,为了避免产生大量无用的验证码,携带过去直接就可以进行覆盖 */
this.$http.get("/verifyCode/image/" + imageCodeKey).then(res => {
// 直接向后端发送请求获取图形验证码的字符串 并进行拼接,赋值给imageCode
// res.data.resultObj 就是 AjaxResult对象中的属性resultObj 即为图像验证码加密后的String
// 获取到图形验证码 显示在前端
this.base64ImageCode = this.imageCodePrefix + res.data.resultObj;
})
},
}
mounted
如果需要页面一加载就发送请求到后端获取图片验证码,就可以使用mounted钩子函数
//页面加载事件
mounted() { /*钩子函数*/
this.getImageCode(); // 这个再点击重新获取验证码时也会触发
}
后端
Controller
/**
* I 注解注入一个对象
*/
@Autowired
private IVerifyCodeService iVerifyCodeService;
@RequestMapping("/verifyCode")
public class VerifyCodeController { /**
* I 注解注入一个对象
*/
@Autowired
private IVerifyCodeService iVerifyCodeService;
/**
* I 注解注入一个对象
*/
@Autowired
private IVerifyCodeService iVerifyCodeService;
/**
* 获取图片验证码的接口
* Restful风格是获取的请求 所有使用GetMapping 传递的参数是UUID(key)
* 后端可以使用Redis进行将前端传递的UUID作为key 生成的验证码作为value进行存储
*/
@GetMapping("/image/{key}")
@ApiOperation(value = "获取图片验证码的接口")
public AjaxResult graphVerifyCode(@PathVariable("key") String key) {
/**
* 快捷键 ctrl + Alt + t 自动生成try catch。。
* */
System.out.println("前端传入的UUID:" + key);
try {
// 获取的是一个经过Base64加密后的String
String base64Str = iVerifyCodeService.graphVerifyCode(key);
// 使用单例模式 直接获取一个AjaxResult对象
// 并将经过base64加密后的String 设置到AjaxResult的如resultObj属性中
return AjaxResult.getAjaxResult().setResultObj(base64Str);
} catch (Exception e) {
e.printStackTrace();
// 直接通过set方法 进行设置提示语
return AjaxResult.getAjaxResult().setSuccess(false).setMsg("操作失败");
}
}
}
service
public interface IVerifyCodeService {
/**
* 用来获取业务层经过逻辑处理后的经过base64加密后的String
*/
String graphVerifyCode(String key);
serviceImpl
@Service
public class VerifyCodeServiceImpl implements IVerifyCodeService {
/**
* 需要注入RedisTemplate
* 用redis将前端生成的UUID作为key进行存储,value就是后端图形验证码经过base64编码加密后的String到内存中,能够快速进行获取,提高效率
*/
@Autowired
private RedisTemplate redisTemplate;
/**
* 注入UserMapper 通过根据电话号码进行查询是否已经注册过
*/
@Autowired
private UserMapper userMapper;
/**
* 获取图片的随机验证码
*/
@Override
public String graphVerifyCode(String key) { // key就是传入的UUID
// 校验前端传入的UUID是否是一个空值
if (!StringUtils.hasLength(key)) { // 如果没有长度 则说明没有key
throw new BusinessException("请输入Key");
}
// 使用工具类获取4位的随机数验证码
String code = StrUtils.getComplexRandomString(4);
System.out.println("后台生成的4位图片随机验证码" + code);
// 4将Code存储在Redis中(在缓冲中,读取速度比较快),使用前端传递的UUID作为Redis的key,code为值,设置过期时间。
// 不允许产生大量无用地图片验证码。【每次都携带UUID,如果再次获取就进行覆盖图形验证码。】,
// 对值进行操作 设置 k为identifing_code v为10086 时长为10 单位为秒
redisTemplate.opsForValue().set(
key, // key位前端传递的UUID 每个浏览器唯一
code, // 生成的随机数验证码
3, // 设置过期时间
TimeUnit.MINUTES // 过期时间的单位
);
// 5使用自带的2D引擎将图片验证码的code生成一个图片
// 6将图片基于Base64编码加密成String,响应给前端 参数为图片显示的高度和宽度,传入code为生成的随机数验证码
String graphVerifyStr = VerifyCodeUtils.VerifyCode(105, 42, code);
// 7前端拿到base字符串,进行图片的展示,用户输入图片验证码
System.out.println("经过加密后的String:" + graphVerifyStr);
// 将图片验证码经过base64编码加密后的String返回给前端,前端通过加上前缀就可以进行显示
return graphVerifyStr;
}
}
strutils工具类
/**
* StrUtils工具类
*/
public class StrUtils {
/**
* 把逗号分隔的字符串转换字符串数组
*
* @param str
* @return
*/
public static String[] splitStr2StrArr(String str, String split) {
if (str != null && !str.equals("")) {
return str.split(split);
}
return null;
}
/**
* 把逗号分隔字符串转换List的Long
*
* @param str
* @return
*/
public static List<Long> splitStr2LongArr(String str) {
String[] strings = splitStr2StrArr(str, ",");
if (strings == null) return null;
List<Long> result = new ArrayList<>();
for (String string : strings) {
result.add(Long.parseLong(string));
}
return result;
}
/**
* 把逗号分隔字符串转换List的Long
*
* @param str
* @return
*/
public static List<Long> splitStr2LongArr(String str, String split) {
String[] strings = splitStr2StrArr(str, split);
if (strings == null) return null;
List<Long> result = new ArrayList<>();
for (String string : strings) {
result.add(Long.parseLong(string));
}
return result;
}
/**
* 获取随机的字符串
*/
public static String getRandomString(int length) {
String str = "0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(str.length());
sb.append(str.charAt(number));
}
return sb.toString();
}
/**
* 获取复杂的随机的字符串
* 参数为需要生成的随机数个数
* <p>
*
*/
public static String getComplexRandomString(int length) {
// String str = "!@#¥%&*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random(); // 创建一个随机数对象
StringBuffer sb = new StringBuffer(); // 创建一个StringBuffer对象,用来存储生成的验证码,线程安全且字符串长度可变
for (int i = 0; i < length; i++) { // 需要执行的次数
int number = random.nextInt(str.length()); // 生成的随机数作为下标,去str字符串中去取对应的值
sb.append(str.charAt(number)); // 通过随机生成的数字,取对应的值拼接到StringBuffer中
}
return sb.toString();
}
public static String convertPropertiesToHtml(String properties) {
//1:容量:6:32GB_4:样式:12:塑料壳
StringBuilder sBuilder = new StringBuilder();
String[] propArr = properties.split("_");
for (String props : propArr) {
String[] valueArr = props.split(":");
sBuilder.append(valueArr[1]).append(":").append(valueArr[3]).append("<br>");
}
return sBuilder.toString();
}
}
VerifyCodeUtils工具类
/**
* 生成图片验证码工具类
*/
public class VerifyCodeUtils {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
*
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
*
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/**
* 输出指定验证码图片流
*
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
/**
* 获取随机验证码及其加密图片
* 用的就是这个生成图片验证码 基于Base64编码进行加密成的String
*/
public static String VerifyCode(int w, int h, String code){
try {
//base64编码器
BASE64Encoder encoder = new BASE64Encoder();
//准备输出流
ByteArrayOutputStream data = new ByteArrayOutputStream();
//使用code生成w宽 h高的图片,并将结果图片存入data流中
outputImage(w, h, data, code);
//使用base64编码成String,返回一个String给前端
return encoder.encode(data.toByteArray());
} catch (IOException e) {
e.printStackTrace();
/**
* 抛出自定义异常
* */
throw new BusinessException("生成验证码失败!");
}
}
/**
* 直接进行测试
* 使用StrUtils调用getComplexRandomString 传入参数为需要即为的验证码
* 获取随机的数字 即验证码
* 通过VertifyCodeUtils调用VerifyCode方法生成一个加密后的图片验证码
* */
public static void main(String[] args) throws Exception{
String code = StrUtils.getComplexRandomString(4);
System.out.println("生成的验证码:"+code); // s4eM
System.out.println(VerifyCode(100, 30, "1234"));
// 这里显示的是 一个没有前缀的String 前缀为data:image/jpeg;base84,
}
}
AjaxResult
将经过base64加密过后的String进行图片展示,前端通过拼接过后进行显示在前端
/**
* 增删改的一个工具类,用来封装是否成功和提示信息
*
* */
public class AjaxResult {
// 默认是true 和 操作成功
private Boolean success = true;
private String msg = "操作成功";
/**
* 定义一个Object类的属性resultObj
* 用来封装上传过后storage存储过后返回给前端的文件路径和文件名
* 和图形验证码经过Base64加密后的String
* */
public Object resultObj;
public Object getResultObj() {
return resultObj;
}
// 链式语法改造 调用set方法时会返回一个AjaxResult就可以继续进行调用方法
// 参数resultObj就是storage存储过后返回给前端的文件路径和文件名
public AjaxResult setResultObj(Object resultObj) {
this.resultObj = resultObj;
return this;
}
public AjaxResult() {
}
/**
* 单例模式
* 1 构造方法私有化
* 2 内部提供一个可供外界调用的一个静态方法
* 使用单例模式进行创建AjaxResult对象 每次调用都是用的这个对象
* 在类加载的时候就创建好了对象 单例的饿汉模式
*/
private static AjaxResult a = new AjaxResult();
/**
* 1构造方法私有化
*/
/* private AjaxResult() {
};*/
/**
* 2 内部提供一个可供外界调用的一个静态方法
* // this指代的是当前AjaxResult类的对象a this = a
*/
public static AjaxResult getAjaxResult() {
return a;
}
/**
* 链式编程思维
*/
public AjaxResult setSuccess(Boolean success) {
this.success = success;
// 当前操作的是谁返回给谁
// this指代的是当前AjaxResult类的对象a
return this;
}
public AjaxResult setMsg(String msg) {
this.msg = msg;
// 当前操作的是谁返回给谁
return this;
}
public AjaxResult(Boolean success, String msg) {
this.success = success;
this.msg = msg;
}
public Boolean getSuccess() { // get方法 前端获取bean属性
return success;
}
public String getMsg() {
return msg;
}
}