SpringBoot解决跨域导致sessionId不一致的实现方式

 更新时间:2026年04月10日 10:08:51   作者:l去留无心  
本文描述了使用SpringBoot和谷歌kaptcha做验证码登录校验时遇到的跨域问题,并通过设置SameSite属性、使用https和redis等方式解决了问题

SpringBoot解决跨域导致sessionId不一致

在用谷歌的kaptcha做验证码登录校验,将后端发布到阿里云,前端是本地启动,用谷歌浏览器(版本85)访问验证码遇到了如下问题(360浏览器、microsoft edge未重现)

可以定位到是浏览器兼容问题

代码是这样的:

后端先用HttpServletRequest request的getSession().setAttribute将验证码存进session,请求登录的时候再用request.getSession().getAttribute来判断,然后发现请求验证码的sessionId跟请求登录的sessionId不一致,导致提示验证码一直失效。

如下为获取验证码的接口

  @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不一致。所以还是要检查是否设置对了跨域

检查后端设置的跨域

这是我的跨域配置类,需要注意的是

当allowCredentials为true时

allowedOrigins尽量不要设置为 *

@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
})

寻思着,这样配置也没问题吧。

经过度娘的助攻,终于找出了问题的源头

新版的chrome,加强了防止CSRF攻击,需要设置Cookie的SameSite属性

SameSite的值可以填3个:Strict,Lax,None.

缺省的值为Lax,而且当你设置其为空时,在新的Chrome中还是会给予默认值Lax.

3个模式的介绍

  • 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;
    }
}

然后将后端继续发布到阿里云,然而的然而 还是翻车了。。。

再次寻求百度,然后发现要满足https +SameSite("None") +SecureCookie(true)

三者条件才能在高版本的谷歌浏览器访问

但是阿里云是http,那怎么办呢

还有一种解决方法

弃用通过session校验,可以引入redis来做判断

上代码:

@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);
        }

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java数据结构之顺序表和链表精解

    Java数据结构之顺序表和链表精解

    我在学习完顺序表后一直对顺序表和链表的概念存在一些疑问,这里给出一些分析和看法,通读本篇对大家的学习或工作具有一定的价值,需要的朋友可以参考下
    2021-09-09
  • Spring中AOP概念与两种动态代理模式原理详解

    Spring中AOP概念与两种动态代理模式原理详解

    AOP是面向切面编程的技术,AOP基于IoC基础,是对OOP的有益补充,流行的AOP框架有Sping AOP、AspectJ,这篇文章主要给大家介绍了关于Spring中AOP概念与两种动态代理模式原理的相关资料,需要的朋友可以参考下
    2021-10-10
  • SpringBoot项目中使用腾讯云发送短信的实现

    SpringBoot项目中使用腾讯云发送短信的实现

    本文主要介绍了SpringBoot项目中使用腾讯云发送短信的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • MyBatis持久层框架的用法知识小结

    MyBatis持久层框架的用法知识小结

    MyBatis 本是apache的一个开源项目iBatis,接下来通过本文给大家介绍MyBatis持久层框架的用法知识小结,非常不错,具有参考借鉴价值,感兴趣的朋友一起学习吧
    2016-07-07
  • XML解析四种方式代码示例详解

    XML解析四种方式代码示例详解

    这篇文章主要介绍了XML解析四种方式代码示例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-12-12
  • Java超详细讲解SpringMVC如何获取请求数据

    Java超详细讲解SpringMVC如何获取请求数据

    Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,本质上相当于 Servlet,Spring MVC 角色划分清晰,分工明细,本章来讲解SpringMVC如何获取请求数据
    2022-04-04
  • java多线程三种上锁方式小结

    java多线程三种上锁方式小结

    本文主要介绍了java多线程三种上锁方式小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-12-12
  • Java(SpringBoot)项目打包(构建)成Docker镜像的几种常见方式

    Java(SpringBoot)项目打包(构建)成Docker镜像的几种常见方式

    在对Spring Boot应用程序进行Docker化时,为应用程序选择正确的基础镜像非常重要,下面这篇文章主要给大家介绍了关于Java(SpringBoot)项目打包(构建)成Docker镜像的几种常见方式,需要的朋友可以参考下
    2023-12-12
  • springboot加载外部jar的项目实践

    springboot加载外部jar的项目实践

    本文主要介绍了springboot加载外部jar的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-02-02
  • 解决java执行cmd命令调用ffmpeg报错Concat error - No such filter ''[0,0]''问题

    解决java执行cmd命令调用ffmpeg报错Concat error - No such filter ''[0,0]

    这篇文章主要介绍了java执行cmd命令,调用ffmpeg报错Concat error - No such filter '[0,0]'解决方法,本文通过截图实例代码说明给大家介绍的非常详细,对大家的工作或学习有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03

最新评论