# 在 Func 里实现登录｜Creght

> 用 ctx.auth.login 在 Creght Func 里实现验证码登录与带自有规则的密码登录：参数必须来自服务端已验证的事实，每次会话签发都有审计可查。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)

登录与用户

- [注册时验证邮箱](/api/auth-verified-registration.md)
- [实现找回密码与修改密码](/api/auth-password-reset.md)
- [在 Func 里实现登录](/api/auth-func-login.md)

本页目录

- [先记住一条规则](#rule)
- [验证码登录（免密码）](#code-login)
- [密码登录 \+ 你自己的规则](#password-login)
- [页面代码](#page)
- [每一次登录都有记录](#audit)
- [其它要知道的](#notes)

登录与用户/在 Func 里实现登录

# 在 Func 里实现登录

ctx.auth.login 给已有用户签发会话，用来做 SDK 表达不了的免密码验证码登录，或者在登录时跑封禁、实名、多租户这类自己的规则。含完整代码、一条不能写错的规则，以及每次登录的审计在哪看。

复制 Markdown 链接

站点的登录默认用 `useAuth().login()` 就够了。要用 Func 实现登录只有两种情况：
**做免密码的验证码登录**（SDK 表达不了），或者 **登录时要跑你自己的服务端规则**——封禁名单、
必须先完成实名、多租户按域名限制谁能登进哪个租户。

核心是 `ctx.auth.login(ref)`：给一个 **已有** 用户签发会话。它不接收密码也不接收验证码，
「凭什么允许登录」由你的代码决定。

**本文范围**

覆盖 `ctx.auth.login`，
配合 `ctx.users.checkPassword`、 `ctx.users.find` 和
`ctx.email.sendCode` / `verifyCode`。改密码见
[实现找回密码与修改密码](/api/auth-password-reset.md)，注册见
[注册时验证邮箱](/api/auth-verified-registration.md)。

## 先记住一条规则

这条写在最前面，因为它是这个能力唯一会出大事的地方：

> **`ctx.auth.login` 的参数必须来自服务端刚刚验证过的事实。**
> 先用 `ctx.users.find` 或 `ctx.users.checkPassword` 拿到用户，再把
> `user.id` 交给 `login`。直接写
> `ctx.auth.login({ email: input.email })` 意味着「浏览器说自己是谁就是谁」——那不是登录，
> 是让任何人以任何账号身份进来。

为什么单独强调它：改密码写错了，真实用户会被锁在门外从而察觉； **冒名登录悄无声息**，
没有任何人会发现。

## 验证码登录（免密码）

需要项目已连接邮件集成，见
[使用集成发送邮件与验证码](/api/func-email-integration.md)。

```typescript
// /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**，也没有办法自己造一个。

## 密码登录 \+ 你自己的规则

```typescript
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`）。
给它单独的提示文案「尝试次数过多，请稍后再试」，否则用户会一直重试到锁死。

## 页面代码

```tsx
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 |

相关： [实现找回密码与修改密码](/api/auth-password-reset.md)、
[注册时验证邮箱](/api/auth-verified-registration.md)、
[使用集成发送邮件与验证码](/api/func-email-integration.md)。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

微信客服

## 链接

- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://creghtlib.site.creght.com)
- [动效库](/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [自己写代码 vs Creght](/compare/self-coding.md)
- [外包 vs 自己做](/compare/outsourcing.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
