使用TypeScript,Node JS,Express JS和Next JS在完整堆栈应用程序中实现身份验证2.0的身份验证
#twitter #typescript #node #nextjs

目录

我们会学到什么

在这里,我们将学习使用Twitter OAuth 2.0在最小工作全堆栈Web应用程序上实现身份验证。我们不会使用Passport.js或类似的库来处理我们的身份验证。结果,我们将更好地理解OAuth 2.0流。我们还将了解以下堆栈:

要求

任何具有JavaScript基本知识的人都可以与此博客一起遵循。
如果您已经有类似的项目设置,也可以直接跳到Twitter OAuth2 Implementation部分。

项目设置

首先,让我们在根目录中添加一个package.json文件,然后添加以下内容:

{
  "private": true,
  "workspaces": [
    "server",
    "client"
  ],
  "scripts": {
    "client:dev": "yarn workspace client dev",
    "server:dev": "yarn workspace server dev",
    "client:add": "yarn workspace client add",
    "server:add": "yarn workspace server add",
    "migrate-db": "yarn workspace server prisma-migrate"
  }
}

您可以在此目录中设置版本控件,但这是可选的。无论哪种方式,我们现在都会为我们的Web应用添加客户和服务器。

客户端设置

通过运行以下命令来制作next.js应用:

yarn create next-app --typescript client

如果要使用JavaScript,请跳过--typescript标志。

这将在项目目录中创建一个client文件夹。导航并删除我们不需要的文件,即client\styles\Home.module.cssclient\pages\api。另外,让我们用以下内容替换client\pages\index.ts中的所有代码:

import { NextPage } from "next";

const Home: NextPage = () => {
  return (
    <div className="column-container">
      <p>Hello!</p>
    </div>
  );
};

export default Home;

使用我们的命令yarn client:dev启动客户端,然后转到http://www.localhost:3000/的地址应显示一个网页,说Hello!

现在设置了前端,让我们继续前进。

服务器设置

制作一个名为server的目录,并在项目目录中使用以下内容制作一个package.json文件:

{
  "name": "server",
  "version": "1.0.0",
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc --sourceMap false",
    "build:watch": "tsc -w",
    "start:watch": "nodemon dist/index.js",
    "dev": "concurrently \"yarn build:watch\" \"yarn start:watch\" --names \"tsc,node\" -c \"blue,green\"",
    "prisma-migrate": "prisma migrate dev",
    "prisma-gen": "prisma generate"
  }
}

在这里,我们添加了各种脚本,以帮助我们在开发阶段。我们将主要使用devmigrate-db脚本。它们使我们能够以观察模式启动服务器,并让我们分别迁移数据库。现在,我们可以返回工作区目录,并使用我们的yarn server:add添加软件包。因此是时候使用终端中使用以下命令安装所需依赖项了:

yarn server:add @prisma/client argon2 axios cookie-parser cors dotenv express jsonwebtoken
yarn server:add -D nodemon prisma typescript concurrently @types/cookie-parser @types/cors @types/express @types/jsonwebtoken @types/node

