Auth.js
Auth.js 是一个广为人知的身份验证库,被各种 JS 框架使用。使用 Auth.js,我们可以从简化的复杂性中获益。我们还可以访问各种身份验证提供商,例如 GitHub、Google、Facebook 等。此外,它可以集成到多个框架中,包括 Qwik。
Auth.js 提供了几个功能,可以增强简单性、生产力、灵活性以及提供商的多样性。以下是 Auth.js 的主要功能
- 提供商:Auth.js 支持多个提供商,简化了我们应用程序中的身份验证过程(例如,Github、Google、Facebook、Twitter)。它还提供单点登录 (SSO) 服务以及传统身份验证。
- 管理:Auth.js 极大地帮助我们专注于业务逻辑。它管理令牌、存储令牌并自动刷新令牌。
- 配置:配置 Auth.js 很简单。它提供简单的安装、错误处理、用于登录和注册的自定义表单以及与提供商的轻松集成。
- 集成:Auth.js 与 JS 框架无缝集成,得益于其全面的文档,提供了清晰的指南供遵循。
- 安全性:虽然 Auth.js 对开发人员友好,但必须认识到确保我们数据高度安全的基础复杂性。
请注意,Auth.js 库仍处于 1.0 之前的阶段,可能存在错误。
安装
您可以使用以下 Qwik 启动脚本轻松添加 Auth.js
npm run qwik add auth
此命令将添加一个新包
@auth/qwik
并创建一个名为 [email protected]
的新文件,其中包含示例配置。
关于使用 Node 手动部署的说明
当使用 Node.js 手动部署应用程序时,特别是使用 Express 等框架时,服务器或 Node 进程本身并不知道它是在 HTTP 下还是 HTTPS 下提供服务。与 Vercel、Netlify 或 Cloudflare 等托管服务不同,这些服务会自动管理 ORIGIN 配置,手动设置需要显式指定。为了确保您的 Node.js 应用程序识别它正在提供服务的正确协议和主机,请设置以下 **环境变量**
- ORIGIN:将其设置为您的应用程序的 URL。例如:ORIGIN=https://your-app-name.example.com
- PROTOCOL_HEADER:这用于指示客户端请求的原始协议。通常,这在代理配置中指定。将此变量设置为:PROTOCOL_HEADER=X-Forwarded-Proto
- HOST_HEADER:此标头有助于识别客户端请求的原始主机。当您的 Node 环境位于代理或负载均衡器后面时,这尤其必要。将此变量设置为:HOST_HEADER=X-Forwarded-Host
Qwik API
useSession
一个 routeLoader$,它返回一个会话对象,如果不存在会话,则返回一个空对象。返回的会话对象的内容可以通过会话回调进行配置。会话数据也可以使用 session REST API 获取。
import { component$ } from '@builder.io/qwik';
import { useSession } from '~/routes/plugin@auth';
export default component$(() => {
const session = useSession();
return <p>{session.value?.user?.email}</p>;
});
useSignIn
一个 routeAction$,用于启动登录流程或将用户发送到列出所有可能提供商的登录页面。使用 useSignIn
登录时,CSRF 令牌在内部处理。
参数
providerId
:一个可选的字符串参数,包含提供商的名称。当提供时,将启动对您的身份提供商的授权请求。当省略时,将重定向到内置/无品牌的登录页面。options
:一个可选的选项对象。redirectTo
:一个可选的字符串,指定用户登录后将被重定向到的 URL。默认为启动登录的页面 URL。
authorizationParams
:一个可选的对象,包含发送到 /authorize 端点的其他参数。有关一些想法,请参阅授权请求 OIDC 规范。
注意:您也可以通过提供商设置
authorizationParams
。authorizationParams 配置
使用 useSignIn
和 <Form> 组件以及可选的 providerId
和 options.redirectTo
的示例
import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { useSignIn } from '~/routes/plugin@auth';
export default component$(() => {
const signIn = useSignIn();
return (
<Form action={signIn}>
<input type="hidden" name="providerId" value="github" />
<input type="hidden" name="options.redirectTo" value="http://qwik-auth-example.com/dashboard" />
<button>Sign In</button>
</Form>
);
});
以编程方式使用 useSignIn
的示例,以及可选的 providerId
和 options.redirectTo
import { component$ } from '@builder.io/qwik';
import { useSignIn } from '~/routes/plugin@auth';
export default component$(() => {
const signIn = useSignIn();
return (
<button onClick$={() => signIn.submit({ providerId: 'github', options: { redirectTo: 'http://qwik-auth-example.com/dashboard' } })}>Sign In</button>
);
});
useSignOut
一个 routeAction$,用于启动注销流程。用户会话将被失效/从 cookie/数据库中删除,具体取决于您选择存储会话的流程。
参数
redirectTo
:一个可选的字符串,指定用户注销后将被重定向到的 URL。默认为启动登录的页面 URL。
“redirectTo”必须被重定向回调处理程序视为有效。默认情况下,它要求 URL 是同一主机名下的绝对 URL,或者您也可以提供以斜杠开头的相对 URL。如果它不匹配,它将重定向到主页。您可以定义自己的重定向回调以允许其他 URL。
使用 useSignOut
和 <Form> 组件以及可选的 redirectTo
的示例
import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { useSignOut } from '~/routes/plugin@auth';
export default component$(() => {
const signOut = useSignOut();
return (
<Form action={signOut}>
<input type="hidden" name="redirectTo" value="/signedout" />
<button>Sign Out</button>
</Form>
);
});
使用useSignOut
以编程方式进行示例,并带有可选的redirectTo
import { component$ } from '@builder.io/qwik';
import { useSignOut } from '~/routes/plugin@auth';
export default component$(() => {
const signOut = useSignOut();
return <button onClick$={() => signOut.submit({ redirectTo: '/signedout' })}>Sign Out</button>;
});
REST API
Auth.js 提供的所有相同 REST API 都可用。
登录
GET /api/auth/signin
显示内置/无品牌登录页面。
POST /api/auth/signin/:provider
启动特定于提供者的登录流程。对于 OAuth 提供者,调用此端点将启动对您的身份提供者的授权请求。此端点也由 useSignIn 操作在内部使用。
回调
GET/POST /api/auth/callback/:provider
登出
GET /api/auth/signout
显示内置/无品牌登出页面。
POST /api/auth/signout
处理用户登出 - 这是一个 POST 提交,以防止恶意链接在未经用户同意的情况下触发用户登出。用户会话将失效/从 cookie/数据库中删除,具体取决于您选择存储会话的流程。此端点也由 useSignOut 方法在内部使用。
会话
GET /api/auth/session
返回对客户端安全的会话对象 - 或者如果不存在会话则返回一个空对象。返回的会话对象的内容可以通过会话回调进行配置。会话数据也可以使用 useSession 路由加载器检索。
CSRF
GET /api/auth/csrf
返回包含 CSRF 令牌的对象。在 NextAuth.js 中,所有身份验证路由都存在 CSRF 保护。它使用“双重提交 cookie 方法”,该方法使用签名的 HttpOnly、仅主机 cookie。此端点返回的 CSRF 令牌必须作为名为 csrfToken 的表单变量传递给对任何 API 端点的所有 POST 提交。
提供者
GET /api/auth/providers
返回已配置的 OAuth 服务列表以及每个服务的详细信息(例如登录和回调 URL)。它有助于动态生成自定义注册页面并检查为配置的每个 OAuth 提供者配置了哪些回调 URL。
示例
GitHub
- 按照 GitHub OAuth 指南 获取您的
GitHub 客户端 ID
、GitHub 客户端密钥
并使用openssl rand -base64 32
或 密钥生成器 生成AUTH_SECRET
。 - 由于默认的
[email protected]
使用 GitHub 作为示例,因此我们无需在那里更改任何内容。但是,可以使用除 GitHub 之外的提供者,或者可以添加其他提供者。Auth.js 还支持许多可以在此文件中设置的 其他选项。
import { QwikAuth$ } from "@auth/qwik";
import GitHub from "@auth/qwik/providers/github";
export const { onRequest, useSession, useSignIn, useSignOut } = QwikAuth$(
() => ({
providers: [GitHub],
}),
);
重要:确保保留
onRequest
导出,因为它用于处理 oAuth 流程重定向。用户完成 oAuth 流程后,GitHub(或任何其他提供者)将把用户重定向回应用程序到/api/auth/callback/github
(或/api/auth/callback/[otherProvider]
)。onRequest
中间件函数将处理对该端点的请求并完成 oAuth 流程。
- 创建或编辑项目根目录下的
.env.local
文件以存储密钥
GITHUB_ID=
GITHUB_SECRET=
AUTH_SECRET=
重要:请阅读 Qwik 文档关于 环境变量 的内容,以确保您安全地使用它们。许多提供者密钥应保持安全,不要暴露给客户端/浏览器。
- 应用程序现在已准备好使用 Auth.js 实现身份验证。
- 享受!
凭据
警告:Auth.js 不鼓励使用此功能。
https://next-auth.js.org/providers/credentials
- 为基于凭据的身份验证提供的功能有意限制,以阻止使用密码,因为密码存在固有的安全风险,并且支持用户名和密码会增加额外的复杂性。
- 由于默认的
[email protected]
使用 GitHub 作为示例,因此我们需要用凭据替换它。
import { QwikAuth$ } from "@auth/qwik";
import GitHub from "@auth/qwik/providers/github";
export const { onRequest, useSession, useSignIn, useSignOut } = QwikAuth$(
() => ({
providers: [
Credentials({
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied
const user = {
id: 1,
name: "Mike",
email: "[email protected]",
};
return user;
},
}),
],
}),
);
- 创建或编辑项目根目录下的
.env.local
文件以存储密钥
AUTH_SECRET=
重要:请阅读 Qwik 文档关于 环境变量 的内容,以确保您安全地使用它们。许多提供者密钥应保持安全,不要暴露给客户端/浏览器。
- 应用程序现在已准备好使用 Auth.js 实现身份验证。
- 享受!
路由保护
可以通过路由event.sharedMap
访问会话数据。因此,可以使用类似于此的代码来保护路由并进行重定向,该代码位于layout.tsx
或页面index.tsx
中
export const onRequest: RequestHandler = (event) => {
const session: Session | null = event.sharedMap.get('session');
if (!session || new Date(session.expires) < new Date()) {
throw event.redirect(302, `/api/auth/signin?redirectTo=${event.url.pathname}`);
}
};
注意:如果放在 layout.tsx 中,请确保重定向目标不共享相同的 layout.tsx,否则可能会发生重定向循环。