在用谷歌的kaptcha做验证码登录校验,将后端发布到阿里云,前端是本地启动,用谷歌浏览器(版本85)访问验证码遇到了如下问题(360浏览器、microsoft edge未重现)
如下为获取验证码的接口
@ApiOperation(value = "获取验证码", notes = "此接口用于获取验证码")
@GetMapping("captcha.jpg")
public void captcha(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
// 生成文字验证码
String text = producer.createText();
// 生成图片验证码
BufferedImage image = producer.createImage(text);
// 保存到验证码到 session
System.out.println("=============================");
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);
System.out.println("生成文字验证码:" + text);
System.out.println("获取验证码 session:" + request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY));
System.out.println("获取验证码 request.getSession().getId():" + request.getSession().getId());
System.out.println("=============================");
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
登录的部分接口
@ApiOperation(value = "系统登录", notes = "此接口用于系统登录")
@PostMapping(value = "/login")
public ApiResponses login(@RequestBody LoginParam loginPARAM, HttpServletRequest request) {
String username = loginPARAM.getUsername();
String password = loginPARAM.getPassword();
String captcha = loginPARAM.getCaptcha();
System.out.println("=============================");
System.out.println("系统登录时 request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY):" + request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY));
System.out.println("系统登录时 request.getSession().getId():" + request.getSession().getId());
。
。
.
}
这种方式请求验证码的时候会带cookie给前端,如下所示,JSESSIONID就是后端的request.getSession().getId(),登录的时候如果设置了跨域,前端会将JSESSIONID返回给后端,后端会进行判断。但是现在的问题就是两次的sessionId不一致。所以还是要检查是否设置对了跨域
这是我的跨域配置类,需要注意的是
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许跨域访问的路径
registry.addMapping("/**")
// 允许跨域访问的源
.allowedOrigins("http://服务器Ip:9528","http://服务器Ip:9001")
// 允许请求方法
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
// 预检间隔时间
.maxAge(168000)
// 允许头部设置
.allowedHeaders("*")
// 是否发送cookie
.allowCredentials(true);
}
}
前端设置跨域主要为:axios.defaults.withCredentials = true,然后此项目前端如下
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
寻思着,这样配置也没问题吧。
经过度娘的助攻,终于找出了问题的源头
SameSite
的值可以填3个:Strict
,Lax
,None
.
缺省的值为Lax
,而且当你设置其为空时,在新的Chrome中还是会给予默认值Lax
.
Strict
严格模式
Lax
宽松模式
None
可以在第三方环境中发送cookie
在这种模式下,必须同时启用Secure
才行
@Configuration
public class SpringSessionConfig {
// 最新的chrome,设置null会默认成lax 但是如果设置samesite为NONE,又需要设置secure。https支持secure,http不行
@Bean
public CookieSerializer httpSessionIdResolver() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setUseHttpOnlyCookie(false);
cookieSerializer.setSameSite("None");
cookieSerializer.setCookiePath("/");
cookieSerializer.setUseSecureCookie(true);
return cookieSerializer;
}
}
然后将后端继续发布到阿里云,然而的然而 还是翻车了。。。
三者条件才能在高版本的谷歌浏览器访问
但是阿里云是http,那怎么办呢
上代码:
@Autowired
private RedisTemplate redisTemplate;
@ApiOperation(value = "获取验证码", notes = "此接口用于获取验证码")
@GetMapping("captcha.jpg")
public void captcha(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
// 生成文字验证码
String text = producer.createText();
// 生成图片验证码
BufferedImage image = producer.createImage(text);
// 保存到验证码到 redis 设置1分钟过期
redisTemplate.opsForValue().set(Constants.KAPTCHA_SESSION_KEY,text,1, TimeUnit.MINUTES);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
@ApiOperation(value = "系统登录", notes = "此接口用于系统登录")
@PostMapping(value = "/login")
public ApiResponses login(@RequestBody LoginParam loginPARAM, HttpServletRequest request) {
String username = loginPARAM.getUsername();
String password = loginPARAM.getPassword();
String captcha = loginPARAM.getCaptcha();
Object kaptcha = redisTemplate.opsForValue().get(Constants.KAPTCHA_SESSION_KEY);
。
。
。
}
这样就可以解决啦
用如上方法写验证码会有一种问题,就是当多个用户同时请求获取验证码,其中先获取验证码的人就会失效。然后做了如下改进
我弃用了谷歌的kaptcha,重写了验证码。给redis set值的时候同时加上一个token,登录的时候需要返回token来验证
续上部分代码
/**
* 生成验证码
*
* @return
*/
public CaptchaDTO getCaptcha() {
//1.在内存中创建一张图片
BufferedImage bi = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
// 画布颜色数组
Color[] colors = new Color[]{Color.BLUE, Color.CYAN, Color.GRAY, Color.GREEN, Color.ORANGE, Color.RED, Color.BLACK};
//2.得到图片
Graphics g = bi.getGraphics();
//3.设置图片的背影色
setBackGround(g, WIDTH, HEIGHT);
//4.设置图片的边框
//setBorder(g,width,height);
//5.在图片上画干扰线
drawRandomLine(g, colors, WIDTH, HEIGHT);
String random = drawRandomNum((Graphics2D) g, colors);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
ImageIO.write(bi, "jpg", outputStream);
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
String imageCode = encoder.encode(outputStream.toByteArray()).replaceAll("\r|\n", "");
String token = JwtTokenUtils.generateCheckCode(random);
CaptchaDTO captchaDTO = new CaptchaDTO();
captchaDTO.setCodeToken(token);
captchaDTO.setImageCode(imageCode);
// 保存到验证码到 redis 设置1分钟过期
redisTemplate.opsForValue().set(Constants.KAPTCHA_SESSION_KEY + token, random, 1, TimeUnit.MINUTES);
return captchaDTO;
}
// 登录部分代码
String username = loginPARAM.getUsername();
String password = loginPARAM.getPassword();
String imageCode = loginPARAM.getImageCode();
String codeToken = loginPARAM.getCodeToken();
// 校验验证码
String code = (String) redisTemplate.opsForValue().get(Constants.KAPTCHA_SESSION_KEY + codeToken);
if (StringUtils.isBlank(code)) {
ApiAssert.failure(ErrorCodeEnum.KAPTCHA_NOT_FOUND);
}
// 清除token,防止重用
redisTemplate.delete(Constants.KAPTCHA_SESSION_KEY + codeToken);
if (!imageCode.equalsIgnoreCase(code)) {
ApiAssert.failure(ErrorCodeEnum.KAPTCHA_ERROR);
}
希望对您有帮助!当然有更好的方法烦请大神指出