Blitz 内置了会话管理,可用于身份提供商或任何类型的身份验证。
会话管理包含以下功能:
你可以通过 SessionContext
对象登录、注销或以其它方式
修改会话,该对象可在服务器上的任何地方访问。
对于登录,你的 UI 中将有可以提交如下所示的登录 mutation 的表单组件。
// app/auth/mutations/login.ts
import { Ctx } from "blitz"
export default async function login(input: SomeTSInputType, ctx: Ctx) {
// 1. 验证输入的数据
// 2. 验证用户凭据
// 3. 获取用户的数据
// 4. 创建一个新的 session(登录)
await ctx.session.$create({ userId: user.id, role: user.role })
}
对于注销,你的 UI 中将有可以提交如下所示的注销 mutation 的表单组件。
撤销一个会话将立即删除所有客户端查询缓存,从而导致页面的所有查询都需要被重 新加载。这可以确保删除缓存中的任何敏感数据。
// app/auth/muations/logout.ts
import { Ctx } from "blitz"
export default async function logout(_: any, ctx: Ctx) {
// 1. 撤销当前用户的 session,进行注销
return await ctx.session.$revoke()
}
每个会话都有 PublicData
,这些数据可用被客户端使用,并且由
于它存储在任何 JavaScript 代码可读取的 cookie 中,有可能被第三方库读取。这
常被用来存储当前的用户 ID、用户角色,可能还有当前的组织 ID。
你可以在任何 query 或 mutation 中更改会话的公共数据,如下所示:
// app/mutations/someMutation.ts
import { Ctx } from "blitz"
export default async function someMutation(input: any, ctx: Ctx) {
// 这会将输入数据与当前 publicData 中已有的数据合并
await ctx.session.$setPublicData({ orgId: 1 })
}
SessionContext
可以从 ctx
中使用,因为
sessionMiddleware
配置于 blitz.config.js
中,它可以作为第二个参数提供
给所有 queries 和 mutations。
// app/queries/someQuery.ts
import { Ctx } from "blitz"
export default async function someQuery(input: any, ctx: Ctx) {
// 访问 SessionContext 类
ctx.session.userId
ctx.session.role
ctx.session.$create(/*...*/)
return
}
getServerSideProps
或 API 路由中你还可以在 getServerSideProps
或 API 路由中使用 getSession
获取会话上
下文,如下所示:
import { getSession } from "blitz"
export const getServerSideProps = async ({ req, res }) => {
const session = await getSession(req, res)
console.log("User ID:", session.userId)
return { props: {} }
}
Blitz 提供了一个 useSession()
Hook,它返回带有 isLoading
属性的
PublicData
。这个 Hook 可以在你的应用中的任何地方使用。
注意:useSession()
默认使用 Suspense,所以你需要在包含它的组件树之上有一
个 <Suspense>
组件。或者你可以设置 useSession({ suspense: false })
来
禁用 Suspense。
import { useSession } from "blitz"
function SomeComponent() {
const session = useSession()
session.userId
session.role
return /*... */
}
如果你正在使用 getServerSideProps
,那么你可以
使用 initialPublicData
选项将会话公共数据传递给 useSession()
。
import { useSession, GetServerSideProps } from "blitz"
export const getServerSideProps: GetServerSideProps = async ({
req,
res,
}) => {
const session = await getSession(req, res)
return { props: { initialPublicData: session.$publicData } }
}
const SomePage: BlitzPage = ({ initialPublicData }) => {
const session = useSession({ initialPublicData })
return /*... */
}
生产环境中,你必须提供其值至少 32 个字符的 SESSION_SECRET_KEY
环境变量。
这是你用于签署 JWT 令牌的秘钥。
在 macOS 和 Linux 上,你可以通过在终端运行 openssl rand -hex 16
来生成它
。
如果用户没有登录,将自动为他们创建一个匿名会话。你可以和登录用户类似地将
ctx.session.$setPublicData()
和 ctx.session.$setPrivateData()
用于匿名
会话。当用户登录时,你为匿名会话设置的任何数据都将自动传输到身份验证过的会
话中。
匿名会话通过将 JWT 令牌存储到客户端上实现永不过期的 httpOnly cookie。
匿名会话的 PublicData
保存到会话 JWT 中,不存储在数据库中。只有你调用
session.$setPrivateData()
,匿名会话才会保存在你的数据库中。
匿名会话将在第一次网络请求时创建,无论这次请求是 SSR 还是 API。只要
sessionMiddleware
位于该请求的中间件中就会执行这种情况。
一个用例是为匿名用户保存购物车内容。如果匿名用户稍后注册或登录,匿名会话数 据可以合并到为其新生成的经过身份验证的会话中。
匿名会话 PublicData
看起来会是这样:
{
userId: null,
}
如果使用 TypeScript,第一次在 types.ts
中更新 Session.PublicData
会像
这样:
import {DefaultCtx, SessionContext, SimpleRolesIsAuthorized} from "blitz"
import {User} from "db"
// 注意:你应该切换到 Postgres 并且给角色类型使用一个 DB 枚举类型
export type Role = "ADMIN" | "USER"
declare module "blitz" {
export interface Ctx extends DefaultCtx {
session: SessionContext
}
export interface Session {
isAuthorized: SimpleRolesIsAuthorized<Role>
PublicData: {
userId: User["id"]
role: Role
+ orgId: number
}
}
}
接着改变所有使用到 ctx.session.$create()
的地方为其添加新的字段:
ctx.session.$create({ userId: 1, role: "ADMIN", orgId: 1 })
你也可以使用 ctx.session.$setPublicData()
来为已登录的用户更新会话数据。
这将与已存在的公共数据进行 合并。
ctx.session.$setPublicData({ orgId: 1 })
要访问客户端上的公共数据:
import { useSession } from "blitz"
function SomeComponent() {
const session = useSession()
session.orgId
return /*... */
}
在服务端上访问公共数据:
// app/queries/someQuery.ts
import { Ctx } from "blitz"
export default async function someQuery(input: any, ctx: Ctx) {
// 访问 SessionContext 类
ctx.session.orgId
return
}
你可以通过将对象传递给 sessionMiddleware
工厂函数来自定义会话管理。
// blitz.config.js
const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz")
module.exports = {
middleware: [
sessionMiddleware({
cookiePrefix: "my-app",
sessionExpiryMinutes: 1234,
isAuthorized: simpleRolesIsAuthorized,
}),
],
}
可用选项:
type SessionConfig = {
cookiePrefix?: string /* 默认:'blitz' */
sessionExpiryMinutes?: number /* 默认:30 days */
sameSite?: "strict" | "lax" | "none" /* 默认:'lax' */
domain?: string /* 默认:undefined。可以被设置为 `.yourDomain.com` 来工作在子域名上 */
publicDataKeysToSyncAcrossSessions?: string[] /* 默认:['role', 'roles'] */
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: string | number) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (
handle: string,
session: Partial<SessionModel>
) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
isAuthorized: ({ ctx: any, args: [...unknown] }) => boolean
}
interface SessionModel extends Record<any, any> {
handle: string
userId?: string | number
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
publicData?: string
privateData?: string
}
默认情况下,会话持久性是 Prisma 的零配置。但你可以自定义它来将会话保存在其 他地方,例如 Redis。如果你有 Prisma 但想要自定义用户或会话模型上的属性名称 ,你也可以自定义它。
可以通过重写上面在 SessionConfig
中定义的数据库访问函数来自定义会话持久
性。这个函数可以做任何事情,但必须符合定义的输入和输出类型。
作为参考,这里是 适用于 Prisma 的默认配置。
当从客户端向 API 端口发出请求时,你需要在 anti-csrf
header 中包含
anti-CSRF 令牌,如下所示:
import { getAntiCSRFToken } from "blitz"
const antiCSRFToken = getAntiCSRFToken()
if (antiCSRFToken) {
// 设置获取请求的 header["anti-csrf"] = antiCSRFToken
}
然后你可以像这样在 API 路由中获取 sessionContext:
import { getSession } from "blitz"
export default async function ({ req, res }) {
const session = await getSession(req, res)
console.log("User ID:", session.userId)
res.json({ userId })
}
经过身份验证的会话使用存储在数据库中的不透明令牌。
string
。httpOnly
和 安全
的 cookie 发送给前端。安全
的 cookie 发送给前
端。SessionContext
interface SessionContext extends PublicData {
/**
* 如果是匿名则为 null
*/
userId: unknown
$handle: string | null
$publicData: PublicData
$authorize(
...args: IsAuthorizedArgs
): asserts this is AuthenticatedSessionContext
$isAuthorized: (
...args: IsAuthorizedArgs
) => this is AuthenticatedSessionContext
$create: (
publicData: PublicData,
privateData?: Record<any, any>
) => Promise<void>
$revoke: () => Promise<void>
$revokeAll: () => Promise<void>
$getPrivateData: () => Promise<Record<any, any>>
$setPrivateData: (data: Record<any, any>) => Promise<void>
$setPublicData: (
data: Partial<Omit<PublicData, "userId">>
) => Promise<void>
}