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

NextJS中授权码流:如何将PKCE code_verifier传入授权回调?

实现PKCE授权码流的正确方案(NextJS + openid-client@v5)

核心结论

必须通过state参数关联服务器端缓存来存储code_verifier,这是符合PKCE安全要求的标准做法,全程不会将敏感的code_verifier暴露给客户端或URL参数。

具体实现步骤

1. 修改授权URL生成逻辑(服务器端)

在服务器端生成PKCE参数、随机state,并将code_verifier以state为键存入分布式缓存(生产环境推荐Redis,开发环境可用内存缓存临时替代):

import { BaseClient, Issuer, generators } from 'openid-client';
import redis from './your-redis-client'; // 引入你的缓存客户端

export async function getAuthorizationUrl() {
  const issuer = await Issuer.discover('https://www.my-oidc-provider-endpoint');
  const client = new issuer.Client({
    client_id: 'myClientId',
    client_secret: 'myClientSecret',
    redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
  });

  // 生成PKCE参数
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  const state = generators.state(); // 生成唯一随机state

  // 将code_verifier存入缓存,设置10分钟过期(覆盖授权流程最长耗时)
  await redis.setex(`pkce_${state}`, 600, code_verifier);

  // 生成带PKCE和state的授权URL
  const authorizationUrl = client.authorizationUrl({
    scope: 'openid email profile rights',
    code_challenge,
    code_challenge_method: 'S256',
    state, // 关键:传递state用于后续关联code_verifier
  });

  return authorizationUrl;
}

2. 客户端跳转授权URL

保持原有逻辑,通过服务端渲染传递授权URL后跳转:

export async function getServerSideProps() {
  const authorizationUrl = await getAuthorizationUrl();
  return { props: { authorizationUrl } };
}

export default function LoginPage({ authorizationUrl }) {
  return (
    <button onClick={() => window.location.assign(authorizationUrl)}>
      登录
    </button>
  );
}

3. 修改回调接口(服务器端)

通过回调中的state从缓存取出code_verifier,完成令牌交换:

// /api/auth/callback.ts
import { authClientProvider } from '../utils/auth';
import redis from './your-redis-client';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const client = await authClientProvider.getClient();
  const params = client.callbackParams(req);
  const { state } = params;

  // 校验state是否存在
  if (!state) {
    return res.status(400).send('缺少state参数');
  }

  // 从缓存获取code_verifier
  const code_verifier = await redis.get(`pkce_${state}`);
  if (!code_verifier) {
    return res.status(400).send('无效或过期的state');
  }

  // 立即删除缓存,避免重复使用
  await redis.del(`pkce_${state}`);

  try {
    const tokenSet = await client.callback(
      `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
      params,
      { code_verifier }
    );

    setTokenCookies(req, res, tokenSet.access_token, tokenSet.refresh_token);
    return res.redirect(process.env.NEXT_PUBLIC_APP_URL);
  } catch (error) {
    console.error('认证回调失败:', error);
    return res.status(500).send('认证失败');
  }
}

关键注意事项

  • 禁止将code_verifier暴露在URL或客户端存储:URL参数会被浏览器历史、日志记录,客户端存储(如localStorage)存在XSS风险,会彻底破坏PKCE的安全防护作用。
  • 缓存必须用分布式存储:NextJS服务器是无状态的,内存缓存无法在多实例环境中共享,生产环境必须用Redis等分布式缓存。
  • 设置合理的缓存过期时间:建议设置为10-15分钟,既避免无效数据占用缓存,又覆盖授权流程的最长可能耗时。

关于客户端生成code_verifier的补充

虽然可以在客户端用原生crypto库生成code_verifier和code_challenge,但不推荐:

  • 需要将code_verifier存入HttpOnly、Secure、SameSite=Strict的Cookie中,回调接口从Cookie读取。
  • 这种方式仍存在XSS风险(即使HttpOnly Cookie能防读取,但若存在CSRF漏洞仍可能被利用),安全性远低于服务器端缓存方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 18:42:50