react课程22-实现较为完整的用户管理功能

Last updated on October 18, 2024 am

本节负责实现身份验证和授权、用户注册、登陆登出、修改用户信息的功能

一、身份验证和授权

(1) Authentication

身份验证是确认用户的身份的过程,目的是验证用户是谁。

首先我们在Authentication中新建了一个user,然后到API DOCS中的user management中寻找Log in with Email/Password这一段代码,copy下来放置进apiAuth中。

1
2
3
4
5
6
7
8
9
10
11
export async function login({ email, password }) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});

if (error) throw new Error(error.message);

console.log(data);
return data;
}

然后可以新建一个useLogin钩子,得到mutation函数来处理登陆。可以看出onSuccess函数可以得到data,于是可以记录到控制台查看信息。

调用 queryClient.setQueryData(['user'], user.user);:将登录成功的用户数据缓存到 ['user'] 查询中,这样应用的其他部分可以使用这个用户信息。(user是object,包含session和user,而我们想要的只是user:用户)

replace是为了替换浏览记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function useLogin() {
const queryClient = useQueryClient();
const navigate = useNavigate();

const { mutate: login, isLoading } = useMutation({
mutationFn: ({ email, password }) => loginApi({ email, password }),
onSuccess: (user) => {
queryClient.setQueryData(['user'], user.user);
console.log(user);
navigate('/dashboard',{replace: true});
},
onError: (err) => {
console.log(err);
toast.error('Provided email or password are incorrect');
},
});

return { login, isLoading };
}

调用 supabase.auth.signInWithPassword({ email, password }) 时,Supabase 会将你提供的 emailpassword 发送到它的身份验证服务。

Supabase 在后台会将提供的 emailpassword 与其数据库中存储的用户信息进行比较。验证流程包括:

  • 查找用户Supabase 会在其用户数据库中根据提供的 email 查找相应的用户记录。
  • 验证密码:找到用户后,Supabase 会对传入的 password 进行加密(通常是哈希处理),然后与数据库中存储的哈希值进行比较。密码在存储时是经过哈希加密的,数据库不直接存储明文密码。

验证结果:

  • 成功:如果邮箱存在且密码匹配,Supabase 会返回一个包含用户数据的响应(即 data 对象)。这表示用户身份验证成功,用户可以登录。
  • 失败:如果邮箱不存在或者密码不正确,Supabase 会返回一个 error 对象,其中包含错误信息(例如 “Invalid login credentials”)。

我们会发现登陆成功后,在浏览器storage出现了token。

image-20240926145909054

(2)Authorization

授权是在身份验证成功后,确定用户对系统资源或操作的访问权限的过程。

建立一个ProtectedRoute组件,并在App.jsx的Route中把Applayout组件用它包裹起来。由于其他界面都在Applayout中被渲染,所以用户必须通过身份验证,才能进入任何在 AppLayout 中渲染的页面。(代码简洁,性能优化,更好的维护性)

ProtectedRoute 是一个自定义的路由组件,主要职责是限制对某些路由的访问,用来保护一组路由,通常是检查用户是否登录或是否有访问权限。

1、从supabase中获取用户的信息

会话(Session)是指在一段时间内,用户与系统之间的持续交互状态或连接。会话管理是现代应用程序和服务中的重要机制,用于跟踪用户的身份和状态。具体来说,会话与身份验证和授权密切相关。会话通常在用户登录或访问某个系统时开始,并在用户注销或超时后结束(详见文末)

通过检查用户的当前会话,从本地内存中获取登录用户的信息。如果没有会话(即用户未登录),返回 null;如果会话有效,则获取并返回用户信息。

1
2
3
4
5
6
7
8
9
10
11
export async function getCurrentUser() {
const { data: session } = await supabase.auth.getSession();
if (!session.session) return null;

const { data, error } = await supabase.auth.getUser();
console.log(data);

if (error) throw new Error(error.message);

return data?.user;
}
2、useUser.js

获取当前用户并将其存储到缓存中,这样就不用每次都重新下载。(获取isAuthenticated)

1
2
3
4
5
6
7
8
export function useUser() {
const { isLoading, data: user } = useQuery({
queryKey: ['user'],
queryFn: getCurrentUser,
});

return { isLoading, user, isAuthenticated: user?.role === 'authenticated' };
}

下图为登陆后React Query工具里得到的信息。

image-20240926153141752

3、在ProtectRouter中来验证是否需要重定向到login界面

navigate不限于回调函数或 useEffect 中使用,但这些场景是最常见和安全的使用方式