安装我们需要的依赖项后,制作几个文件以最小运行Express Server:

  • server/tsconfig.json根据首选项编辑或跳过如果不使用Typescript
  {
    "compilerOptions": {
      "target": "ES2018",
      "module": "commonjs",
      "lib": [
        "esnext", "esnext.asynciterable"
      ],
      "strict": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "declaration": true,
      "moduleResolution": "node",
      "noImplicitAny": true,
      "strictNullChecks": true,
      "strictFunctionTypes": true,
      "noImplicitThis": true,
      "noUnusedLocals": true,
      "noUnusedParameters": true,
      "noImplicitReturns": true,
      "noFallthroughCasesInSwitch": true,
      "allowSyntheticDefaultImports": true,
      "esModuleInterop": true,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "resolveJsonModule": true,
      "incremental": false,
      "baseUrl": "./src",
      "watch": false,
      "removeComments": true,
      "outDir": "./dist",
      "rootDir": "./src"
    },
    "types": ["node"],
    "include": ["./src/**/*.ts"],
  }
  • server/src/index.ts聆听服务器。
  import { CLIENT_URL, SERVER_PORT } from "./config";
  import cookieParser from "cookie-parser";
  import cors from "cors";
  import express from "express";

  const app = express();

  const origin = [CLIENT_URL];

  app.use(cookieParser());
  app.use(cors({
    origin,
    credentials: true
  }))

  app.get("/ping", (_, res) => res.json("pong"));

  app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))
  • server/src/config.ts导出一些恒定配置变量
  import { PrismaClient } from "@prisma/client"

  export const CLIENT_URL = process.env.CLIENT_URL!
  export const SERVER_PORT = process.env.SERVER_PORT!

  export const prisma = new PrismaClient()
  • server/.env为服务器设置端口,客户端URL和数据库URL。如果您要使用版本控制,请确保忽略此文件
  DATABASE_URL=postgres://postgres:postgres@localhost:5432/twitter-oauth2
  CLIENT_URL=http://www.localhost:3000
  SERVER_PORT=3001
  • server/prisma/schema.prisma让Prisma处理数据库结构
  generator client {
    provider = "prisma-client-js"
  }

  datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
  }

  enum UserType {
    local
    twitter
  }

  model User {
    id       String   @id @default(uuid())
    name     String
    username String   @unique
    type     UserType @default(local)
  }

现在使用yarn migrate-db命令迁移数据库,然后我们可以使用yarn server:dev运行服务器。

我们现在应该能够在http://localhost:3001/ping

上ping我们的服务器

Twitter OAuth2实现

我们准备通过Twitter OAuth 2.0实现身份验证到我们的应用中。我们将遵循this方法。
首先,我们必须在Twitter上制作一个应用程序。

设置Twitter用户身份验证设置

前往twitter's developer portal,在项目中以任何名称制作项目和开发应用程序。 Twitter将向您展示所需的东西。从Twitter获得这些应用程序可能需要几个小时才能获得批准。完成后,请转到应用程序的设置页面以设置一些必要的字段。
根据您的应用程序设置或编辑用户身份验证。

由于我只需要阅读此最小Web应用程序的配置文件信息,因此这些是我使用的设置:

保存Twitter客户ID和客户端秘密。

注意http://www.localhost:3000起作用,但不起作用,但不起作用。
因此,我在两个网站中添加了www.

客户

前端身份验证按钮

现在我们在客户端中添加按钮,这将导致我们的后端进行身份验证。
为此,我们需要使用有效的Twitter oauth url getter函数和一个按钮到达URL。

import twitterIcon from "../public/twitter.svg";
import Image from "next/image";

const TWITTER_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ" // give your twitter client id here

// twitter oauth Url constructor
function getTwitterOauthUrl() {
  const rootUrl = "https://twitter.com/i/oauth2/authorize";
  const options = {
    redirect_uri: "http://www.localhost:3001/oauth/twitter", // client url cannot be http://localhost:3000/ or http://127.0.0.1:3000/
    client_id: TWITTER_CLIENT_ID,
    state: "state",
    response_type: "code",
    code_challenge: "y_SfRG4BmOES02uqWeIkIgLQAlTBggyf_G7uKT51ku8",
    code_challenge_method: "S256",
    scope: ["users.read", "tweet.read", "follows.read", "follows.write"].join(" "), // add/remove scopes as needed
  };
  const qs = new URLSearchParams(options).toString();
  return `${rootUrl}?${qs}`;
}

// the component
export function TwitterOauthButton() {
  return (
    <a className="a-button row-container" href={getTwitterOauthUrl()}>
      <Image src={twitterIcon} alt="twitter icon" />
      <p>{" twitter"}</p>
    </a>
  );
}

