在这篇文章中,我想与大家分享我使用 Django Allauth 无头(Headless)后端认证,与 TanStack Start前端结合使用的经验和心得。我今年才刚刚学习 Web 编程,定有不少错漏,还望读者指正。

演示项目代码库

https://github.com/sd44/django-allauth

项目背景

Life is short, you need Python

虽然目前前后端分离架构正被 TypeScript 一体化框架(如Next.js/Tanstack Start等)冲击,特别是在要求全栈协作、类型一致性、小团队敏捷项目上,但我仍爱 Python Django 的简洁清晰、快速开发和易于维护。本文无意也无力讨论架构的优劣,就此打住吧。

Django AllauthNextAuth.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 后端

安装后端

DjangoDjango Allauth, Django Ninja 等的安装,参见官方文档和代码库 backend/pyproject.toml, backend/mysite/settings.py

Django 提供总体管理,Ninja 提供其他自定义 API。其中 settings.py 中需要注意如下几点。

前后端、社交账户如 GitHub Google 等相关 URL 配置必须完全一致:

  • 协议(httphttps 不同)
  • 域名(localhost127.0.0.1 不同)
  • 端口(30008000 不同)
  • 路径,包含或不包含最后的 /不同

CORS 和 CSRF 安全设置

我前端服务器端口为 3000

CORS 和 CSRF 安全设置
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

AllAuth
1
2
3
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,可方便结合AxiosTanstack Query,参考如下配置:

frontend/orval.config.ts
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
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函数,我们直接使用。

frontend/lib/cookies.ts
1
2
3
4
5
6
7
8
9
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

frontend/utils/custom-instance.ts
1
2
3
4
5
6
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
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 ,以获取用户状态:

frontend/src/router.tsx
1
2
3
4
5
6
7
8
createTanStackRouter({
routeTree,
context: {
queryClient,
sessionData: null,
},
// ... ...
}),
frontend/src/routes/__route.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 数据,已确定用户是否登陆。如

frontend/src/routes/index.tsx
1
2
3
4
5
6
7
export const Route = createFileRoute('/')({
loader: ({ context }) => {
// console.log('context in index.tsx loader: ', context);
return context.sessionData;
},
component: Home,
});

除获取用户信息外 index.tsx 文件中,还有其他相关内容,这里尤其注意 登录、注销、注册等操作完成后使 queryClient 之前获取的相应数据失效,必要时也要使路由失效(例如注销操作)。可详细看下 index.tsx, 在此不表。

总结

祝您开发过程更加顺畅。

Comments