导读:本期聚焦于杨建军创作的《JS前端如何对接Spring OAuth2安全认证?完整流程与实战代码详解》,敬请观看详情。前端页面发起请求却总是收到401,刷新令牌该怎么存才安全,跨域携带Cookie又有哪些坑?这些问题的根源大多出在JS与Spring OAuth2的配合方式上。本文从前端视角拆解OAuth2授权码模式的完整链路,讲解access_token与refresh_token的获取、存储和自动刷新策略,演示如何用axios拦截器实现无感知续期,并给出与Spring Authorization Server对接时的CORS配置和PKCE增强方案。同时分析localStorage与httpOnly Cookie两种令牌存储方式的利弊,帮助你搭建一套既安全又好维护的前后端分离认证体系。

前后端分离架构下,认证这块最容易踩坑。后端用Spring OAuth2搭好了授权服务器,前端JS这边却经常搞得一团糟:要么token拿到了却带不上,要么token过期后页面直接卡死,要么刷新令牌放在localStorage里被人一扫而空。其实JS与Spring OAuth2的配合有一套相对固定的套路,理解了授权码流程中每一步前端要做什么,问题就解决了一大半。本文从前端开发者的角度出发,把整个对接过程掰开揉碎讲清楚。

JS前端如何对接Spring OAuth2安全认证?完整流程与实战代码详解

一、先搞清楚前端在OAuth2授权码流程中的位置

Spring OAuth2最推荐的模式是授权码模式(Authorization Code Flow),整个流程里前端要做的事情其实只有三件:引导用户跳转授权页、用授权码换令牌、带着令牌访问资源。很多前端开发者搞不定OAuth2,本质原因是把整个流程想象成了普通的登录接口调用,以为发一个POST请求就能拿到token,结果发现一堆重定向和跨域问题。

标准的授权码流程是这样的:用户访问前端页面,前端发现本地没有令牌,就把浏览器重定向到授权服务器的/oauth2/authorize端点,URL里带上client_id、redirect_uri、response_type=code、scope等参数。用户在授权页登录并同意授权后,Spring授权服务器会带着一个code参数重定向回你指定的redirect_uri。前端页面加载时检查URL中有没有code参数,有就拿它去/oauth2/token端点换令牌。换到令牌后,把access_token存起来,后续请求统一放到Authorization头里。

注意一个细节:换令牌这一步,如果前端是公开客户端(比如纯浏览器SPA应用,没有后端中转),是不应该携带client_secret的,因为浏览器里藏不住任何密钥。这时候要用PKCE(Proof Key for Code Exchange)来代替密钥做防护。PKCE的思路是前端先生成一个随机字符串code_verifier,算出它的SHA-256哈希作为code_challenge传给授权端点,换令牌时再把原始的code_verifier传过去,授权服务器验证两者匹配才发令牌。

二、令牌存储方案的选择与实现

拿到令牌后第一件事就是决定存哪里。主流方案有两种:localStorage和httpOnly Cookie,各有明显的优缺点。

localStorage的读写最方便,JS可以直接操作,代码也很直观:

function saveTokens(tokenResponse) {
  localStorage.setItem('access_token', tokenResponse.access_token);
  localStorage.setItem('refresh_token', tokenResponse.refresh_token);
  // access_token通常2小时过期,提前存好过期时间点
  const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
  localStorage.setItem('expires_at', String(expiresAt));
}

function getAccessToken() {
  return localStorage.getItem('access_token');
}

function isTokenExpired() {
  return Date.now() > Number(localStorage.getItem('expires_at'));
}</code>

但localStorage的问题是任何一段被注入的XSS脚本都能读到令牌,一旦泄露攻击者就能直接冒充用户。httpOnly Cookie方案则把令牌写到只有浏览器能读、JS读不到的Cookie里,配合Spring后端的CSRF防护一起使用,安全性明显更高。代价是每次都要处理跨域Cookie的问题,前端请求必须设置withCredentials: true,后端CORS配置里也要明确允许凭证。