注意:为简单起见,我们正在硬编码code_challengecode_verifier。您可以随机生成它。

client\components\TwitterOauthButton.tsx中添加上述代码后,我们将在Path client\public\twitter.svg上添加一个Twitter SVG图标(来自this之类的在线资源)。
然后,我们将在主页上导入组件:

import { TwitterOauthButton } from "../components/TwitterOauthButton";

const Home: NextPage = () => {
  return (
    <div className="column-container">
      <p>Hello!</p>
      <TwitterOauthButton />
    </div>
  );
};

这就是之后的样子:

单击Twitter图标将导致我们进入Twitter页面,我们可以在其中授权应用程序:

当然,单击authorize app按钮导致Cannot GET /oauth/twitter响应,因为我们尚未实现后端。

我查询

让我们请求从前端登录的用户通过钩子client\hooks\useMeQuery.ts

import { useEffect, useState } from "react";
import axios, { AxiosResponse } from "axios";

export type User = {
  id: string;
  name: string;
  username: string;
  type: "local" | "twitter";
};

export function useMeQuery() {
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [data, setData] = useState<User | null>(null);

  useEffect(() => {
    setLoading(true);
    axios
      .get<any, AxiosResponse<User>>(`http://www.localhost:3001/me`, {
        withCredentials: true,
      })
      .then((v) => {
        if (v.data) setData(v.data);
      })
      .catch(() => setError("Not Authenticated"))
      .finally(() => setLoading(false));
  }, []);

  return { error, data, loading };
}

这将为我们的最小应用做出足够的工作。我们将使用它来确定渲染内容。如果我们从挂钩中获取用户,我们将渲染用户名。否则,我们将渲染Login with Twitter按钮

import type { NextPage } from "next";
import { TwitterOauthButton } from "../components/TwitterOauthButton";
import { useMeQuery } from "../hooks/useMeQuery";

const Home: NextPage = () => {
  const { data: user } = useMeQuery();
  return (
    <div className="column-container">
      <p>Hello!</p>
      {user ? (// user present so only display user's name
        <p>{user.name}</p>
      ) : (// user not present so prompt to login
        <div>
          <p>You are not Logged in! Login with:</p>
          <TwitterOauthButton />
        </div>
      )}
    </div>
  );
};

export default Home;

以上是最终的client\pages\index.tsx的样子。转到http://www.localhost:3000,并在加载页面时检查浏览器的网络窗口。您应该看到ME查询在那里执行。

它的404,因为我们还没有在后端实施

造型

让我们通过修改client\styles\globals.css文件时添加一些基本样式:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

.column-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.row-container {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
}

.a-button {
  border: 2px solid grey;
  border-radius: 5px;
}

.a-button:hover {
  background-color: #111133;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
  body {
    color: white;
    background: black;
  }
}

这就是我们在客户端要做的。最终主页应该看起来像这样:

服务器

正如我们从前端看到的那样,我们需要在服务器中实现GET /oauth/twitter路由,以使应用程序工作的一部分。看看twitter documentation揭示了我们需要执行的步骤,以便我们可以阅读范围中提到的信息here
这些步骤总结如下:

  1. 获取访问令牌
  2. 从访问令牌获取Twitter用户
  3. 在我们的数据库中提高用户
  4. 创建cookie,以便服务器可以验证用户
  5. 用cookie重定向到客户

注意:只有前两个步骤与Twitter Oauth实现有关

让我们添加一个文件server\src\oauth2.ts,我们将在其中添加与OAUTH相关的代码。我们将通过在此处定义功能来完成上述步骤:

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code; // getting the code if the user authorized the app

  // 1. get the access token with the code

  // 2. get the twitter user using the access token

  // 3. upsert the user in our db

  // 4. create cookie so that the server can validate the user

  // 5. finally redirect to the client

  return res.redirect(CLIENT_URL);
}