不推荐在组件首次渲染的时候使用navigate。详见Router专题

  1. 得到 isAuthenticated
  2. 如果没有登陆就重定向到login界面(注意是在加载完成后检验是否认证!!!
  3. 如果还在加载就返回一个加载指示器
  4. 如果已经登陆就渲染App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ProtectedRoute({ children }) {
const navigate = useNavigate();
const { isLoading, isAuthenticated } = useUser();

useEffect(
function () {
if (!isAuthenticated && !isLoading) navigate('/login');
},
[isAuthenticated, isLoading, navigate]
);

if (isLoading)
return (
<FullPage>
<Spinner />
</FullPage>
);

if (isAuthenticated) return children;
}

(3)Log out

1、apiAuth
1
2
3
4
5
export async function logout({ email, password }) {
const { error } = await supabase.auth.signOut();

if (error) throw new Error(error.message);
}
2、useLogout

queryClient.removeQueries():清除 react-query 缓存中的所有数据,确保用户退出后,应用不再保留之前的查询数据。

navigate('/login', { replace: true }):将用户重定向到登录页面,并且使用 replace: true 替换当前的历史记录,这样用户在点击“返回”按钮时不会回到之前已登出的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function useLogout() {
const queryClient = useQueryClient();
const navigate = useNavigate();

const { mutate: logout, isLoading } = useMutation({
mutationFn: logoutApi,
onSuccess: () => {
queryClient.removeQueries();
navigate('/login', { replace: true });
},
});

return { logout, isLoading };
}
3、Logout组件
1
2
3
4
5
6
7
8
9
function Logout() {
const { logout, isLoading } = useLogout();

return (
<ButtonIcon disabled={isLoading} onClick={logout}>
{!isLoading ? <HiArrowRightOnRectangle /> : <SpinnerMini />}
</ButtonIcon>
);
}

二、实现注册功能

在这个APP中,逻辑是只有登陆成功后才能注册其他用户。

(1)创建注册表单

1、需要用到的工具
1
2
const { register, formState, getValues, handleSubmit } = useForm();
const { errors } = formState;
2、验证信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{...register('fullName', { required: 'This field is required' })}

{...register('email', {
required: 'This field is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Please provide a valid email address',
},
})}

{...register('password', {
required: 'This field is required',
minLength: {
value: 8,
message: 'Password needs a minimum of 8 characters',
},
})}

{...register('passwordConfirm', {
required: 'This field is required',
validate: (value) =>
value === getValues().password || 'Passwords need to match',
})}
3、如何提交表单?错误如何展示?
1
2
<Form onSubmit={handleSubmit(onSubmit)}>
<FormRow label="Full name" error={errors?.fullName?.message}> {/以此类推/}

(2)实现把用户注册到supabase

伤到了,谁懂,supabase界面全英文,看到有一个警告,但是看不懂也没当回事,但是验证创建user的时候就出问题了。原来现在需要打开自定义SMTP的开关才能用随便的邮箱去注册用户了。在设置-Authentication里面。这个表单输入的信息要是真实的。

image-20240926172620751

否则就会报错:Error: Email address “2768260754@rr.com“ cannot be used as it is not authorized at Object.signup [as mutationFn]

但是这还没有结束,下一小节是如何实现邮箱验证。supabase你关闭SMTP发送的功能好巧不巧,我现在要用到。。。💔

1、apiAuth

传递给signUp的object中可以包含一个可选项,因为只有email和password是必需项,所以名字可以通过这样的方式传递进去,同时可以再传一个用户的头像。

1
2
3
4
5
6
7
8
9
export async function signup({ fullName, email, password }) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: { data: { fullName, avatar: '' } },
});
if (error) throw new Error(error.message);
return data;
}
2、useSignUp
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
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { signup as signupApi } from '../../services/apiAuth';
import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';

export function useSignup() {
const queryClient = useQueryClient();
const navigate = useNavigate();

const { mutate: signup, isLoading } = useMutation({
mutationFn: signupApi,
onSuccess: (user) => {
toast.success(
"Account successfully created!Please verify the new account from the user's email address"
);
console.log(user);
},
onError: (err) => {
console.log(err);
toast.error(err.message);
},
});

return { signup, isLoading };
}
3、onSubmit
1
2
3
function onSubmit({ fullName, email, password }) {
signup({ fullName, email, password }, { onSettled: reset });
}

这里reset会引发问题。jonas还没改。

4、其他
  • 在Authentication-URL Configuration中的两个URL栏,分别填入了

