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

如何用Clerk实现SAAS系统前后端及CLI统一认证?求代码示例

使用Clerk实现CLI与Web端统一认证方案

核心流程

完全复刻GitHub CLI的认证逻辑,步骤如下:

  1. CLI生成随机临时验证码(如6位字符)+ 唯一state值防CSRF
  2. CLI自动打开浏览器,跳转到Web应用专属CLI认证页面,携带临时码和state
  3. 用户在Web端确认身份(Clerk已自动处理登录态)并授权CLI访问
  4. Web端通过Clerk API生成个人访问令牌(PAT),关联当前用户
  5. CLI轮询后端接口,获取生成的PAT并存储到本地配置文件
  6. 后续CLI请求后端时,携带该PAT完成身份验证

Web端配置(基于已有的Next.js + Clerk项目)

1. 新增CLI认证页面

在app/cli-auth/page.tsx创建页面,接收CLI传来的参数并引导用户授权:

'use client';
import { useAuth } from '@clerk/nextjs';
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function CliAuthPage({ searchParams }: { searchParams: { code: string; state: string } }) {
  const { user, isLoaded } = useAuth();
  const [status, setStatus] = useState<'idle' | 'authorizing' | 'done'>('idle');
  const { code, state } = searchParams;

  useEffect(() => {
    if (!isLoaded || !user || status !== 'idle') return;

    setStatus('authorizing');
    axios.post('/api/cli/generate-pat', {
      userId: user.id,
      tempCode: code,
      state
    })
    .then(() => setStatus('done'))
    .catch(err => {
      console.error(err);
      setStatus('idle');
    });
  }, [isLoaded, user, status, code, state]);

  if (!isLoaded) return <div>加载中...</div>;
  if (!user) return <div>请先登录</div>;

  return (
    <div className="p-8 max-w-md mx-auto">
      {status === 'authorizing' && <div>正在生成CLI令牌...</div>}
      {status === 'done' && <div>令牌已生成,可关闭此页面返回CLI</div>}
    </div>
  );
}

2. 后端API生成PAT

在app/api/cli/generate-pat/route.ts创建路由,使用Clerk Server SDK生成令牌:

import { clerkClient } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { userId, tempCode, state } = await req.json();

  // 生产环境需将临时码+state存入Redis并设置过期时间,此处省略校验逻辑
  const pat = await clerkClient.users.createUserToken(userId, {
    name: 'CLI Access Token',
    expiresIn: '365d',
    permissions: ['read:data', 'write:data'] // 按需配置权限
  });

  // 将PAT与临时码关联存储(生产用Redis替代内存)
  globalThis.cliTokens = globalThis.cliTokens || {};
  globalThis.cliTokens[tempCode] = { pat, state };

  return NextResponse.json({ success: true });
}

3. 后端API供CLI轮询令牌

在app/api/cli/get-pat/route.ts创建轮询接口:

import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const tempCode = searchParams.get('code');
  const state = searchParams.get('state');

  if (!tempCode || !state) {
    return NextResponse.json({ error: 'Missing params' }, { status: 400 });
  }

  const stored = globalThis.cliTokens?.[tempCode];
  if (!stored || stored.state !== state) {
    return NextResponse.json({ status: 'pending' });
  }

  // 返回令牌后删除存储,防止重复获取
  delete globalThis.cliTokens[tempCode];
  return NextResponse.json({ pat: stored.pat });
}

CLI端实现(Node.js示例)

1. 核心认证逻辑

const open = require('open');
const axios = require('axios');
const fs = require('fs');
const path = require('path');

// 生成随机字符串工具函数
function generateRandomString(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}

async function authLogin() {
  const tempCode = generateRandomString(6);
  const state = generateRandomString(16);

  // 打开Web认证页面
  const authUrl = `http://localhost:3000/cli-auth?code=${tempCode}&state=${state}`;
  console.log(`正在打开浏览器:${authUrl}`);
  await open(authUrl);

  console.log('请在浏览器中完成授权,等待令牌生成...');

  // 轮询获取令牌
  let interval = setInterval(async () => {
    try {
      const res = await axios.get('http://localhost:3000/api/cli/get-pat', {
        params: { code: tempCode, state }
      });

      if (res.data.pat) {
        clearInterval(interval);
        // 存储令牌到本地配置文件(生产建议用keytar存系统密钥链)
        const configPath = path.join(process.env.HOME, '.mycli', 'config.json');
        fs.mkdirSync(path.dirname(configPath), { recursive: true });
        fs.writeFileSync(configPath, JSON.stringify({ token: res.data.pat }));
        console.log('认证成功!令牌已保存');
      }
    } catch (err) {
      console.error('轮询出错:', err.message);
    }
  }, 2000);
}

module.exports = { authLogin };

2. CLI命令集成

用commander库快速创建命令:

const { Command } = require('commander');
const { authLogin } = require('./auth');

const program = new Command();

program
  .command('auth login')
  .description('登录CLI')
  .action(authLogin);

program.parse(process.argv);

后端验证CLI令牌

后续CLI请求时携带Authorization: Bearer <PAT>,后端验证逻辑示例:

// app/api/protected/route.ts
import { clerkClient } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const authHeader = req.headers.get('authorization');
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = await clerkClient.verifyToken(token);
    const user = await clerkClient.users.getUser(decoded.sub);
    
    // 校验权限
    if (!decoded.permissions.includes('read:data')) {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    }

    return NextResponse.json({ user: { id: user.id, email: user.emailAddresses[0].emailAddress } });
  } catch (err) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }
}

关键注意事项

  • 临时码和state必须严格校验,生产环境用Redis存储并设置5分钟过期时间
  • PAT权限遵循最小权限原则,按需分配
  • CLI存储令牌时优先用系统密钥链(如keytar库),避免明文存储
  • Web端认证页面需校验state参数,防止CSRF攻击

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 02:32:10