You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中HttpOnly Cookie未设置且浏览器不向后端发送的问题

Spring Boot迁移HttpOnly Cookie时浏览器不设置Cookie的问题排查与解决

问题背景

尝试将Spring Boot应用从local storage迁移至HttpOnly Cookie实现安全防护,但遇到以下核心问题:

  • 响应头存在Set-Cookie字段,但浏览器未设置Cookie(即使将HttpOnly设为false也无法在浏览器中看到)
  • 后续接口请求中request.getCookies()始终为null,导致接口返回401未授权

相关代码片段

1. CookieUtil工具类

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;

public class CookieUtil {

    public static void create(HttpServletResponse httpServletResponse, String name, String value, Integer maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setSecure(false);
        cookie.setHttpOnly(true);
        cookie.setMaxAge(maxAge);
        cookie.setPath("/");
        cookie.setDomain("localhost");
        httpServletResponse.addCookie(cookie);
    }

    public static void clear(HttpServletResponse httpServletResponse, String name) {
        Cookie cookie = new Cookie(name, null);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(0);
        httpServletResponse.addCookie(cookie);
    }
}

测试时将HttpOnly设为false,浏览器仍未显示该Cookie;setSecure已设为false适配本地开发环境

2. 登录接口方法

public AccessTokenResponse signIn(LoginDto loginDto) {
    AccessTokenResponse accessTokenResponse = authorizations.signIn(loginDto);
    String jwt= accessTokenResponse.getToken();
    int maxAge= (int)accessTokenResponse.getExpiresIn();
    CookieUtil.create(response, "accessToken", jwt, maxAge);
    return accessTokenResponse;
}

使用Keycloak作为身份管理器,能正常获取token,响应头存在Set-Cookie字段,但浏览器未完成Cookie设置

3. 获取员工信息接口

@GetMapping("/employee")
public ResponseEntity<EmployeeDto> getEmployeeByKeycloakId(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    String token = null;
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("accessToken")) {
                token = cookie.getValue();
                break;
            }
        }
    }
    if (token == null) {
        return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
    }
    String userid= tokenService.getUserIdFromToken(token);
    return new ResponseEntity<>(
            employeeMapper.toEmployee(this.employeeUseCases.getByKeycloakId(userid)),
            HttpStatus.OK
    );
}

接口始终返回401,request.getCookies()为null,说明浏览器未向后端发送Cookie

4. 前端代码

登录请求

signIn(user: User, rememberMe: boolean): Observable<any> {
    return this._httpClient.post(`${this._apiUrl}/login`, user).pipe(
        switchMap((response: any) => {
            if (response.access_token) {   
                if(rememberMe){
                    this._storageService.setItem('rememberMe', JSON.stringify(rememberMe));
                }
            }
            this._authenticated = true;
            return this._userService.get();
        })
    );
}

获取员工信息请求(已设置withCredentials)

get(): any {
    return this._httpClient
        .get<User>(`${this._apiUrl}/employee`, { withCredentials: true })
        .pipe(
            tap((user) => {
                console.log(user)
                this._user.next(user);
            })
        );
}

后端跨域配置

@CrossOrigin(origins = "http://localhost:4200", allowCredentials = "true")
@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeController 

@CrossOrigin(origins = "http://localhost:4200", allowCredentials = "true")
@RestController
@RequestMapping("/api/v1/auth")
public class AuthorisationController {

解决方案

1. 修正Cookie的Domain配置

本地开发时,后端运行在localhost:8080、前端在localhost:4200,设置cookie.setDomain("localhost")会触发浏览器的同源校验逻辑(不同端口属于不同源,部分浏览器对localhost的Domain处理有特殊限制)。直接移除cookie.setDomain("localhost"),让浏览器自动绑定当前请求的域名,确保Cookie能被正确识别。

2. 登录请求添加withCredentials: true

前端登录请求是触发Set-Cookie的核心请求,必须开启withCredentials才能让浏览器接收跨域返回的Cookie。修改登录请求代码:

signIn(user: User, rememberMe: boolean): Observable<any> {
    return this._httpClient.post(`${this._apiUrl}/login`, user, { withCredentials: true }).pipe(
        switchMap((response: any) => {
            if (response.access_token) {   
                if(rememberMe){
                    this._storageService.setItem('rememberMe', JSON.stringify(rememberMe));
                }
            }
            this._authenticated = true;
            return this._userService.get();
        })
    );
}

3. 验证Cookie的MaxAge参数

确认Keycloak返回的expiresIn转换为int后是正数,避免因MaxAge为负数或0导致Cookie立即过期。

4. 全局配置跨域(可选但更可靠)

替代控制器层面的@CrossOrigin,全局配置跨域确保所有请求都能正确处理:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:4200")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

5. 浏览器调试验证

  • 在开发者工具的Application > Cookies面板查看是否存在accessToken(HttpOnly为true时不会在document.cookie中显示,但会在此面板列出)
  • 检查登录请求的响应头,确认Set-Cookie字段格式正确,无语法错误
  • 查看员工信息请求的请求头,确认是否携带Cookie字段

内容的提问来源于stack exchange,提问作者Oussema Sahbeni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 17:56:06