(http://localhost:5173/dashboard) 和(http://localhost:5173)我不知道是干嘛用的

  • “Temp Mail”(临时邮件)是指一种可以临时使用的电子邮件服务,允许用户生成一个临时的电子邮件地址。用户可以使用这个地址接收邮件,而不需要使用自己的真实邮箱。

    地址:https://temp-mail.org/en/ 界面:(上面生成邮箱,下面是收件箱)

image-20240926192815075

由于我们打开了SMTP的验证要求,所以用户在注册后都会在邮箱中收到一封需要验证的邮件,否则还是不能用这个邮箱登陆。

(3)如何配置Gmail解决supabase停止发送邮件

这里是这个紧急变革的讨论区https://github.com/orgs/supabase/discussions/29370

咱们也是跟supabase的维护者对上话了😂能写进简历里吗

image-20240927091045026

一开始我以为只有那些专门的邮箱发送服务商才可以,于是打算使用看起来很靠谱的Resend,但是由于它需要自己有个域名,甚至去华为买了一个(首年1元的)域名。。。

但是后来发现Gmail居然也可以?!

1、设置自定义SMTP

在supabase的设置中找到Auth,下滑找到这个地方,首先点击here,进入设置邮箱limit的地方,把第一条设置改成3以上(每小时能够发的邮件数)。下面的email和name就相应填,email一定要填gmail邮箱,name可以随意。

img

下面的HOST和POST number就是smtp.gmail.com和465.

Username需要再填一次邮箱地址,不是随便填的!!!!!!!!!!

密码也不是Gmail的登陆密码,而是需要专门的密码,在Gmail账户中打开两步验证后找到设置应用专用密码,然后生成一个,注意生成后只会出现一次,需要立马复制下来。

这里每次更改密码再回来会发现没有改变,这也是我去问supabase维护者的问题,但是其实没有影响的。

image-20240927092904298
2、如果遇到问题怎么获得详细的错误原因?

在我们supabase的项目sidebar中有一个是image-20240927093243940

在里面可以找到关于Auth的所有日志,比如当SMTP没设置好就是这个错误:

image-20240927093406787

点进去会有详细的信息。

3、成功!🥳

经过一番不懈的努力,我终于实现了邮箱验证的功能,为了确认真的可以收到邮件,我使用了Temp emial的随机地址wamacex775@exweme.com(但是只要你不更换,每次进去都会是这个地址,所以很方便),并且在收件箱中真的看见了邮件:

img

(4)确保安全性

修改Policy,否则未登录的用户也是可以访问到这些数据库的信息的。

三、构建Header

这是现在的Header:image-20240927093914593

我们在注册用户的函数中,上传了除开email和密码的可选项,其中包含了avatar。这些信息可以在React Query工具里看到。

image-20240926224628775

我们的Header包含头像、还有两个图标。其中UserAvatar.jsx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function UserAvatar() {
const { user } = useUser();
const { fullName, avatar } = user.user_metadata;

return (
<StyledUserAvatar>
<Avatar
src={avatar || 'default-user.jpg'}
alt={`Avatar of ${fullName}`}
/>
<span>{fullName}</span>
</StyledUserAvatar>
);
}

四、实现更改密码和头像

(1)准备工作

1、在storage中建立自己的头像库,命名为avatars
2、添加Policy

选择这一条,然后为auth用户添加所有的操作权限

image-20240927100812071 image-20240927100757718
3、得到avatars中图片的URL路径

随便在其中上传一张图片,点击get url得到其URL路径,在后续的apiAuth中修改avatar的函数中需要用到。

image-20240927101356754

(2)建立更新用户信息的函数

1、函数参数

函数updateCurrentUser接收一个object,包含密码、全名和头像。

export async function updateCurrentUser({ password, fullName, avatar })

2、更新password或fullName

之所以可以把更新这些信息的函数笼统地弄成一个,是因为修改密码和修改全名并不在一个表格中,所以可以条件性地得到updateData。记住fullName和avatar在注册的时候就是通过options中的data关键字注册的,在传递要更新的内容的时候,也要把它们包裹在一个对象中传递。

在没有上传avatar的时候,就可以结束了,因为下面是有关更新avatar的内容。

1
2
3
4
5
6
7
8
9
//更新密码或者全名
let updateData;
if (password) updateData = { password };
if (fullName) updateData = { data: { fullName } };

const { data, error } = await supabase.auth.updateUser(updateData);

if (error) throw new Error(error.message);
if (!avatar) return data;
3、上传avatar

自定义filename,使用上面的操作返回的用户信息可以得到用户id,使用random()函数连接,放置图像名字重复。此处不需要返回data,这里是直接把图像上传到avatars库中。下面好更新。

1
2
3
4
5
6
7
const fileName = `avatar-${data.user.id}-${Math.random()}`;

const { error: storageError } = await supabase.storage
.from('avatars')
.upload(fileName, avatar);

if (storageError) throw new Error(storageError.message);
4、在用户信息中更新avatar

得到上面的URL中,把相应信息做一个替换,就可以更新了。

1
2
3
4
5
6
7
8
9
10
  const { data: updatedUser, error: error2 } = await supabase.auth.updateUser({
data: {
avatar: `${supabaseUrl}/storage/v1/object/public/avatars/${fileName}`,
},
});

if (error2) throw new Error(error2.message);

return updatedUser;
}

