# 创建并添加自定义的用户与权限提供程序

> Source: https://strapi.nodejs.cn/cms/configurations/users-and-permissions-providers/new-provider-guide

🌐 Creating and adding a custom Users & Permissions provider

通过在 `src/index.js` 中注册自定义 OAuth 提供程序，并实现一个返回用户 `email` 和 `username` 的 `authCallback`，为 Strapi 的用户与权限功能创建自定义 OAuth 提供程序，以便自动注册或登录。

🌐 Create custom OAuth providers for Strapi's Users & Permissions feature by registering them in `src/index.js` and implementing an `authCallback` that returns user `email` and `username` for automatic registration or login.

Strapi 提供了一个 [内置提供者](/cms/configurations/users-and-permissions-providers#setting-up-the-provider---examples) 列表，用于 [用户与权限功能](/cms/features/users-permissions)。你也可以按照本指南创建自己的提供者。

🌐 Strapi provides a list of [built-in providers](/cms/configurations/users-and-permissions-providers#setting-up-the-provider---examples) for the [Users & Permissions feature](/cms/features/users-permissions). You can also create your own provider following this guide.

:::prerequisites

你已经阅读了 [用户与权限提供者文档](/cms/configurations/users-and-permissions-providers) 并理解了登录流程。

🌐 You have read the [Users & Permissions providers documentation](/cms/configurations/users-and-permissions-providers) and understood the login flow.

:::

## 创建自定义提供程序 {#creating-a-custom-provider}

🌐 Creating a custom provider

你可以使用 [the `register` 生命周期函数](/cms/configurations/functions#register) 在 Strapi 应用的 `src/index.js|ts` 文件中创建你自己的自定义提供者。使用以下根据你的需求调整的代码示例：

🌐 You can use [the `register` lifecycle function](/cms/configurations/functions#register) to create your own custom provider in the `src/index.js|ts` file of your Strapi application. Use the following code example adjusted to your needs:

```js title="/src/index.js"
module.exports = {
  register({ strapi }) {
    strapi
      .plugin("users-permissions")
      .service("providers-registry")
      .add("example-provider-name", {
        icon: "",
        enabled: true,
        grantConfig: {
          key: "",
          secret: "",
          callback: `${strapi.config.server.url}/auth/example-provider-name/callback`,
          scope: ["email"],
          authorize_url: "https://awesome.com/authorize",
          access_url: "https://awesome.com/token",
          oauth: 2,
        },
        async authCallback({ accessToken, providers, purest }) {
          // use whatever you want here to get the user info
          return {
            username: "test",
            email: "test",
          };
        },
      });
  },
};
```

有关传递给 `grantConfig` 的参数的更多信息，请参阅 [`grant` 文档](https://github.com/simov/grant)。有关 `purest` 的更多信息，请参阅 [`purest` 文档](https://github.com/simov/purest)。

🌐 For additional information on parameters passed to `grantConfig`, please refer to the  [`grant` documentation](https://github.com/simov/grant). For additional information about `purest` please refer to [`purest` documentation](https://github.com/simov/purest).

### 身份验证流程如何工作 {#how-authentication-flow-works}

🌐 How authentication flow works

`authCallback` 函数返回的对象必须至少包含 `username` 和 `email` 属性。Strapi 使用这个返回的电子邮件地址来自动处理注册和登录的认证流程：

🌐 The object returned by the `authCallback` function must contain at least `username` and `email` properties. Strapi uses this returned email address to automatically resolve the authentication flow for both registration and login:

* 如果用户不存在：Strapi 会在返回的 `email` 和 `username` 下注册一个新用户，然后登录该用户，返回 JWT 和用户对象。
* 如果用户已存在：Strapi 会检索与该 `email` 匹配的现有用户并将其登录，返回 JWT 和用户对象。

由于这种设计，你无需在自定义提供程序中实现单独的查找或注册逻辑。你只需要确保你的 `authCallback` 从外部提供程序获取用户的资料，并返回用户的 `email` 和 `username`。

🌐 Because of this design, you do not need to implement separate lookup or registration logic inside your custom provider. You only need to ensure your `authCallback` fetches the user's profile from the external provider and returns the user's `email` and `username`.

### 前端设置 {#frontend-setup}

🌐 Frontend setup

配置 Strapi 和提供程序后，你必须在前端应用中：

🌐 Once you have configured Strapi and the provider, in your frontend application you must:

- 创建一个链接到 `GET STRAPI_BACKEND_URL/api/connect/${provider}` 的按钮（例如，`https://strapi.mywebsite/api/connect/github`）。
- 创建一个前端路由，如 `FRONTEND_URL/connect/${provider}/redirect`，必须处理 `access_token` 参数，并且必须使用 `access_token` 参数请求 `STRAPI_BACKEND_URL/api/auth/${provider}/callback`。<br/>  JSON 请求响应将是 `{ "jwt": "...", "user": {...} }`。

现在你可以按照[令牌使用](/cms/features/users-permissions#token-usage)中描述的方式进行身份验证请求。

🌐 Now you can make authenticated requests, as described in [token usage](/cms/features/users-permissions#token-usage).

:::caution Troubleshooting

- **未提供电子邮件**：当 `authCallback` 未能返回有效的 `email` 地址时，会发生此错误。
  - 确保你的提供者应用已配置正确的 OAuth 范围（例如 `email` 或 `profile`）以请求用户的电子邮件。
  - 检查身份提供者是否确实在有效载荷中返回了电子邮件。请注意，一些用户可能在身份提供者端隐藏了他们的电子邮件地址或将其设置为私密。
- **错误 429**：这很可能是因为你的登录流程进入了循环。要向后端发起新的请求，你需要等待几分钟或重启后端。
- **Grant：缺少会话或提供程序配置错误**：可能由于多种原因造成。
  - **无法构建重定向 URL**：请确保已在 `config/server.js` 中设置后端 URL：[设置服务器 URL](/cms/configurations/users-and-permissions-providers#setting-up-the-server-url)
  - **会话/Cookie/缓存问题**：你可以尝试在私密窗口中重新尝试。
  - **使用 ngrok 域名不正确**：检查你的 URL，并确保使用 ngrok URL 而不是 `http://localhost:1337`。不要忘记检查示例应用中设置的后端 URL，位于 `src/config.js`。
- **你无法访问你的管理面板**：最可能的原因是你使用 ngrok URL 设置了后端 URL 构建它，然后你停止/重新启动了 ngrok。你需要将后端 URL 替换为新的 ngrok URL，然后再次运行 `yarn build` 或 `npm run build`。

:::

### 重置密码 {#reset-password}

🌐 Reset password

**只能用于通过电子邮件提供商注册的用户。**

假定的一般流程：

🌐 The assumed general flow:

1. 用户访问你的**忘记密码页面**。
2. 用户输入他们的电子邮件地址。
3. 你忘记密码的页面会向后端发送请求，以向用户发送包含重置密码链接的电子邮件。
4. 用户收到电子邮件并单击特殊链接。
5. 该链接会将用户重定向到你的**重置密码页面**。
6. 用户输入新密码。
7. **重置密码页面** 将新密码发送到后端。
8. 如果请求包含步骤 3 链接中包含的代码，则密码将被更新。
9. 用户可以使用新密码登录。

以下部分详细介绍了步骤 3 和 7。

🌐 The following section details steps 3 and 7.

#### 忘记密码：请求重置密码的链接 {#forgotten-password-ask-for-the-reset-password-link}

🌐 Forgotten password: ask for the reset password link

此操作会向用户发送一封电子邮件，邮件中包含指向你的重置密码页面的链接。该链接将使用步骤7中[重置密码](#reset-password)所需的 URL 参数 `code` 进行增强。

🌐 This action sends an email to a user with the link to your reset password page. The link will be enriched with the url param `code` that is needed for the [reset password](#reset-password) at step 7.

首先，你必须指定以下内容：

🌐 First, you must specify the following:

- 在管理面板：_设置 > 用户与权限插件 > 高级设置 > 重置密码_ 页面，将 `url` 指向你的重置密码页面。
- 在管理面板中：_设置 > 用户与权限插件 > 电子邮件模板_ 页面，点击 _发货人电子邮件_。

然后，你的**忘记密码页面**必须向你的后端发送以下请求：

🌐 Then, your **forgotten password page** has to make the following request to your backend:

```js

// Request API.
axios
  .post('http://localhost:1337/api/auth/forgot-password', {
    email: 'user@strapi.io', // user's email
  })
  .then(response => {
    console.log('Your user received an email');
  })
  .catch(error => {
    console.log('An error occurred:', error.response);
  });
```

#### 重置密码：发送新密码 {#reset-password-send-the-new-password}

🌐 Reset Password: Send the new password

此操作将更新用户密码。
这同样适用于 [GraphQL 插件](/cms/plugins/graphql)，使用 `resetPassword` 变更。

🌐 This action will update the user password.
This also works with the [GraphQL Plugin](/cms/plugins/graphql), with the `resetPassword` mutation.

你的**重置密码页面**必须向你的后端发出以下请求：

🌐 Your **reset password page** has to make the following request to your backend:

```js

// Request API.
axios
  .post('http://localhost:1337/api/auth/reset-password', {
    code: 'privateCode', // code contained in the reset link of step 3.
    password: 'userNewPassword',
    passwordConfirmation: 'userNewPassword',
  })
  .then(response => {
    console.log("Your user's password has been reset.");
  })
  .catch(error => {
    console.log('An error occurred:', error.response);
  });
```

你也可以通过 `/change-password` API 端点更新已认证用户的密码：

🌐 You can also update an authenticated user password through the `/change-password` API endpoint:

```js

// Request API.
axios.post(
  'http://localhost:1337/api/auth/change-password',
  {
    currentPassword: 'currentPassword',
    password: 'userNewPassword',
    passwordConfirmation: 'userNewPassword',
  },
  {
    headers: {
      Authorization: 'Bearer <user jwt>',
    },
  }
);
```

### 电子邮件验证 {#email-validation}

🌐 Email validation

:::note

在生产环境中，确保已设置 `url` 配置属性。否则，验证链接将重定向到 `localhost`。关于该配置的更多信息请参见 [这里](/cms/configurations/server)。

🌐 In production, make sure the `url` config property is set. Otherwise the validation link will redirect to `localhost`. More info on the config [here](/cms/configurations/server).

:::

注册后，如果你已将 **启用电子邮件确认** 设置为 **开启**，用户将通过电子邮件收到一个确认链接。用户必须点击该链接以验证他们的注册。

🌐 After registering, if you have set **Enable email confirmation** to **ON**, the user will receive a confirmation link by email. The user has to click on it to validate their registration.

确认链接示例：`https://yourwebsite.com/api/auth/email-confirmation?confirmation=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTk0OTgxMTE3LCJleHAiOjE1OTc1NzMxMTd9.0WeB-mvuguMyr4eY8CypTZDkunR--vZYzZH6h6sChFg`

🌐 Example of the confirmation link: `https://yourwebsite.com/api/auth/email-confirmation?confirmation=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNTk0OTgxMTE3LCJleHAiOjE1OTc1NzMxMTd9.0WeB-mvuguMyr4eY8CypTZDkunR--vZYzZH6h6sChFg`

如果需要，你可以通过提出以下请求重新发送确认电子邮件：

🌐 If needed you can re-send the confirmation email by making the following request:

```js

// Request API.
axios
  .post(`http://localhost:1337/api/auth/send-email-confirmation`, {
    email: 'user@strapi.io', // user's email
  })
  .then(response => {
    console.log('Your user received an email');
  })
  .catch(error => {
    console.error('An error occurred:', error.response);
  });
```

## 向你的 Strapi 应用添加新提供者 {#adding-a-new-provider-to-your-strapi-application}

🌐 Adding a new provider to your Strapi application

:::info

本文件可能没有与 Strapi 5 保持最新，并且仍在进行中。同时， [contributions](https://github.com/strapi/documentation/blob/main/CONTRIBUTING.md) 非常欢迎。

:::

**[Grant](https://github.com/simov/grant) 提供多个常用 OAuth 提供商的配置。 [Custom](https://github.com/simov/grant#misc-custom-providers) 也支持提供商**。 <br/> 你可以在此查看并试用 200 多个支持的提供商： [OAuth Playground](https://grant.outofindex.com/)。

### 准备好你的文件 {#prepare-your-files}

🌐 Prepare your files

要在 Strapi 上添加新提供商，你需要对以下文件进行更改：

🌐 To add a new provider on Strapi, you will need to perform changes to the following files:

```
extensions/users-permissions/services/Providers.js
extensions/users-permissions/config/functions/bootstrap.js
```

如果这些文件不存在，你需要从你的 `node_modules` 或 Strapi 单仓库中复制。你可以查看 [插件扩展](/cms/plugins-development/plugins-extension) 以获取关于其工作原理的更多信息。

🌐 If these files don't exist you will need to copy from your `node_modules` or the Strapi mono-repo. You can see [plugin extensions](/cms/plugins-development/plugins-extension) for more information on how it works.

我们将一步一步进行。

🌐 We will go step by step.

### 配置你的提供者请求 {#configure-your-provider-request}

🌐 Configure your provider request

在 `Provider.js` 文件的 `getProfile` 函数中配置新的提供程序。

🌐 Configure the new provider in the `Provider.js` file at the `getProfile` function.

`getProfile` 接受三个参数：

🌐 The `getProfile` takes three params:

- **提供者**：所使用提供者的名称，字符串形式。
- **查询**：查询是提供者回调的结果。
- **回调**：将继续内部 Strapi 登录逻辑的回调函数。

这里是一个使用 `discord` 提供程序的例子。

🌐 Here is an example that uses the `discord` provider.

### 配置你的 OAuth 通用信息 {#configure-your-oauth-generic-information}

🌐 Configure your OAuth generic information

```js
case 'discord': {
  const discord = new Purest({
    provider: 'discord',
    config: {
      'discord': {
        'https://discordapp.com/api/': {
          '__domain': {
            'auth': {
              'auth': {'bearer': '[0]'}
            }
          },
          '{endpoint}': {
            '__path': {
              'alias': '__default'
            }
          }
        }
      }
    }
  });
}
```

这段代码创建了一个 `Purest` 对象，它为我们提供了一种与提供商的 REST API 交互的通用方式。

🌐 This code creates a `Purest` object that gives us a generic way to interact with the provider's REST API.

有关使用 `Purest` 模块的更多规范，请参阅  [Official Purest Documentation](https://github.com/simov/purest)

你可能还想看看已经制作好的众多配置 [here](https://github.com/simov/purest-providers/blob/master/config/providers.json)。

### 获取你的用户信息 {#retrieve-your-users-information}

🌐 Retrieve your user's information

对于我们的 Discord 提供商，它将如下所示：

🌐 For our Discord provider it will look like the following:

```js
  discord.query().get('users/@me').auth(access_token).request((err, res, body) => {
    if (err) {
      callback(err);
    } else {
      // Combine username and discriminator because discord username is not unique
      const username = `${body.username}#${body.discriminator}`;
      callback(null, {
        username,
        email: body.email
      });
    }
  });
  break;
}
```

这是我们开关的下一部分。既然我们已经正确配置了我们的提供者，我们就想用它来获取用户信息。

🌐 Here is the next part of our switch. Now that we have properly configured our provider, we want to use it to retrieve
user information.

在这里，你可以看到 `purest` 的真正威力，你只需在所需的 URL 上发起一个 GET 请求，使用 `query` 参数中的 `access_token` 进行认证。

🌐 Here you see the real power of `purest`, you can simply make a get request on the desired URL, using the `access_token`
from the `query` parameter to authenticate.

这样，你应该能够检索所需的用户信息。

🌐 That way, you should be able to retrieve the user info you need.

现在，你可以简单地使用用户的用户名和邮箱调用 `callback` 函数。这样，Strapi 将能够从数据库中检索你的用户并登录。

🌐 Now, you can simply call the `callback` function with the username and email of your user. That way, Strapi will be able
to retrieve your user from the database and log you in.

### 将新的提供者模型配置到数据库上 {#configure-the-new-provider-model-onto-database}

🌐 Configure the new provider model onto database

现在，我们需要为我们的新提供商配置‘模型’。这样，我们的设置就可以存储在数据库中，并从管理面板进行管理。

🌐 Now, we need to configure our 'model' for our new provider. That way, our settings can be stored in the database, and
managed from the admin panel.

打开文件 `packages/strapi-plugin-users-permissions/config/functions/bootstrap.js`

🌐 Open the file `packages/strapi-plugin-users-permissions/config/functions/bootstrap.js`

将你的提供商所需的字段添加到 `grantConfig` 对象中。
对于我们的 Discord 提供商，它将如下所示：

🌐 Add the fields your provider needs into the `grantConfig` object.
For our discord provider it will look like:

```js
discord: {
  enabled: false,  // make this provider disabled by default
  icon: 'comments', // The icon to use on the UI
  key: '',  // our provider app id (leave it blank, you will fill it with the Content Manager)
  secret: '', // our provider secret key (leave it blank, you will fill it with the Content Manager)
  callback: '/auth/discord/callback', // the callback endpoint of our provider
  scope: [  // the scope that we need from our user to retrieve information
    'identify',
    'email'
  ]
},
```
