前后端分离验证码之cookie+redis方案
前后端分离后,由于没有了session,导致验证码内容存储在session已经不可能了,因此考虑存储在redis。本文将介绍一种基于cookie + redis方案的验证码
一、方案的提出
(1)验证码存放位置
没了session,则存储在redis,why?
因为redis 具有key自动过期,所以用来存放验证码最为合适
(2)验证码的生成及校验
- 方案一
- 网上比较常见的方案,公司妇幼系统也在用的方案
- 生成阶段:每次调用验证码接口,生成一个唯一key,value为验证码的值,然后存放redis;返回前端时,不再是一张图片,而是json,内含唯一Key + 验证码图片base64
- 校验阶段:请求后台校验时,需要带上这个key + 验证码的value
- 优点:实现代码简单
- 缺点:如果用户恶意的频繁大量调用验证码接口,由于旧的验证码存储在redis的key没有立即删除,redis中验证码key会堆积(虽然有过期,但也顶不住大量并发生成);需要设置额外的限流拦截
- 方案二
- 参考开源项目 https://captcha.anji-plus.com/#/doc
- 生成阶段:依赖前端vue组件,生成一个浏览器全局的clientUid,传给后端去生成验证码;后端redis存储key为clientUid,value为验证码值,返回json,内含clientUid+图片base64
- 校验阶段:请求后台时,需要带上这个clientUid+ 验证码的value
- 优点:完美解决了方案一带来的用户恶意刷新验证码导致redis中验证码堆积的问题
- 不足:比较依赖前端生成的clientUid,如果能够不依赖这个前端的clientUid就更好了
- 方案三
- 参考方案二思路,但是用了cookie技术
- 生成阶段:客户端唯一标识clientUid不需要需要前端,而是以cookie的方式写入到浏览器,如果浏览器已经有该cookie,则以浏览器cookie有值为准。这样这个客户端唯一标识就不依赖前端了;后端redis存储key为clientUid,value为验证码值;返回前端时可以直接返回图片,response加一个addCookie的操作
- 校验阶段:从cookie中读取到客户端唯一标识,然后去redis中取验证码对应的值进行内容比对即可
- 优点:完善了方案二的不足,同时如果从session的项目改造成token时,该方案的前端改动最少
- 不足:依赖cookie,不适合无cookie场景
本次恰好参与了一个从session改成token的前后端分离项目,验证码直接用的方案三
二、实现代码
(1)验证码的生成
Controller层
/**
* 获取用户登录图形验证码
*
* @param request
* @param response
*/
@ApiOperation(value = "获取图形验证码")
@GetMapping("verifyCode")
public void verifyCode(@ApiIgnore HttpServletRequest request, @ApiIgnore HttpServletResponse response) {
// 从cookie中获取验证码对应的唯一key
String verifyKey = Optional.ofNullable(WebUtils.getCookie(request,
WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME))
.map(Cookie::getValue).orElse(null);
if (StringUtils.isBlank(verifyKey)) {
verifyKey = UUID.randomUUID().toString().replace("-", "");
}
// 这里每个请求都add新cookie,如果不每次add,则有可能会导致 cookie的path发生变化
response.addCookie(CookieHelper.generateCookie(WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME,
verifyKey, "/", request));
// 生成随机字串,用来做验证码
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
try {
// 生成图片
int width = 100;
int height = 40;
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
VerifyCodeUtils.outputImage(width, height, response.getOutputStream(), verifyCode);
} catch (IOException e) {
log.error("生成用户登录图形验证码出错:", e);
throw new SPIException(BasicEcode.FAILED);
}
// 存入会话redis, 2分钟内有效
redisService.set(RedisConstant.VERIFY_CODE_KEY_PREFIX + verifyKey, verifyCode, 120, TimeUnit.SECONDS);
}
cookie工具类
public class CookieHelper {
private CookieHelper() {
}
public static Cookie generateCookie(String name, String value, String path, HttpServletRequest request) {
Cookie cookie = new Cookie(name, value);
// 这个path的写法参考SpringBoot源码写的
cookie.setPath(StringUtils.isBlank(path) ? getRequestContext(request) : path);
cookie.setSecure(false);
cookie.setHttpOnly(true);
// 设置为-1时,关闭浏览器自动失效,设置为0马上失效
cookie.setMaxAge(-1);
return cookie;
}
public static void addCookie(String name, String value, HttpServletRequest request, HttpServletResponse response) {
response.addCookie(generateCookie(name, value, null, request));
}
/**
* 设置全局cookie,相同域名下不同项目都可以访问
* @param name cookie名称
* @param value cookie值
* @param request
* @param response
*/
public static void addGlobalCookie(String name, String value, HttpServletRequest request, HttpServletResponse response){
response.addCookie(generateCookie(name, value, "/", request));
}
/**
* description: 删除cookie
* @param name cookie名字
* @param request
* @param response
* @return void
* @author ZENG.XIAO.YAN
* @time 2021-07-23 13:57
*/
public static void deleteCookie(String name, HttpServletRequest request, HttpServletResponse response) {
Cookie cookie = new Cookie(name, null);
cookie.setPath(getRequestContext(request));
cookie.setMaxAge(0);
response.addCookie(cookie);
}
/**
* 删除全局cookie
* @param name
* @param request
* @param response
*/
public static void deleteGlobalCookie(String name, HttpServletRequest request, HttpServletResponse response) {
Cookie cookie = new Cookie(name, null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
private static String getRequestContext(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath.length() > 0 ? contextPath : "/";
}
}
(2)检验相关代码
直接上代码
// 校验图形验证码
// 1.从cookie中取出验证码的key
Cookie cookie = WebUtils.getCookie(request, WebSecurityConfiguration.USER_LOGIN_VERIFY_CODE_COOKIE_NAME);
String verifyKey = Optional.ofNullable(cookie).map(Cookie::getValue).orElse(null);
if (StringUtils.isBlank(verifyKey)) {
// 没有key,直接提示过期
CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
return null;
}
// 2.从redis中拿到对应key的数据
String redisKey = RedisConstant.VERIFY_CODE_KEY_PREFIX + verifyKey;
String redisVerifyCode = (String) redisService.get(redisKey);
if (StringUtils.isBlank(redisVerifyCode)) {
CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_EXPIRED);
return null;
}
// 3.比较值
if (!redisVerifyCode.equalsIgnoreCase(authenticationBean.getVerifyCode())) {
CustomResponse.error(request, response, PlatformExceptionCode.VERIFY_CODE_ERROR);
return null;
}
// 图形验证码校验成功后,直接从会话中移除
redisService.delete(redisKey);
三、小结
cookie +redis的方式的验证码特别适合那种从session改造成token的前后端分离项目
作者:zeng1994
出处:http://www.cnblogs.com/zeng1994/
本文版权归作者和博客园共有,欢迎转载!但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接!