(3)useUpdate

onSuccess中,queryClient.setQueryData(['user'], user);查询参数必须是数组,否则就会报下图的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function useUpdateUser() {
const queryClient = useQueryClient();

const { mutate: updateUser, isLoading: isUpdating } = useMutation({
mutationFn: updateCurrentUser,
onSuccess: ({user}) => {
toast.success('User account successfully updated');
queryClient.setQueryData(['user'], user);
queryClient.invalidateQueries({
queryKey: ['user'],
});
},
onError: (err) => toast.error(err.message),
});

return { isUpdating, updateUser };
}

image-20240927105001683

原因是:

  • 数据结构:React Query 在内部维护查询缓存时,期望查询键(比如 ['user'])是一个数组,因为数组可以包含多个层次的键名和参数。直接使用字符串 'user' 会导致它无法正确处理查询数据的结构。
  • 查询数据格式:当你使用 queryClient.setQueryData 设置数据时,它希望能在内部维护一个对象结构。如果你只传递了一个字符串,它无法创建或修改必要的内部属性。

?.涉及到的知识点详细分析(COPY)

(1)session(会话)

会话是用户与系统之间的持续交互状态,它保存用户的身份和状态信息,以便在多次请求中维持用户的登录状态和操作权限。在你提供的代码中,session 是 Supabase 通过 JWT 来管理的会话,它记录了用户的登录状态,并用来确定用户是否有权限访问某些资源。

1. 会话的定义
  • 会话是用户在一段时间内与应用程序或服务器之间进行的交互过程。它可以持续几秒钟到数小时,甚至更久,取决于会话的管理方式和应用程序的配置。
  • 会话通常在用户登录或访问某个系统时开始,并在用户注销或超时后结束。
2. 会话的作用
  • 维持用户的身份验证状态:会话允许系统在多次请求之间记住用户的身份。例如,用户登录后,会话用于保存登录状态,以免用户每次访问页面时都需要重新登录。
  • 存储用户的临时信息:会话可以用来在不同页面间共享用户的临时数据,例如购物车信息、用户首选项等。
  • 确保安全性:通过会话,系统可以跟踪和验证用户的操作,防止未经授权的访问。
3. 会话是如何工作的
  • 当用户登录系统时,服务器会为该用户创建一个唯一的会话 ID,并将会话信息(如用户身份、权限等)与该会话 ID 关联。
  • 会话 ID 通常通过 CookiesToken(例如 JWT,JSON Web Token),或其他机制(如 URL 参数)存储在客户端。
  • 在每次用户发送请求时,客户端会将会话 ID 传递给服务器,服务器根据这个 ID 查找与之关联的会话数据,确认用户的身份和权限。
4. 会话的生命周期
  • 创建:会话通常在用户成功登录后由服务器创建,分配一个唯一的会话 ID。
  • 维持:在会话存续期间,用户的每次请求都使用同一会话 ID,服务器根据这个 ID 确认用户身份。
  • 销毁:会话可以通过用户主动注销、会话超时(例如长时间不活动)或服务器强制结束而销毁。当会话销毁后,用户需要重新登录以继续访问系统。
5. 会话在前端和后端的角色
  • 前端:前端通过 Cookies 或 Local Storage 来存储会话 ID 或 Token。在每次向服务器发出请求时,前端会自动携带这些凭据来验证用户身份。
  • 后端:后端负责生成和管理会话,保存会话相关的用户状态、信息,并在每次请求时验证用户的会话是否有效。
6. 会话与 Token(如 JWT)的关系
  • 传统会话:会话 ID 由服务器生成,并且服务器需要存储会话信息。这种方式通常需要维护一个会话存储,适用于基于服务器的应用程序。
  • Token(如 JWT):JWT 是一种常用的无状态会话机制,其中用户的身份信息通过加密后保存在 Token 中。服务器不需要存储会话信息,而是通过验证和解码 Token 来识别用户。
7. Supabase 中的会话

在你使用的 Supabase 中,会话是通过 JWT(JSON Web Token) 进行管理的。当用户登录时,Supabase 会生成一个 JWT,并将其保存在客户端。每次请求都会带上这个 Token,以证明用户的身份。

  • supabase.auth.getSession():该方法用于获取当前会话信息(包括 JWT)。如果用户当前登录了,这个会话就会返回有效的 JWT。
  • supabase.auth.getUser():在有有效会话时,使用该方法获取当前会话中的用户详细信息。

react课程22-实现较为完整的用户管理功能
http://example.com/2024/09/26/react课程22-实现较为完整的用户管理功能/
Author
Yaodeer
Posted on
September 26, 2024
Licensed under