返回 导航

其他

hangge.com

Vue3 - Supabase的集成与使用详解4(身份验证、访问控制)

作者:hangge | 2024-02-27 08:50
    Supabase 提供了强大的身份验证(Authentication)服务,使开发者能够轻松地添加用户身份验证和管理功能到其应用程序中。本文我将通过用户登录和权限验证样例演示身份验证功能的使用。

四、身份验证、访问控制

1,功能说明

(1)Supabase 提供了管理用户的功能,包括查看、创建、更新和删除用户信息。开发者可以通过 Supabase 控制台或 API 来管理用户,实现对用户身份的精确控制。
(2)Supabase 提供了用户注册和登录的功能,用户可以通过邮箱和密码进行注册,并使用已注册的帐户进行登录。此外,Supabase 还支持第三方身份验证,如社交媒体登录(例如,使用 GoogleGitHubGitLab 等账号进行登录)。
(3)Supabase 的身份验证服务基于 OAuth 2.0 JSON Web TokensJWT)标准。它提供了一个安全的身份验证机制,确保用户身份得到安全验证,并且可以生成包含用户信息的 JWT 令牌,用于在客户端和服务器之间进行安全的身份验证和授权。

2,创建用户

(1)这里我们采用通过 Supabase 控制台方式来创建用户,首先我们进入“Authentication”页面,然后点击右上方的“Add user”按钮,然后选择“Create new user

(2)填写创建用户的邮箱地址和登录密码,勾选“Auto Confirm User”,点击“Create user”按钮即创建了一个用户:

3,创建 RLS 安全配置

(1)首先我们在 Supabase 上创建一张 book 表,并且插入一些初始数据:
drop table if exists book;

create table book (
  "id" bigint generated by default as identity primary key,
  "name" character varying,
  "price" numeric
);

INSERT INTO book ("name", "price") VALUES
  ('我与地坛', 29.00),
  ('失败者的春秋', 38.00),
  ('南北战争三百年', 35.00);

(2)接着我们进入“Authentication”页面,点击“Policies”菜单项,确保 book 表的 RLS 功能是开启的(没有开启点击右侧 enable 按钮开启)。最后点击“New Policy”按钮开始创建一条规则:

(3)在弹出框中选择“For full customization

(4)然后添加如下规则内容,表示未登录情况下可以进行数据查看,然后点击右下角的“Review”按钮

(5)接着会显示该规则对应的sql语句,我们点击右下角的“save pplicy”按钮即可。

(6)然后我们继续同样的操作,创建一个登录用户可以进行增删改查所有操作的规则:

(7)这样 book 表的两个安全规则都配置完毕了:

4,样例代码

我们在前文样例的基础上增加了用户登录和注销功能:
<script setup>
// 引入Vue 3的响应式引用(ref)和生命周期钩子(onMounted)
import { ref, onMounted } from 'vue'
 
// 引入Supabase客户端实例
import { supabase } from './lib/supabaseClient'
 
// 创建一个响应式的引用(ref)用于存储书籍数据
const books = ref([])
const isLoggedIn = ref(false);
const userId = ref('');
const username = ref('');
const password = ref('');

// 登录
async function login() {
  try {
    // 调用接口进行登录
    const { data, error } = await supabase.auth.signInWithPassword({
      email: username.value,
      password: password.value,
    })

    // 登录结果显示
    if (error) {
      alert('登录失败:' + error.message);
    } else {
      console.log('登录成功:', data)
      userId.value = data.user.id
      isLoggedIn.value = true
    }
  } catch (error) {
    alert('登录失败:' + error.message)
  }
}

// 退出登录
async function logout() {
  await supabase.auth.signOut()
  isLoggedIn.value = false;
}
 
// 查询书籍
async function getBooks() {
  // 使用Supabase实例的select方法从'book'表中获取所有数据
  const { data } = await supabase.from('book').select()
 
  // 将查询结果更新到books引用中
  books.value = data
}

// 新增一本书籍
async function addOneBook() {
  await supabase.from('book').insert({ name: `新书籍${getRandNum()}`, price: getRandNum()})
  await getBooks()
}

// 新增三本书籍
async function addThreeBooks() {
  await supabase.from('book').insert([
   { name: `新书籍${getRandNum()}`, price: getRandNum()},
   { name: `新书籍${getRandNum()}`, price: getRandNum()},
   { name: `新书籍${getRandNum()}`, price: getRandNum()}
  ])
  await getBooks()
}

