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

Network Tab可见JWT Token,但JavaScript无法获取响应Authorization头

问题描述

通过React的JavaScript fetch API向Spring Boot后端发送登录请求,浏览器Network Tab显示响应包含带JWT Token的Authorization头,但在代码中调用res.headers.get('Authorization')时返回null,响应Body数据正常。

相关代码

前端fetch API代码

import { SIGN_IN, SIGN_UP } from "./ActionType";

export const signinAction = (data) => async (dispatch) => {
    try {
        const res = await fetch("http://localhost:5050/signin", {
            method: "GET",
            headers: {
                Authorization: "Basic " + btoa(data.email + ":" + data.password),
            },

        });
        console.log(res);
        const user = await res.json();
        console.log("signin user: ", user);
        const token = res.headers.get("Authorization");
        localStorage.setItem("token", token);
        dispatch({ type: SIGN_IN, payload: token });
        console.log("signin token: ", token);
    } catch (error) {
        console.log(error);
    }
};

export const signupAction = (data) => async (dispatch) => {
    try {
        const res = await fetch("http://localhost:5050/signup", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                email: data.email,
                name: data.name,
                userName: data.userName,
                password: data.password,
            }),
        });
        const user = await res.json();
        console.log("signup user: ", user);
        dispatch({ type: SIGN_UP, payload: user });
    } catch (error) {
        console.log(error);
    }
};

Spring Boot登录接口代码

package com.vinitpk.instagramapi.instagram.controller;

import com.vinitpk.instagramapi.instagram.exception.UserException;
import com.vinitpk.instagramapi.instagram.model.User;
import com.vinitpk.instagramapi.instagram.repository.UserRepository;
import com.vinitpk.instagramapi.instagram.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
public class AuthController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @PostMapping("/signup")
    public ResponseEntity<User> registerUserHandler(@RequestBody User user) throws UserException {
        User createdUser = userService.registerUser(user);
        System.out.println(createdUser.getUserName());
        return new ResponseEntity<User>(createdUser, HttpStatus.OK);
    }

    @GetMapping("/signin")
    public ResponseEntity<User> signinUserHandler(Authentication authentication ) throws BadCredentialsException {
        Optional<User> optionalUser = userRepository.findByEmail(authentication.getName());
        System.out.println(optionalUser.get());
        if(optionalUser.isPresent()){
            return new ResponseEntity<User>(optionalUser.get(),HttpStatus.OK);
        }
        throw new BadCredentialsException("Invalid User name or password");
    }

}

JWT Token生成过滤器代码

package com.vinitpk.instagramapi.instagram.configuration;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@Component
public class JwtTokenGeneratorFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException
    {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(authentication != null){
            SecretKey key = Keys.hmacShaKeyFor(SecurityContext.JWT_KEY.getBytes());
            String jwt = Jwts.builder()
                    .issuer("instagram")
                    .issuedAt(new Date())
                    .claim("authorities", populateAuthorities(authentication.getAuthorities()))
                    .claim("userName", authentication.getName())
                    .expiration(new Date(new Date().getTime() + (10 * 60 * 60 * 1000)))
                    .signWith(key).compact();
            response.setHeader(SecurityContext.Header, jwt);
            System.out.println(jwt);
        }
        filterChain.doFilter(request, response);
    }

    public String populateAuthorities(Collection<? extends GrantedAuthority> collection){

        Set<String> authorities = new HashSet<>();
        for(GrantedAuthority authority : collection){
            authorities.add(authority.getAuthority());
        }

        return String.join(",", authorities);
    }

    protected boolean shouldNotFilter(HttpServletRequest httpServletRequest) throws ServletException{
        return !httpServletRequest.getServletPath().equals("/signin");
    }
}

原因分析

这是浏览器CORS响应头暴露限制导致的:浏览器默认仅允许前端访问部分标准响应头(如Content-Type、Cache-Control),对于自定义或非标准响应头(此处的Authorization),必须由后端通过Access-Control-Expose-Headers明确声明允许暴露,否则前端无法通过res.headers.get()获取到该头信息。

解决方案

1. Spring Boot后端配置CORS,暴露Authorization头

可以选择全局配置或局部接口配置:

方式一:全局CORS配置

创建CORS配置类,统一设置跨域规则并暴露Authorization头:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许的前端域名,生产环境建议指定具体域名,不要用*
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        // 关键:声明暴露Authorization响应头给前端
        config.setExposedHeaders(List.of("Authorization"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

方式二:局部接口配置

若仅需给登录接口配置,可在AuthController类上添加@CrossOrigin注解:

@CrossOrigin(origins = "http://localhost:3000", exposedHeaders = "Authorization")
@RestController
public class AuthController {
    // 原有代码不变
}

2. 前端验证

重启后端服务后,前端再次调用登录接口,此时res.headers.get("Authorization")即可正确获取到JWT Token。

注意:后续请求携带Token时,需要给Token添加Bearer 前缀(如果后端验证时要求),示例:

const token = res.headers.get("Authorization");
localStorage.setItem("token", `Bearer ${token}`);

内容的提问来源于stack exchange,提问作者Vinit Pk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 19:44:53