如果是企业内部系统或者安全要求高的项目,建议令牌相关的操作都由一个轻量的BFF层(Backend For Frontend)代理,前端只跟同源的BFF交互,Cookie由BFF和授权服务器之间流转,浏览器端完全接触不到令牌本体。这是目前社区公认比较稳的架构。

三、用axios拦截器实现请求携带与无感知刷新

令牌有了,接下来就是让每个请求自动带上它,并且在过期时自动刷新。用axios的拦截器可以做到一次封装、全局生效。

请求拦截器负责在发请求前塞入Authorization头,响应拦截器负责捕获401并触发刷新逻辑:

import axios from 'axios';

const api = axios.create({ baseURL: 'https://api.ipipp.com' });

// 请求拦截:统一塞令牌
api.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = 'Bearer ' + token;
  }
  return config;
});

let refreshing = null; // 保存进行中的刷新请求,防止并发重复刷新

api.interceptors.response.use(
  res => res,
  async err => {
    const { config, response } = err;
    if (response && response.status === 401 && !config._retried) {
      config._retried = true;
      // 保证同一时刻只有一个刷新请求,其他请求等它完成
      refreshing = refreshing || refreshToken();
      const newToken = await refreshing;
      refreshing = null;
      config.headers.Authorization = 'Bearer ' + newToken;
      return api(config); // 重发原请求
    }
    return Promise.reject(err);
  }
);

async function refreshToken() {
  const res = await axios.post('https://auth.ipipp.com/oauth2/token',
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: localStorage.getItem('refresh_token'),
      client_id: 'spa-client'
    }),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
  );
  localStorage.setItem('access_token', res.data.access_token);
  localStorage.setItem('refresh_token', res.data.refresh_token);
  return res.data.access_token;
}

这里有个容易被忽略的坑:页面同时发出多个请求,令牌恰好过期时,多个请求都会收到401,如果没有并发控制,会触发多次刷新,而Spring授权服务器默认refresh_token是旋转的(用一次就作废),第二次刷新就会失败。上面代码用refreshing变量把刷新请求合并成一个,所有401的请求都等同一个刷新结果,这是生产环境必须处理的细节。

另外注意Content-Type必须是application/x-www-form-urlencoded,OAuth2 token端点不接收JSON格式的请求体,很多前端在这里折腾半天收到的都是400错误,原因就是发的是JSON。

四、跨域与Spring端的配套配置

前端配置都对了还报CORS错误,说明Spring这边没配好。授权服务器和资源服务器需要分别处理跨域。对于token端点的跨域,Spring Authorization Server需要显式放行OPTIONS预检请求,否则预检被拦截后真正的POST请求根本发不出去。

资源服务器侧的典型配置如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .cors(cors -> cors.configurationSource(corsSource()))
      .authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/public/**").permitAll()
        .anyRequest().authenticated())
      .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(Customizer.withDefaults()));
    return http.build();
  }

  @Bean
  public CorsConfigurationSource corsSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.ipipp.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true); // 使用Cookie方案时必须为true
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
  }
}

两个容易踩的点:一是setAllowCredentials(true)setAllowedOrigins里的通配符不能同时使用,带了凭证就必须写明确的域名,否则Spring会直接抛异常;二是前端如果用Cookie存session或令牌,axios实例要配置withCredentials: true,两边缺一个Cookie都带不过去。

整体来说,JS与Spring OAuth2的配合没有太多玄学,核心就是理清授权码流程里前端的三个动作,选对令牌存储方案,把拦截器和并发刷新处理好,再让后端把CORS配规范。按这套思路落地,一套安全且体验流畅的认证体系就能稳定跑起来了。

Spring OAuth2JS前端认证JWT令牌修改时间:2026-09-13 15:10:50

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56081.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。