如何用Clerk实现SAAS系统前后端及CLI统一认证?求代码示例
使用Clerk实现CLI与Web端统一认证方案
核心流程
完全复刻GitHub CLI的认证逻辑,步骤如下:
- CLI生成随机临时验证码(如6位字符)+ 唯一state值防CSRF
- CLI自动打开浏览器,跳转到Web应用专属CLI认证页面,携带临时码和state
- 用户在Web端确认身份(Clerk已自动处理登录态)并授权CLI访问
- Web端通过Clerk API生成个人访问令牌(PAT),关联当前用户
- CLI轮询后端接口,获取生成的PAT并存储到本地配置文件
- 后续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
相关产品推荐
相关产品推荐

