方案
- 前端在登录页面时,请求验证码的信息;
- 后端可以利用UUID生成随机数key和验证码code存储在redis中,设置好倒计时间;
- 后端再把随机数key和含验证码code的base64的图片传给前端;
- 前端在登录接口时,把用户名/密码/key/验证码code一起传给后端;
- 后端对前端传的key和code和redis中存储的进行比对,并把验证码失败或者成功结果传给前端;
代码实现
后端
kaptcha谷歌验证码工具依赖
<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
config文件配置图片验证码的生成规则;具体配置参数可查看此链接;
@Configuration
public class KaptchaConfig {
@Bean
DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
提供验证码的接口;别忘了在安全访问的配置中取消/captcha接口的token验证;
@GetMapping("/captcha")
public ResultJson captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
//存储key和code到redis中
boolean flag = redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120000); //时间是毫秒
//传输base64验证码图片和token
Map<String, Object> map = new HashMap<>();
map.put("token", key);
map.put("captchaImg", base64Img);
return ResultJson.ok().data(map);
}
CaptchaFilter 图片验证码校验过滤器
/**
* 图片验证码校验过滤器,在登录过滤器前
*/
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {
private final String loginUrl = "/login";
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String url = request.getRequestURI();
if (loginUrl.equals(url) && request.getMethod().equals("POST")) {
log.info("获取到login链接,正在校验验证码 -- " + url);
try {
validate(request);
} catch (CaptchaException e) {
log.info(e.getMessage());
// 交给登录失败处理器处理
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
}
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) {
String code = request.getParameter("code");
String token = request.getParameter("token");
if (StringUtils.isBlank(code) || StringUtils.isBlank(token)) {
throw new CaptchaException("验证码不能为空");
}
if(!code.equals(redisUtil.hget(Const.captcha_KEY, token))) {
throw new CaptchaException("验证码不正确");
}
// 一次性使用
redisUtil.hdel(Const.captcha_KEY, token);
}
}
前端
请求/captcha接口
getCaptcha() {
this.$axios.get("/captcha").then((res) => {
let data = res.data.data;
this.loginForm.token = data.token;
this.captchaImg = data.captchaImg;
});
显示验证码,点击可切换验证码
<el-form-item label="验证码" prop="code">
<el-input
v-model="loginForm.code"
prefix-icon="el-icon-key"
></el-input>
<el-image
:src="captchaImg"
class="captchaImg"
@click="getCaptcha"
></el-image>
</el-form-item>
请求登录接口方法
data() {
return {
loginForm: {
username: null,
password: null,
code: null,//验证码
token: null,//验证码的token
},
};
},
methods: {
loginSubmit(formName) {
this.$refs[formName].validate((valid) => {
//表单校验
if (valid) {
axios
.post("/login", this.$qs.stringify(this.loginForm), {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
})
.then((response) => {
console.log(response.data);
var message = response.data.message;
var success = response.data.success;
var token = response.data.data.token;
var refreshToken = response.data.data.refreshToken;
console.log(message);
var messageType = null;
console.log("success的值:", success);
console.log(typeof success);
if (success === true) {
messageType = "success";
} else {
messageType = "error";
this.getCaptcha();//重新请求验证码接口
}
console.log(messageType);
this.$message({
//登录提示信息
message: message,
type: messageType,
});
if (token) {
//token有值
localStorage.setItem("token", token); //将token存入本地
localStorage.setItem("refreshToken", refreshToken); //将token存入本地
this.$router.push({name:"Index"}); //跳转到首页
}
})
.catch(function(error) {
// 请求失败处理
console.log(error);
});
console.log("login submit!");
} else {
console.log("error submit!!");
return false;
}
});
},
}
参考
https://www.zhuawaba.com/post/19#%E7%94%9F%E6%88%90%E9%AA%8C%E8%AF%81%E7%A0%81