Rest 身份验证
所有网络请求都会经过你在 RestEndpoint 中可选定义的 getRequestInit。
Cookie 认证(credentials)
下面是一个通过发送 fetch credentials 实现简单 cookie 认证的示例:
AuthdEndpoint
MyResource
import { RestEndpoint, type RestGenerics } from '@data-client/rest'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { async getRequestInit(body: any): Promise<RequestInit> { return { ...(await super.getRequestInit(body)), credentials: 'same-origin', }; } }
▶Usage
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
请求
GET /my/1
content-type: application/json
cookie: session=abc;
响应200 OK
{
"id": "1",
"title": "this post"
}
Access Token 或 JWT
- static member
- function singleton
- async function
login
AuthdEndpoint
Auth
MyResource
import { RestEndpoint, type RestGenerics } from '@data-client/rest'; import { login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { declare static accessToken?: string; getHeaders(headers: HeadersInit) { // TypeScript doesn't infer properly const EP = this.constructor as typeof AuthdEndpoint; if (!EP.accessToken) return headers; return { ...headers, 'Access-Token': EP.accessToken, }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); AuthdEndpoint.accessToken = accessToken; };
▶Usage
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
请求
GET /my/1
access-token: mytoken
content-type: application/json
响应200 OK
{
"id": "1",
"title": "this post"
}
login
AuthdEndpoint
Auth
MyResource
import { RestEndpoint, type RestGenerics } from '@data-client/rest'; import { getAuthToken, setAuthToken, login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { async getHeaders(headers: HeadersInit) { return { ...headers, 'Access-Token': await getAuthToken(), }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); setAuthToken(accessToken); };
▶Usage
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
请求
GET /my/1
access-token: mytoken
content-type: application/json
响应200 OK
{
"id": "1",
"title": "this post"
}
login
AuthdEndpoint
Auth
MyResource
import { RestEndpoint, type RestGenerics } from '@data-client/rest'; import { getAuthToken, setAuthToken, login } from './login'; export default class AuthdEndpoint< O extends RestGenerics = any, > extends RestEndpoint<O> { getHeaders(headers: HeadersInit) { return { ...headers, 'Access-Token': getAuthToken(), }; } } export const handleLogin = async e => { const { accessToken } = await login(new FormData(e.target)); setAuthToken(accessToken); };
▶Usage
import { MyResource } from './MyResource'; MyResource.get({ id: 1 });
请求
GET /my/1
access-token: mytoken
content-type: application/json
响应200 OK
{
"id": "1",
"title": "this post"
}
从 React Context 获取认证头
注意
不建议将 React Context 用于不需要显示的状态(例如认证 token)。这会导致不必要的重新渲染,并增加应用的复杂度。
- Resource
- RestEndpoint
借助 hookifyResource,我们可以把任意 Resource 转换为使用 hook 来创建 endpoint 的版本
resources/Post.ts
import { Entity, resource, hookifyResource } from '@data-client/rest';
import { useAuthContext } from '../AuthContext';
class Post extends Entity {
id = '';
title = '';
}
export const PostResource = hookifyResource(
resource({ path: '/posts/:id', schema: Post }),
function useInit(): RequestInit {
const accessToken = useAuthContext();
return {
headers: {
'Access-Token': accessToken,
},
};
},
);
然后就可以在 React 组件中以 hook 的形式获取这些 endpoint
import { useSuspense } from '@data-client/react';
import { PostResource } from 'resources/Post';
function PostDetail({ id }) {
const post = useSuspense(PostResource.useGet(), { id });
return <div>{post.title}</div>;
}
注意
使用这种方式意味着所有 endpoint 调用都只能发生在函数渲染期间。
import { useController } from '@data-client/react';
import { PostResource } from './resources/Post';
function CreatePost() {
const controller = useController();
const createPost = PostResource.useCreate();
return (
<form
onSubmit={e =>
controller.fetch(createPost, new FormData(e.currentTarget))
}
>
{/* ... */}
</form>
);
}
首先,我们提供一种利用上下文修改请求头的简单方式。
api/AuthdEndpoint.ts
import { RestEndpoint, type RestGenerics } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
declare accessToken?: string;
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': this.accessToken,
};
}
}
接下来,我们使用 extend 生成一个注入了该上下文的新 endpoint。
import { useMemo } from 'react';
import type { IRestEndpoint } from '@data-client/rest';
import { useAuthContext } from './AuthContext';
function useEndpoint(endpoint: IRestEndpoint) {
const accessToken = useAuthContext();
return useMemo(
() => endpoint.extend({ accessToken }),
[endpoint, accessToken],
);
}
注意
使用这种方式意味着所有 endpoint 调用都只能发生在函数渲染期间。
import { useController } from '@data-client/react';
import { PostResource } from './api/Post';
import { useEndpoint } from './useEndpoint';
function CreatePost() {
const controller = useController();
const createPost = useEndpoint(PostResource.create);
return (
<form
onSubmit={e =>
controller.fetch(createPost, {}, new FormData(e.target))
}
>
{/* ... */}
</form>
);
}
代码组织
如果你的大部分 Resources 都使用类似的认证机制,可以尝试从一个定义了这些通用定制的基类进行扩展。
401 登出处理
当用户的授权过期时,服务器通常会在响应中予以表明,标准做法是返回 401。LogoutManager 可以轻松触发所有取消授权的清理工作。