// 更新书籍
async function updateBook() {
  // 更新最后一条数据
  if (books.value.length > 0) {
    const lastBook = books.value[books.value.length - 1];
    await supabase.from('book')
      .update({ name: `新书籍${getRandNum()}`, price: getRandNum()}).eq('id', lastBook.id)
    await getBooks()
  }
}

// 删除书籍
async function deleteBook() {
  // 删除最后一条数据
  if (books.value.length > 0) {
    const lastBook = books.value[books.value.length - 1];
    await supabase.from('book').delete().eq('id', lastBook.id)
    await getBooks()
  }
}

// 插入或更新书籍
async function upsertBook() {
  await supabase.from('book')
    .upsert({ id: 100, name: `新书籍${getRandNum()}`, price: getRandNum()})
  await getBooks()
  supabase.removeChannel('book-channel')
}

// 生成一个介于10(包含)和 100(不包含)之间的随机整数
function getRandNum() {
  return Math.floor(Math.random() * 90) + 10;
}
 
// 在组件被挂载后(mounted)执行getBooks函数,获取并更新书籍数据
onMounted(() => {
  getBooks()
})
</script>
 
<template>
  <div class="login-containter" v-if="!isLoggedIn">
    <label for="username">用户名:</label>
    <input v-model="username" type="text" id="username" />
    <label for="password">密码:</label>
    <input v-model="password" type="password" id="password" />
    <button @click="login">登录</button>
  </div>  
  <div class="login-containter" v-else>
    <label>用户id:{{ userId }} <button @click="logout">注销</button></label>
  </div>
  <div>
    <button class="action-button" @click="addOneBook">新增一本书籍</button>
    <button class="action-button" @click="addThreeBooks">新增三本书籍</button>
    <button class="action-button" @click="updateBook">更新最后一本书</button>
    <button class="action-button" @click="deleteBook">删除最后一本书</button>
    <button class="action-button" @click="upsertBook">插入或更新书籍</button>
  </div>
  <!-- 展示书籍数据的无序列表 -->
  <ul class="book-list">
    <li v-for="book in books" :key="book.id">
      {{ book.name }} (编号:{{ book.id }},价格:{{ book.price }})
    </li>
  </ul>
</template>

<style scoped>

  .login-containter {
    margin-bottom: 20px;
  }

  .login-containter input{
    margin-right: 10px;
  }
  .action-button {
    margin-right: 5px; /* 设置按钮之间的外边距 */
  }

  .book-list {
    margin-top: 20px; /* 设置书籍列表上方的外边距 */
    padding: 0; /* 移除列表的内边距 */
  }

  .book-list li {
    margin-bottom: 8px; /* 设置每本书之间的外边距 */
  }
</style>

5,运行测试

(1)由于安全策略允许所有用户(无论是否登录)对 book 表进行 Select 操作,因此页面打开后首先会自动加载并显示 book 表中的所有数据。

(2)而 book 表的 insertupdatedelete 操作需要登录后才允许执行,因此我们对数据进行操作时会报无权限错误。

(3)使用我们之前创建用户的邮箱和密码信息进行登录后,页面显示用户的 id 以及注销按钮,点击注销按钮可以退出登录状态。

(4)登录后控制台会打印出用户的完整信息。

(5)并且登录后所有的功能都是可以正常使用了。

附:用户注册

1,通过代码进行用户注册

(1)上面样例我们是在 Supabase Dashboard 控制台上手动直接新建用户的,其实 Supabase 也支持通过客户端接口进行用户注册。下面代码我们增加一个了注册功能:
<script setup>
// 引入Vue 3的响应式引用(ref)和生命周期钩子(onMounted)
import { ref, onMounted } from 'vue'
 
// 引入Supabase客户端实例
import { supabase } from './lib/supabaseClient'
 
// 创建一个响应式的引用(ref)用于存储书籍数据
const books = ref([])
const isLoggedIn = ref(false);
const userId = ref('');
const username = ref('');
const password = ref('');

// 注册
async function signUp() {
  try {
    // 调用接口进行注册
    const { data, error } = await supabase.auth.signUp({
      email: username.value,
      password: password.value,
    })

    // 登录结果显示
    if (error) {
      alert('注册失败:' + error.message);
    } else {
      console.log('注册成功:', data)
    }
  } catch (error) {
    alert('注册失败:' + error.message)
  }
}