在做任何一个之前,请确保我们在.env文件中的Twitter Oauth客户端秘密。我们还将在其中添加一个JWT秘密,以便我们可以加密发送给客户的cookie。最终的.env文件应该看起来像这样:

DATABASE_URL=postgres://postgres:postgres@localhost:5432/twitter-oauth2
CLIENT_URL=http://www.localhost:3000
SERVER_PORT=3001
JWT_SECRET=put-your-jwt-secret-here
TWITTER_CLIENT_SECRET=put-your-twitter-client-secret-here

使用代码获取访问令牌

添加以下代码(注释的行解释了他们的工作)以获取访问令牌:

// add your client id and secret here:
const TWITTER_OAUTH_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ";
const TWITTER_OAUTH_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET!;

// the url where we get the twitter access token from
const TWITTER_OAUTH_TOKEN_URL = "https://api.twitter.com/2/oauth2/token";

// we need to encrypt our twitter client id and secret here in base 64 (stated in twitter documentation)
const BasicAuthToken = Buffer.from(`${TWITTER_OAUTH_CLIENT_ID}:${TWITTER_OAUTH_CLIENT_SECRET}`, "utf8").toString(
  "base64"
);

// filling up the query parameters needed to request for getting the token
export const twitterOauthTokenParams = {
  client_id: TWITTER_OAUTH_CLIENT_ID,
  // based on code_challenge
  code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
  redirect_uri: `http://www.localhost:3001/oauth/twitter`,
  grant_type: "authorization_code",
};

// the shape of the object we should recieve from twitter in the request
type TwitterTokenResponse = {
  token_type: "bearer";
  expires_in: 7200;
  access_token: string;
  scope: string;
};

