应用场景
这次在实训的时候写了一个线上购物商城的项目,做完了想加点东西,尝试在登陆时加载一个图片验证码,本来以为挺简单的,但是做起来还有点复杂,记录一下心得
尝试过程
后端实现加载图片验证码
这段是借鉴之前的一个项目的代码,废话不多说,上代码
接口:IVerifyCodeGen
package com.example.codeutil;
import java.io.IOException;
import java.io.OutputStream;
public interface IVerifyCodeGen {
/**
* 生成验证码并返回code,将图片写的os中
*
* @param width
* @param height
* @param os
* @return
* @throws IOException
*/
String generate(int width, int height, OutputStream os) throws IOException;
/**
* 生成验证码对象
*
* @param width
* @param height
* @return
* @throws IOException
*/
VerifyCode generate(int width, int height) throws IOException;
}
生成随机字母和数字的类:RandomUtils
package com.example.codeutil;
import java.awt.*;
import java.util.Random;
public class RandomUtils extends org.apache.commons.lang3.RandomUtils {
private static final char[] CODE_SEQ = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9' };
private static final char[] NUMBER_ARRAY = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private static Random random = new Random();
public static String randomString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(String.valueOf(CODE_SEQ[random.nextInt(CODE_SEQ.length)]));
}
return sb.toString();
}
public static String randomNumberString(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(String.valueOf(NUMBER_ARRAY[random.nextInt(NUMBER_ARRAY.length)]));
}
return sb.toString();
}
public static Color randomColor(int fc, int bc) {
int f = fc;
int b = bc;
Random random = new Random();
if (f > 255) {
f = 255;
}
if (b > 255) {
b = 255;
}
return new Color(f + random.nextInt(b - f), f + random.nextInt(b - f), f + random.nextInt(b - f));
}
public static int nextInt(int bound) {
return random.nextInt(bound);
}
}
生成验证码图片的类:SimpleCharVerifyCodeGenImpl
package com.example.codeutil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
/**
* 验证码实现类
*/
public class SimpleCharVerifyCodeGenImpl implements IVerifyCodeGen {
private static final Logger logger = LoggerFactory.getLogger(SimpleCharVerifyCodeGenImpl.class);
private static final String[] FONT_TYPES = { "\u5b8b\u4f53", "\u65b0\u5b8b\u4f53", "\u9ed1\u4f53", "\u6977\u4f53", "\u96b6\u4e66" };
private static final int VALICATE_CODE_LENGTH = 4;
/**
* 设置背景颜色及大小,干扰线
*
* @param graphics
* @param width
* @param height
*/
private static void fillBackground(Graphics graphics, int width, int height) {
// 填充背景
graphics.setColor(Color.WHITE);
//设置矩形坐标x y 为0
graphics.fillRect(0, 0, width, height);
// 加入干扰线条
for (int i = 0; i < 8; i++) {
//设置随机颜色算法参数
graphics.setColor(RandomUtils.randomColor(40, 150));
Random random = new Random();
int x = random.nextInt(width);
int y = random.nextInt(height);
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
graphics.drawLine(x, y, x1, y1);
}
}
/**
* 生成随机字符
*
* @param width
* @param height
* @param os
* @return
* @throws IOException
*/
@Override
public String generate(int width, int height, OutputStream os) throws IOException {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphics = image.getGraphics();
fillBackground(graphics, width, height);
String randomStr = RandomUtils.randomString(VALICATE_CODE_LENGTH);
createCharacter(graphics, randomStr);
graphics.dispose();
//设置JPEG格式
ImageIO.write(image, "JPEG", os);
return randomStr;
}
/**
* 验证码生成
*
* @param width
* @param height
* @return
*/
@Override
public VerifyCode generate(int width, int height) {
VerifyCode verifyCode = null;
try (
//将流的初始化放到这里就不需要手动关闭流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
) {
String code = generate(width, height, baos);
verifyCode = new VerifyCode();
verifyCode.setCode(code);
verifyCode.setImgBytes(baos.toByteArray());
} catch (IOException e) {
logger.error(e.getMessage(), e);
verifyCode = null;
}
return verifyCode;
}
/**
* 设置字符颜色大小
*
* @param g
* @param randomStr
*/
private void createCharacter(Graphics g, String randomStr) {
char[] charArray = randomStr.toCharArray();
for (int i = 0; i < charArray.length; i++) {
//设置RGB颜色算法参数
g.setColor(new Color(50 + RandomUtils.nextInt(100),
50 + RandomUtils.nextInt(100), 50 + RandomUtils.nextInt(100)));
//设置字体大小,类型
g.setFont(new Font(FONT_TYPES[RandomUtils.nextInt(FONT_TYPES.length)], Font.BOLD, 26));
//设置x y 坐标
g.drawString(String.valueOf(charArray[i]), 15 * i + 5, 19 + RandomUtils.nextInt(8));
}
}
}
验证码类的调用类:VerifyCode
package com.example.codeutil;
public class VerifyCode {
private String code;
private byte[] imgBytes;
private long expireTime;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public byte[] getImgBytes() {
return imgBytes;
}
public void setImgBytes(byte[] imgBytes) {
this.imgBytes = imgBytes;
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
}
登录接口中比对验证码
失败尝试:session存储
在借鉴的项目中,作者将每次生成的验证码的码值存储在session中,发送到前端,前端输入数据后再传到后端,再从session中取出验证码的码值,在原项目中能够正常运行,是因为这个项目不是前后端分离的项目,每次对话的session是一样的,但是在前后端分离的项目中,由于每次请求的会话id是不同的,因此在后端是取不到之前存储的session的,我看网上很多说要在vue的main.js中加配置
axios.defaults.withCredentials = true
再在后端配置一个filter,但是我尝试之后并没有什么效果。
成功尝试:Redis暂存验证码信息
后来想想有两种解决方法,直接将验证码存到数据库中,队友是这样做的,最后做出来了,但是这样我觉得比较浪费资源;使用Redis暂存验证码,问题顺利解决,只是第一次用Redis,用的很不熟练,刚开始直接在pom.xml中加了个依赖就开始用,连下载都没有下载(属实小丑了)
话不多说上代码
pom.xml的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
Redis的配置类config
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession
public class RedisConfig {
}
RedisTemplateConfig
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisService类
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, String> redisTemplate ;
public void set(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void delete(String key) {
redisTemplate.delete(key);
}
}
Controller类
package com.example.controller;
import com.example.codeutil.IVerifyCodeGen;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.example.codeutil.SimpleCharVerifyCodeGenImpl;
import com.example.codeutil.VerifyCode;
import com.example.common.Result;
import com.example.common.enums.ResultCodeEnum;
import com.example.common.enums.RoleEnum;
import com.example.entity.Account;
import com.example.service.AdminService;
import com.example.service.BusinessService;
import com.example.service.RedisService;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 基础前端接口
*/
@RestController
public class WebController {
@Resource
private AdminService adminService;
@Resource
private BusinessService businessService;
@Resource
private UserService userService;
@Resource
private RedisService redisService ;
@GetMapping("/")
public Result hello() {
return Result.success("访问成功");
}
/**
* 登录
*/
@PostMapping("/login")
public Result login(@RequestBody Account account, @RequestParam String captcha, HttpServletRequest request) {
if (ObjectUtil.isEmpty(account.getUsername()) || ObjectUtil.isEmpty(account.getPassword())
|| ObjectUtil.isEmpty(account.getRole()) || StrUtil.isBlank(captcha)) {
return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);
}
// 验证验证码
System.out.println(captcha);
String sessionCaptcha = redisService.get("VerifyCode" );
System.out.println(sessionCaptcha);
if (StrUtil.isBlank(sessionCaptcha) || !sessionCaptcha.equalsIgnoreCase(captcha)) {
return Result.error(ResultCodeEnum.CAPTCHA_ERROR);
}
if (RoleEnum.ADMIN.name().equals(account.getRole())) {
account = adminService.login(account);
}
if (RoleEnum.BUSINESS.name().equals(account.getRole())) {
account = businessService.login(account);
}
if (RoleEnum.USER.name().equals(account.getRole())) {
account = userService.login(account);
}
return Result.success(account);
}
/**
* 获取验证码方法
* @param request
* @param response
*/
@GetMapping("/verifyCode")
public void verifyCode(HttpServletRequest request, HttpServletResponse response) {
IVerifyCodeGen iVerifyCodeGen = new SimpleCharVerifyCodeGenImpl();
try {
// 设置长宽
VerifyCode verifyCode = iVerifyCodeGen.generate(80, 28);
String code = verifyCode.getCode();
// 将VerifyCode绑定session
request.getSession().setAttribute("VerifyCode", code);
System.out.println(code);
redisService.set("VerifyCode" , code, 5, TimeUnit.MINUTES);
// 设置响应头
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
// 在代理服务器端防止缓冲
response.setDateHeader("Expires", 0);
// 设置响应内容类型
response.setContentType("image/jpeg");
response.getOutputStream().write(verifyCode.getImgBytes());
response.getOutputStream().flush();
} catch (IOException e) {
System.out.println("异常处理");
}
}
/**
* 注册
*/
@PostMapping("/register")
public Result register(@RequestBody Account account) {
if (StrUtil.isBlank(account.getUsername()) || StrUtil.isBlank(account.getPassword())
|| ObjectUtil.isEmpty(account.getRole())) {
return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);
}
if (RoleEnum.ADMIN.name().equals(account.getRole())) {
adminService.register(account);
}
if (RoleEnum.BUSINESS.name().equals(account.getRole())) {
businessService.register(account);
}
if (RoleEnum.USER.name().equals(account.getRole())) {
userService.register(account);
}
return Result.success();
}
/**
* 修改密码
*/
@PutMapping("/updatePassword")
public Result updatePassword(@RequestBody Account account) {
if (StrUtil.isBlank(account.getUsername()) || StrUtil.isBlank(account.getPassword())
|| ObjectUtil.isEmpty(account.getNewPassword())) {
return Result.error(ResultCodeEnum.PARAM_LOST_ERROR);
}
if (RoleEnum.ADMIN.name().equals(account.getRole())) {
adminService.updatePassword(account);
}
if (RoleEnum.BUSINESS.name().equals(account.getRole())) {
businessService.updatePassword(account);
}
if (RoleEnum.USER.name().equals(account.getRole())) {
userService.updatePassword(account);
}
return Result.success();
}
}
前端
<template>
<div class="container">
<div style="width: 400px; padding: 30px; background-color: white; border-radius: 5px;">
<div style="text-align: center; font-size: 20px; margin-bottom: 20px; color: #333">欢迎登录线上购物商城</div>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="username">
<el-input prefix-icon="el-icon-user" placeholder="请输入账号" v-model="form.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input prefix-icon="el-icon-lock" placeholder="请输入密码" show-password v-model="form.password"></el-input>
</el-form-item>
<el-form-item prop="role">
<el-select v-model="form.role" placeholder="请选择角色" style="width: 100%">
<el-option label="管理员" value="ADMIN"></el-option>
<el-option label="商家" value="BUSINESS"></el-option>
<el-option label="用户" value="USER"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="verifycode">
<el-input prefix-icon="el-icon-picture" placeholder="请输入验证码" v-model="form.verifycode"></el-input>
<div style="margin-top: 10px;">
<img :src="captchaUrl" @click="refreshCaptcha" style="cursor: pointer;" alt="点击刷新验证码"/>
</div>
</el-form-item>
<el-form-item>
<el-button style="width: 100%; background-color: #333; border-color: #333; color: white" @click="login">登 录</el-button>
</el-form-item>
<div style="display: flex; align-items: center">
<div style="flex: 1"></div>
<div style="flex: 1; text-align: right">
还没有账号?请 <a href="/register">注册</a>
</div>
</div>
</el-form>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
form: {
username: '',
password: '',
role: '',
verifycode: ''
},
rules: {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],
role: [
{ required: true, message: '请选择角色', trigger: 'blur' },
],
verifycode: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
},
captchaUrl: ''
}
},
mounted() {
this.captchaUrl=this.timestamp("http://localhost:9090/verifyCode");
},
methods: {
login() {
this.$refs['formRef'].validate((valid) => {
if (valid) {
const { username, password, role, verifycode } = this.form;
this.$request.post('/login', { username, password, role }, { params: { captcha: verifycode } }).then(res => {
if (res.code === '200') {
let user = res.data;
localStorage.setItem("xm-user", JSON.stringify(user)); // 存储用户数据
if (user.role === 'USER') {
location.href = '/front/home';
} else {
location.href = '/home';
}
this.$message.success('登录成功');
} else {
this.$message.error(res.msg);
}
})
}
})
},
timestamp(url){
var gettime=new Date().getTime();
if(url.indexOf("?")>-1){
url=url+"×tamp="+gettime;
}else{
url=url+"?timestamp="+gettime;
}
return url;
},
refreshCaptcha() {
var url='http://localhost:9090/verifyCode';
var gettime=new Date().getTime();
if(url.indexOf("?")>-1){
url=url+"×tamp="+gettime;
}else{
url=url+"?timestamp="+gettime;
}
this.captchaUrl = url;
}
}
}
</script>
<style scoped>
.container {
height: 100vh;
overflow: hidden;
background-image: url("@/assets/imgs/bg1.jpg");
background-size: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
a {
color: #2a60c9;
}
</style>
小结
这次经历让我对session的机制有了更深的了解:
当用户第一次访问服务器时,服务器会创建一个新的 HttpSession
对象并分配一个唯一的会话 ID(通常是一个长字符串)。
会话 ID 通常通过 Cookie(默认名为 JSESSIONID
)或 URL 重写(如果 Cookie 不被支持)在客户端和服务器之间传递。
- 服务器将会话数据(例如用户信息)保存在
HttpSession
对象中。 - 在后续请求中,服务器通过会话 ID 找到相应的
HttpSession
对象,并从中读取或写入数据。 - 会话有默认的超时时间,超过这个时间没有活动的会话会自动失效。
- 可以通过调用
session.invalidate()
方法手动使会话失效。
在前后端分离的项目中,每次会话的id是不同的,这时可以通过使用Redis来暂存信息