在 Func 里实现登录
ctx.auth.login 给已有用户签发会话,用来做 SDK 表达不了的免密码验证码登录,或者在登录时跑封禁、实名、多租户这类自己的规则。含完整代码、一条不能写错的规则,以及每次登录的审计在哪看。
站点的登录默认用 useAuth().login() 就够了。要用 Func 实现登录只有两种情况:
做免密码的验证码登录(SDK 表达不了),或者登录时要跑你自己的服务端规则——封禁名单、
必须先完成实名、多租户按域名限制谁能登进哪个租户。
核心是 ctx.auth.login(ref):给一个已有用户签发会话。它不接收密码也不接收验证码,
「凭什么允许登录」由你的代码决定。
先记住一条规则
这条写在最前面,因为它是这个能力唯一会出大事的地方:
ctx.auth.login的参数必须来自服务端刚刚验证过的事实。 先用ctx.users.find或ctx.users.checkPassword拿到用户,再把user.id交给login。直接写ctx.auth.login({ email: input.email })意味着「浏览器说自己是谁就是谁」——那不是登录, 是让任何人以任何账号身份进来。
为什么单独强调它:改密码写错了,真实用户会被锁在门外从而察觉;冒名登录悄无声息, 没有任何人会发现。
验证码登录(免密码)
需要项目已连接邮件集成,见 使用集成发送邮件与验证码。
// /backend/func/auth/login.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'
const SCENE = 'login'
// 第一步:要验证码。与找回密码同一套写法——先查再发,两条分支返回值一样
export function requestCode(input: { email?: string }, ctx: TalizenFuncContext) {
const email = (input.email ?? '').trim()
if (!email) throw new Error('请填写邮箱')
const user = ctx.users.find({ email })
if (user) {
try {
ctx.email.sendCode({ to: email, scene: SCENE })
} catch (err) {
console.error('send login code failed', String(err))
}
}
return { ok: true }
}
// 第二步:验码并登录
export function withCode(
input: { email?: string; code?: string },
ctx: TalizenFuncContext,
) {
const email = (input.email ?? '').trim()
const code = (input.code ?? '').trim()
if (!email || !code) throw new Error('请填写邮箱和验证码')
const user = ctx.users.find({ email })
if (!user) throw new Error('验证码错误或已过期')
if (!ctx.email.verifyCode({ to: email, scene: SCENE, code }))
throw new Error('验证码错误或已过期')
// 用 find 回来的 user.id,不是 input.email
return ctx.auth.login({ userId: user.id })
}
返回的是登录后的用户对象;会话 cookie 由平台写在这次响应上,页面拿到返回值就等于已登录。 Func 代码看不到会话 token,也没有办法自己造一个。
密码登录 + 你自己的规则
export function withPassword(
input: { account: string; password: string },
ctx: TalizenFuncContext,
) {
// 失败次数与平台的 /auth/login 共用同一份额度:错 5 次锁 1 小时,
// 所以这条路不能被拿来绕过登录锁定爆破密码
if (!ctx.users.checkPassword({ account: input.account, password: input.password }))
throw new Error('账号或密码错误')
const user = ctx.users.find({ account: input.account })
if (!user) throw new Error('账号或密码错误')
// 这里才是用 Func 做登录的理由:平台管不了的规则
if (ctx.db.get('banned_users', user.id)) throw new Error('该账号已被封禁')
if (!user.profile?.onboarded) return { ok: false, next: '/onboarding' }
return ctx.auth.login({ userId: user.id })
}
连续验错太多次时 checkPassword 抛 429(不是返回 false)。
给它单独的提示文案「尝试次数过多,请稍后再试」,否则用户会一直重试到锁死。
页面代码
import { invoke } from 'talizen/func'
import { useAuth } from 'talizen/auth'
async function loginWithCode(email: string, code: string) {
await invoke('auth/login.withCode', { email, code })
// 会话 cookie 已经随这次响应下发,刷新一下 useAuth() 的状态即可
await useAuth().refresh()
window.location.href = '/account'
}
登出仍然用 useAuth().logout(),不需要写 Func。
每一次登录都有记录
平台会记下每一次会话签发:时间、来源、IP、设备。来源分四种——直连登录、
OAuth(带 provider)、注册、以及 Func(带是哪个 Func 文件签的)。
在编辑器 后端 → 用户 里,某个用户那一行的菜单 → 登录记录 就能看到。
这不只是给你看登录历史用的。Func 里的登录是唯一一条「站点代码说谁登录就谁登录」的路, 所以哪个文件签发了哪个账号的会话必须能查到——上面那条规则万一写错了,这里是唯一能发现的地方。
其它要知道的
| 情况 | 行为 |
|---|---|
浏览器直连 /auth/login | 照旧可用。没有「把登录收口到 Func」的开关,两者并存;你可以只给需要的入口写 Func |
| 用户不存在 | login 抛 404。别把它原样回给浏览器——统一回「账号或密码错误」「验证码错误」这类不区分的文案 |
| 账号被停用 | 抛 403,与直连登录一致 |
| 指人的三个字段 | email / account / userId 只能填一个,填两个报 400 |
| 只用第三方登录的账号 | login 照样能签发会话(它不看有没有密码);checkPassword 对这类账号返回 false |
| OAuth 登录 | 仍由平台处理,不要在 Func 里写回调或换 token |
相关:实现找回密码与修改密码、 注册时验证邮箱、 使用集成发送邮件与验证码。