// the main step 1 function, getting the access token from twitter using the code that twitter sent us
export async function getTwitterOAuthToken(code: string) {
  try {
    // POST request to the token url to get the access token
    const res = await axios.post<TwitterTokenResponse>(
      TWITTER_OAUTH_TOKEN_URL,
      new URLSearchParams({ ...twitterOauthTokenParams, code }).toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${BasicAuthToken}`,
        },
      }
    );

    return res.data;
  } catch (err) {
    console.error(err);

    return null;
  }
}

从访问令牌中获取Twitter用户

类似的代码以使用户从访问令牌中获取:

// the shape of the response we should get
export interface TwitterUser {
  id: string;
  name: string;
  username: string;
}

// getting the twitter user from access token
export async function getTwitterUser(accessToken: string): Promise<TwitterUser | null> {
  try {
    // request GET https://api.twitter.com/2/users/me
    const res = await axios.get<{ data: TwitterUser }>("https://api.twitter.com/2/users/me", {
      headers: {
        "Content-type": "application/json",
        // put the access token in the Authorization Bearer token
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return res.data.data ?? null;
  } catch (err) {
    console.error(err);

    return null;
  }
}

检查他们是否工作

让我们看看他们是否成功地吸引了我们的用户。在server\src\oauth2.ts文件中添加所有代码后,应该看起来像这样:

import { CLIENT_URL } from "./config";
import axios from "axios";
import { Request, Response } from "express";

// add your client id and secret here:
const TWITTER_OAUTH_CLIENT_ID = "T1dLaHdFSWVfTnEtQ2psZThTbnI6MTpjaQ";
const TWITTER_OAUTH_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET!;

// the url where we get the twitter access token from
const TWITTER_OAUTH_TOKEN_URL = "https://api.twitter.com/2/oauth2/token";

// we need to encrypt our twitter client id and secret here in base 64 (stated in twitter documentation)
const BasicAuthToken = Buffer.from(`${TWITTER_OAUTH_CLIENT_ID}:${TWITTER_OAUTH_CLIENT_SECRET}`, "utf8").toString(
  "base64"
);

// filling up the query parameters needed to request for getting the token
export const twitterOauthTokenParams = {
  client_id: TWITTER_OAUTH_CLIENT_ID,
  code_verifier: "8KxxO-RPl0bLSxX5AWwgdiFbMnry_VOKzFeIlVA7NoA",
  redirect_uri: `http://www.localhost:3001/oauth/twitter`,
  grant_type: "authorization_code",
};

// the shape of the object we should recieve from twitter in the request
type TwitterTokenResponse = {
  token_type: "bearer";
  expires_in: 7200;
  access_token: string;
  scope: string;
};

// the main step 1 function, getting the access token from twitter using the code that the twitter sent us
export async function getTwitterOAuthToken(code: string) {
  try {
    // POST request to the token url to get the access token
    const res = await axios.post<TwitterTokenResponse>(
      TWITTER_OAUTH_TOKEN_URL,
      new URLSearchParams({ ...twitterOauthTokenParams, code }).toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${BasicAuthToken}`,
        },
      }
    );

    return res.data;
  } catch (err) {
    return null;
  }
}

// the shape of the response we should get
export interface TwitterUser {
  id: string;
  name: string;
  username: string;
}

// getting the twitter user from access token
export async function getTwitterUser(accessToken: string): Promise<TwitterUser | null> {
  try {
    // request GET https://api.twitter.com/2/users/me
    const res = await axios.get<{ data: TwitterUser }>("https://api.twitter.com/2/users/me", {
      headers: {
        "Content-type": "application/json",
        // put the access token in the Authorization Bearer token
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return res.data.data ?? null;
  } catch (err) {
    return null;
  }
}

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code;

  // 1. get the access token with the code
  const TwitterOAuthToken = await getTwitterOAuthToken(code);
  console.log(TwitterOAuthToken);

  if (!TwitterOAuthToken) {
    // redirect if no auth token
    return res.redirect(CLIENT_URL);
  }

  // 2. get the twitter user using the access token
  const twitterUser = await getTwitterUser(TwitterOAuthToken.access_token);
  console.log(twitterUser);

  if (!twitterUser) {
    // redirect if no twitter user
    return res.redirect(CLIENT_URL);
  }

  // 3. upsert the user in our db

  // 4. create cookie so that the server can validate the user

  // 5. finally redirect to the client

  return res.redirect(CLIENT_URL);
}

导入并将路由添加到我们的Express App:

app.get("/ping", (_, res) => res.json("pong"));

// activate twitterOauth function when visiting the route 
app.get("/oauth/twitter", twitterOauth);
app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))

现在运行客户端和服务器,并查看服务器控制台,如果我们单击前端的Twitter按钮并授权应用程序。

我们现在成功从Twitter获得了用户!
最重要的部分,即从Twitter获取用户。现在我们可以完成我们的项目。

完成Web应用程序

让我们完成GET /oauth/twitter工作所需的其余步骤。由于它们与OAuth无关,因此我将在server\src\config.ts文件中添加功能。

import { PrismaClient, User } from "@prisma/client"
import { CookieOptions, Response } from "express";
import { TwitterUser } from "./oauth2";
import jwt from "jsonwebtoken";

export const CLIENT_URL = process.env.CLIENT_URL!
export const SERVER_PORT = process.env.SERVER_PORT!
export const prisma = new PrismaClient()

// step 3
export function upsertUser(twitterUser: TwitterUser) {
  // create a new user in our database or return an old user who already signed up earlier 
  return prisma.user.upsert({
    create: {
      username: twitterUser.username,
      id: twitterUser.id,
      name: twitterUser.name,
      type: "twitter",
    },
    update: {
      id: twitterUser.id,
    },
    where: {  id: twitterUser.id},
  });
}

// JWT_SECRET from our environment variable file
export const JWT_SECRET = process.env.JWT_SECRET!

// cookie name
export const COOKIE_NAME = 'oauth2_token'

// cookie setting options
const cookieOptions: CookieOptions = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production'
  sameSite: "strict"
}

// step 4
export function addCookieToRes(res: Response, user: User, accessToken: string) {
  const { id, type } = user;
  const token = jwt.sign({ // Signing the token to send to client side
    id,
    accessToken,
    type
  }, JWT_SECRET);
  res.cookie(COOKIE_NAME, token, {  // adding the cookie to response here
    ...cookieOptions,
    expires: new Date(Date.now() + 7200 * 1000),
  });
}

导入功能并在server\src\oauth2.ts中使用它们:

import { prisma, CLIENT_URL, addResCookie } from "./config";

...

// the function which will be called when twitter redirects to the server at http://www.localhost:3001/oauth/twitter
export async function twitterOauth(req: Request<any, any, any, {code:string}>, res: Response) {
  const code = req.query.code;

  // 1. get the access token with the code
  const twitterOAuthToken = await getTwitterOAuthToken(code);

  if (!twitterOAuthToken) {
    // redirect if no auth token
    return res.redirect(CLIENT_URL);
  }

  // 2. get the twitter user using the access token
  const twitterUser = await getTwitterUser(twitterOAuthToken.access_token);

  if (!twitterUser) {
    // redirect if no twitter user
    return res.redirect(CLIENT_URL);
  }


  // 3. upsert the user in our db
  const user = await upsertUser(twitterUser)

  // 4. create cookie so that the server can validate the user
  addCookieToRes(res, user, twitterOAuthToken.access_token)

  // 5. finally redirect to the client
  return res.redirect(CLIENT_URL);
}

注意:为简单起见,我们正在cookie中发送访问令牌。对于Web应用程序,我们应该将其存储在更安全的地方,例如数据库。

最后,在server\src\index.ts文件中添加me查询。

import { CLIENT_URL, COOKIE_NAME, JWT_SECRET, prisma, SERVER_PORT } from "./config";
import cookieParser from "cookie-parser";
import cors from "cors";
import express from "express";
import jwt from 'jsonwebtoken'
import { getTwitterUser, twitterOauth } from "./oauth2";
import { User } from "@prisma/client";

const app = express();
const origin= [CLIENT_URL];
app.use(cookieParser());
app.use(cors({
  origin,
  credentials: true
}))
app.get("/ping", (_, res) => res.json("pong"));

type UserJWTPayload = Pick<User, 'id'|'type'> & {accessToken: string}

app.get('/me', async (req, res)=>{
  try {
    const token = req.cookies[COOKIE_NAME];
    if (!token) {
      throw new Error("Not Authenticated");
    }
    const payload = await jwt.verify(token, JWT_SECRET) as UserJWTPayload;
    const userFromDb = await prisma.user.findUnique({
      where: { id: payload?.id },
    });
    if (!userFromDb) throw new Error("Not Authenticated");
    if (userFromDb.type === "twitter") {
      if (!payload.accessToken) {
        throw new Error("Not Authenticated");
      }
      const twUser = await getTwitterUser(payload.accessToken);
      if (twUser?.id !== userFromDb.id) {
        throw new Error("Not Authenticated");
      }
    }
    res.json(userFromDb)
  } catch (err) {
    res.status(401).json("Not Authenticated")
  }
})

// activate twitterOauth function when visiting the route 
app.get("/oauth/twitter", twitterOauth);
app.listen(SERVER_PORT, () => console.log(`Server listening on port ${SERVER_PORT}`))

现在完成了!让我们看看当我们单击客户端上的Twitter按钮并在此处授权应用程序时会发生什么。

我们现在在其中看到我们的Twitter用户名而不是Twitter按钮,这表明me查询已成功执行。结果,我们现在通过Twitter OAuth 2.0在我们的最小全堆栈Web应用程序中使用了一个有效的用户身份验证系统。

结论

感谢您的阅读! This是具有所有代码的GitHub存储库。找到更多有趣的事情,可以使用Twitter API here。可以找到通过Twitter OAuth 2.0实现身份验证的另一个示例,可以找到here

Rafid Hamid

撰写