在这篇文章中,我想与大家分享我使用 Django Allauth 无头(Headless)后端认证,与 TanStack Start前端结合使用的经验和心得。我今年才刚刚学习Web编程,定有不少错漏,还望读者指正。
演示项目代码库
sd44/ninja-allauth-fullstack
项目背景
Life is short, you need Python
虽然目前前后端分离架构正被 TypeScript 一体化框架(如Next.js/Tanstack Start等)冲击,特别是在要求全栈协作、类型一致性、小团队敏捷项目上,但我仍爱Python Django的简洁清晰、快速开发和易于维护。本文无意也无力讨论架构的优劣,就此打住吧。
Django Allauth 与 NextAuth.js, Better Auth都提供多种认证方式(如手机号码、邮箱、通行密钥、数十种社交账户认证等)。但官方 Allauth Headless + React SPA示例仍然是JS,而非TS代码;网络上也缺乏Allauth 对接 Tanstack/Next SSR前端的教程。本文便由此而生,但限于篇幅,只提出个别避坑指南,并不完整。
技术栈
- 后端:
Django, Django Allauth, Django Ninja - 前端:
React, TanStack Start/Query, Orval - 数据库:
Django支持的多种数据库均可,它也提供了近乎完美的数据库迁移指令
步骤概述
1. 设置 Django 后端
安装后端
Django 和 Django Allauth, Django Ninja 等的安装,参见官方文档和代码库 backend/pyproject.toml, backend/mysite/settings.py。
Django提供总体管理,Ninja提供其他自定义API。其中 settings.py 中需要注意如下几点。
前后端、社交账户如GitHub Google等相关 URL 配置必须完全一致
- 协议(
http与https 不同) - 域名(
localhost与127.0.0.1 不同) - 端口(
3000、8000 不同) - 路径,包含或不包含最后的
/不同
CORS和CSRF安全设置
我前端服务器端口为 3000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| ## DANGER: allow all origins
## CORS_ALLOW_ALL_ORIGINS: bool
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
## CORS_ALLOWED_ORIGINS_REGEXES = [
## r"^http://localhost:\d+$",
## r"^http://127\.0\.0\.1:\d+$",
## ]
from corsheaders.defaults import default_headers
## 除默认设置外,加入一些allauth中的特定headers
CORS_ALLOW_HEADERS = (
*default_headers,
"x-session-token",
"x-email-verification-key",
"x-password-reset-key",
)
CORS_ALLOW_CREDENTIALS = True
## CSRF_TRUSTED_ORIGINS: list of strings
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
## 一些教程推荐开发环境设置如下内容,但实际上并不必要。
## https://pypi.org/project/django-cors-headers/
## SESSION_COOKIE_SAMESITE = 'None' # Allow cross-site cookies
## CSRF_COOKIE_SAMESITE = 'None' # Allow cross-site CSRF cookies
## CSRF_COOKIE_SECURE = False # 开发时关闭CSRF保护,避免跨域问题;生产环境中https应设置为True
|
配置AllAuth 和 Ninja 提供 openapi 文件
安装PyYAML 依赖,并配置如下,以便提供/_allauth/openapi.yaml, /_allauth/openapi.json and /_allauth/openapi.html
1
2
3
4
| # 文件路径:backend/mysite/settings.py
HEADLESS_SERVE_SPECIFICATION = True #
HEADLESS_SPECIFICATION_TEMPLATE_NAME = "headless/spec/swagger_cdn.html" # Redoc
## HEADLESS_SPECIFICATION_TEMPLATE_NAME = "headless/spec/redoc_cdn.html" # Swagger
|
Ninja OpenApi文件则是 /api/openapi.json
2. 创建前端项目
安装
前端安装见相关文档和我的代码库 frontend/package.json,略。虽然我使用的是Tanstack Start,但想必 Next.js 中也有类似功能实现。
通过 OpenApi 文件自动生成接口和类型
有很多软件包可以实现根据 OpenApi 文件生成相应类型和接口,我选用了功能强大的Orval,可方便结合Axios与Tanstack Query,参考如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // 文件路径:frontend/orval.config.ts
const BASE_URL = 'http://localhost:8000';
export default {
ninjaFile: {
output: {
mode: 'single',
target: 'src/openapi/ninja.ts',
baseUrl: BASE_URL,
schemas: 'src/openapi/ninjaModel',
client: 'react-query',
mock: false,
// biome: true, 我使用的 biome lint
override: {
mutator: {
path: './src/utils/custom-instance.ts',
name: 'customInstance',
},
},
},
input: {
target: `${BASE_URL}/api/openapi.json`,
},
},
allauthFile: {
output: {
mode: 'single',
target: 'src/openapi/allauth.ts',
baseUrl: BASE_URL,
schemas: 'src/openapi/allAuthModel',
client: 'react-query',
mock: false, // TODO: 暂时不懂mock
override: {
mutator: {
path: './src/utils/custom-instance.ts',
name: 'customInstance',
},
},
},
input: {
target: `${BASE_URL}/_allauth/openapi.json`,
},
},
};
|
运行orval命令就会自动生成了
1
2
| bunx orval
## npx orval
|
获取 CSRFToken
OAuth 除个别 GET 操作外,基本都需要同步传递 CSRFToken,默认存储在cookie中。因此我们需要获取它,并将其放入默认的Http 客户端实例中。
Tanstack start 提供有 getCookie函数,我们直接使用。
1
2
3
4
5
6
7
8
9
10
| // 文件路径:frontend/lib/cookies.ts
import { createServerFn } from '@tanstack/react-start';
import { getCookie } from '@tanstack/react-start/server';
// BUG: Tanstack getCookie Must be in serverFn https://github.com/TanStack/router/issues/4022#issuecomment-3019980723
export const getCSRFTokenByCookie = createServerFn({ method: 'GET' }).handler(async () => {
// Note: 如果获取csrfToken之前没有 get 服务端 url,可能会导致csrfToken为 null 或过期失效
await fetch('http://localhost:8000/_allauth/browser/v1/config');
return (await getCookie('csrftoken')) || '';
});
|
Http Verb携带CSRFToken
1
2
3
4
5
6
7
| // 文件路径:frontend/utils/custom-instance.ts
const csrftoken = await getCSRFTokenByCookie();
// console.log("csrftoken", csrftoken);
if (csrftoken) {
config.headers['X-CSRFToken'] = csrftoken; // TODO: 只针对browser client, app client应当设置X-Session-Cookie
}
|
社交账户 Auth Redirect
由于社交账户认证重定向会导致用户面临重定向(302),因此此调用仅在浏览器中可用,并且必须以同步(非XHR)方式调用, enctype 为 application/x-www-form-urlencoded.这也就意味着Orval生成的axios/fetch 等异步JS调用方式无法使用,需要我们自己实现,如下代码源于 AllAuth React-SPA 官方示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 文件路径:frontend/lib/auth.ts
export function postForm(action: string, data: Record<string, string>) {
const f = document.createElement('form');
f.method = 'POST';
f.action = action;
for (const key of Object.keys(data)) {
const d = document.createElement('input');
d.type = 'hidden';
d.name = key;
d.value = data[key];
f.appendChild(d);
}
document.body.appendChild(f);
f.submit();
}
|
路由守卫
传统的路由守卫过于笨重,注册、登录、注销、用户认证状态等集于一身,现在已经是 5202年了,我们使用更现代更低耦合的方法吧。幸好 Tanstack Start 提供了强大的路由功能可以方便完成以上功能。。
在根路由上下文中存储当前 session ,以获取用户状态:
1
2
3
4
5
6
7
8
9
| // 文件路径:frontend/src/router.tsx
createTanStackRouter({
routeTree,
context: {
queryClient,
sessionData: null,
},
// ... ...
}),
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 文件路径:frontend/src/routes/__root.tsx
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
sessionData: Awaited<ReturnType<typeof getUserSession>>['sessionData'] | null;
}>()({
beforeLoad: async ({ context }) => {
console.log('in beforeLoad, context: ', context);
const getSession = await context.queryClient.fetchQuery({
queryKey: ['user'],
queryFn: () => getUserSession(), // getUserSession 是 Orval自动生成的接口
}); // we're using react-query for caching, see router.tsx
console.log('getSession: ', getSession);
return { sessionData: getSession.sessionData };
},
|
如此,我们就可以在每个路由获取上下文中的当前session数据,已确定用户是否登陆。如
1
2
3
4
5
6
7
8
| // 文件路径:frontend/src/routes/index.tsx
export const Route = createFileRoute('/')({
loader: ({ context }) => {
// console.log('context in index.tsx loader: ', context);
return context.sessionData;
},
component: Home,
});
|
除获取用户信息外 index.tsx 文件中,还有其他相关内容,这里尤其注意 登录、注销、注册等操作完成后使 queryClient 之前获取的相应数据失效,必要时也要使路由失效(例如注销操作)。
可详细看下 index.tsx, 在此不表。
总结
祝您开发过程更加顺畅。