6.5.6 登录异常怎么办——错误处理
一句话破题
第三方登录流程涉及多方交互,任何环节都可能出错。好的错误处理能让用户知道发生了什么、该怎么做,而不是面对一个空白页面。
核心价值
完善错误处理能让你:
- 提升用户体验,减少流失
- 快速定位问题根源
- 降低客服压力
常见错误类型
| 错误类型 | 常见原因 | 用户提示 |
|---|---|---|
| 用户取消授权 | 点击了"拒绝" | 您已取消授权,请重试 |
| state 验证失败 | CSRF 攻击/链接过期 | 请求已过期,请重新登录 |
| access_token 获取失败 | 网络问题/密钥错误 | 登录服务暂时不可用 |
| 用户信息获取失败 | token 过期/权限不足 | 无法获取您的信息 |
统一错误处理
typescript
// lib/oauth-errors.ts
export class OAuthError extends Error {
constructor(
public code: string,
public userMessage: string,
public debugMessage?: string
) {
super(userMessage)
this.name = 'OAuthError'
}
}
export const OAuthErrors = {
USER_CANCELLED: new OAuthError(
'user_cancelled',
'您已取消授权,如需登录请重试',
'User denied authorization'
),
STATE_MISMATCH: new OAuthError(
'state_mismatch',
'登录请求已过期,请重新登录',
'State parameter validation failed'
),
TOKEN_FAILED: new OAuthError(
'token_failed',
'登录服务暂时不可用,请稍后再试',
'Failed to exchange code for token'
),
USER_INFO_FAILED: new OAuthError(
'user_info_failed',
'无法获取您的账号信息,请重试',
'Failed to fetch user info'
),
BINDING_CONFLICT: new OAuthError(
'binding_conflict',
'该账号已绑定其他用户',
'OAuth account already linked to another user'
),
}回调处理示例
typescript
// app/api/auth/wechat/callback/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
try {
// 1. 检查用户是否取消授权
const error = searchParams.get('error')
if (error === 'access_denied') {
throw OAuthErrors.USER_CANCELLED
}
// 2. 验证 state
const state = searchParams.get('state')
if (!verifyState(state)) {
throw OAuthErrors.STATE_MISMATCH
}
// 3. 获取 code
const code = searchParams.get('code')
if (!code) {
throw new OAuthError('no_code', '授权失败', 'No code in callback')
}
// 4. 换取 token
const tokenRes = await fetch(TOKEN_URL)
if (!tokenRes.ok) {
throw OAuthErrors.TOKEN_FAILED
}
const tokenData = await tokenRes.json()
if (tokenData.errcode) {
console.error('WeChat token error:', tokenData)
throw OAuthErrors.TOKEN_FAILED
}
// 5. 获取用户信息
const userRes = await fetch(USER_INFO_URL)
if (!userRes.ok) {
throw OAuthErrors.USER_INFO_FAILED
}
// 6. 登录成功
const user = await handleOAuthLogin(userInfo)
await createSession(user)
return Response.redirect('/dashboard')
} catch (error) {
// 记录详细错误用于调试
console.error('OAuth callback error:', {
error,
url: request.url,
timestamp: new Date().toISOString(),
})
// 返回用户友好的错误页面
const errorCode = error instanceof OAuthError ? error.code : 'unknown'
const message = error instanceof OAuthError ? error.userMessage : '登录失败'
return Response.redirect(
`/login?error=${encodeURIComponent(errorCode)}&message=${encodeURIComponent(message)}`
)
}
}错误展示页面
tsx
// app/login/page.tsx
export default function LoginPage({ searchParams }) {
const error = searchParams.error
const message = searchParams.message
return (
<div>
{error && (
<div className="bg-red-50 border border-red-200 rounded p-4 mb-4">
<p className="text-red-700">{message || '登录失败,请重试'}</p>
{error === 'user_cancelled' && (
<p className="text-sm text-red-500 mt-1">
如果您想使用此服务,需要授权我们获取基本信息
</p>
)}
</div>
)}
<div className="space-y-4">
<button onClick={() => signIn('wechat')}>
微信登录
</button>
</div>
</div>
)
}监控与告警
typescript
// lib/error-reporting.ts
interface OAuthErrorLog {
code: string
provider: string
timestamp: string
ip?: string
userAgent?: string
debugMessage?: string
}
export async function reportOAuthError(
error: OAuthError,
request: Request,
provider: string
) {
const log: OAuthErrorLog = {
code: error.code,
provider,
timestamp: new Date().toISOString(),
ip: request.headers.get('x-forwarded-for') || undefined,
userAgent: request.headers.get('user-agent') || undefined,
debugMessage: error.debugMessage,
}
// 发送到日志服务
console.error('[OAuth Error]', JSON.stringify(log))
// 如果错误率过高,触发告警
// await checkErrorRate(provider)
}避坑指南
新手最容易犯的错
- 直接把后端错误信息展示给用户——泄露敏感信息
- 所有错误都显示"登录失败"——用户不知道如何解决
- 没有记录错误日志——无法排查问题
- 没有处理网络超时——用户等待后看到空白页