// 登录
async function login() {
  try {
    // 调用接口进行登录
    const { data, error } = await supabase.auth.signInWithPassword({
      email: username.value,
      password: password.value,
    })

    // 注册结果
    if (error) {
      alert('登录失败:' + error.message);
    } else {
      console.log('登录成功:', data)
      userId.value = data.user.id
      isLoggedIn.value = true
    }
  } catch (error) {
    alert('登录失败:' + error.message)
  }
}

// 退出登录
async function logout() {
  await supabase.auth.signOut()
  isLoggedIn.value = false;
}
 
// 查询书籍
async function getBooks() {
  // 使用Supabase实例的select方法从'book'表中获取所有数据
  const { data } = await supabase.from('book').select()
 
  // 将查询结果更新到books引用中
  books.value = data
}

// 新增一本书籍
async function addOneBook() {
  await supabase.from('book').insert({ name: `新书籍${getRandNum()}`, price: getRandNum()})
  await getBooks()
}

// 新增三本书籍
async function addThreeBooks() {
  await supabase.from('book').insert([
   { name: `新书籍${getRandNum()}`, price: getRandNum()},
   { name: `新书籍${getRandNum()}`, price: getRandNum()},
   { name: `新书籍${getRandNum()}`, price: getRandNum()}
  ])
  await getBooks()
}

// 更新书籍
async function updateBook() {
  // 更新最后一条数据
  if (books.value.length > 0) {
    const lastBook = books.value[books.value.length - 1];
    await supabase.from('book')
      .update({ name: `新书籍${getRandNum()}`, price: getRandNum()}).eq('id', lastBook.id)
    await getBooks()
  }
}

// 删除书籍
async function deleteBook() {
  // 删除最后一条数据
  if (books.value.length > 0) {
    const lastBook = books.value[books.value.length - 1];
    await supabase.from('book').delete().eq('id', lastBook.id)
    await getBooks()
  }
}

// 插入或更新书籍
async function upsertBook() {
  await supabase.from('book')
    .upsert({ id: 100, name: `新书籍${getRandNum()}`, price: getRandNum()})
  await getBooks()
  supabase.removeChannel('book-channel')
}

// 生成一个介于10(包含)和 100(不包含)之间的随机整数
function getRandNum() {
  return Math.floor(Math.random() * 90) + 10;
}
 
// 在组件被挂载后(mounted)执行getBooks函数,获取并更新书籍数据
onMounted(() => {
  getBooks()
})
</script>
 
<template>
  <div class="login-containter" v-if="!isLoggedIn">
    <label for="username">用户名:</label>
    <input v-model="username" type="text" id="username" />
    <label for="password">密码:</label>
    <input v-model="password" type="password" id="password" />
    <button @click="login">登录</button>
    <button @click="signUp">注册</button>
  </div>  
  <div class="login-containter" v-else>
    <label>用户id:{{ userId }} <button @click="logout">注销</button></label>
  </div>
  <div>
    <button class="action-button" @click="addOneBook">新增一本书籍</button>
    <button class="action-button" @click="addThreeBooks">新增三本书籍</button>
    <button class="action-button" @click="updateBook">更新最后一本书</button>
    <button class="action-button" @click="deleteBook">删除最后一本书</button>
    <button class="action-button" @click="upsertBook">插入或更新书籍</button>
  </div>
  <!-- 展示书籍数据的无序列表 -->
  <ul class="book-list">
    <li v-for="book in books" :key="book.id">
      {{ book.name }} (编号:{{ book.id }},价格:{{ book.price }})
    </li>
  </ul>
</template>

<style scoped>

  .login-containter {
    margin-bottom: 20px;
  }

  .login-containter input{
    margin-right: 10px;
  }
  .action-button {
    margin-right: 5px; /* 设置按钮之间的外边距 */
  }

  .book-list {
    margin-top: 20px; /* 设置书籍列表上方的外边距 */
    padding: 0; /* 移除列表的内边距 */
  }

  .book-list li {
    margin-bottom: 8px; /* 设置每本书之间的外边距 */
  }
</style>


(2)页面打开后我们输入要注册的用户邮箱和密码,然后点击注册按钮:

(3)接着用户邮箱会收到一封确认邮件,我们点击邮件里的确认链接,注册的用户便被激活可以使用了。

2,自动激活

(1)默认使用邮箱进行注册时需要通过确认邮件进行激活,如果需要注册时自动激活,可以对注册策略进行如下设置。

(2)取消确认邮件功能后,用户注册后就可以直接使用了。
评论

全部评论(0)

回到顶部