使用开放授权进行身份验证

OAuth(开放授权)是一个允许用户在第三方应用上访问授权提供方给出的指定内容, 而无需提供用户名和密码的开放标准. 也是现在进行身份验证的常用方式.

流程

普遍的流程大致为:

前提: 第三方应用向OAuth授权方申请OAuth客户端和OAuth应用程序密钥.

  1. 用户在第三方进行身份验证时, 被重定向至在OAuth提供方申请的OAuth客户端进行验证.

  2. 用户同意授权后再重定向回第三方, 并携带一个临时授权验证代码code.

  3. 第三方使用申请的OAuth应用程序, OAuth应用程序密钥和用户的code回到OAuth提供方进行验证.

  4. 如果验证通过, OAuth提供方将会返回一个token, 这个token可以在指定的时间权限范围内访问用户数据.

以 OSU 的 OAuth 为例

官方文档: osu!web Documentation

下面的步骤顺序按照上面的流程进行对应

第一步, 在自己的 OSU 账号设置里注册一个"开放授权客户端"

应用回调链接就是上面流程中的第二点, OSU 会根据这里的内容返回code进行验证. [1]

第二步, 使用户登陆时转跳到 OAuth 客户端

"use client";

export default function SigninButton() {
  function osu() {
    const osuAuthUrl = `https://osu.ppy.sh/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_OSU_CLIENT_ID}&response_type=code`;

    window.location.href = osuAuthUrl;
  }

  return (
    <button onClick={osu}>
      <span>Login with osu</span>
    </button>
  );
}

上面的这一大串代码的用途简单来说就是点击这个按钮之后将用户重定向到https://osu.ppy.sh/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_OSU_CLIENT_ID}&response_type=code这个网页(OSU 官网)进行授权.

${process.env.NEXT_PUBLIC_OSU_CLIENT_ID}就是你在 OSU 那里注册获得的客户端 id.[2]

第三步, 接收 code

如果用户同意授权之后就会按照之前填的回调链接进行重定向.

Next.js为例, 要接收这个请求, 需要在app/api/auth/callback/osu文件夹里建立一个route.ts文件, 因为用户返回时使用GET方法进行请求, route.ts就需要导出一个名为GET的函数.

import { osuAuth } from "@/lib/auth";

export const GET = async (request: Request) => {
  const { searchParams } = new URL(request.url); // 获取查询参数
  const code = searchParams.get("code"); // 从 osu 重定向回来后会携带一个 code
  const callbackUrl = searchParams.get("callbackUrl") || "/";

  if (!code) {
    return new Response("No code provided", { status: 400 });
  }

  try {
    await osuAuth(code);

    const redirectUrl = new URL(callbackUrl, request.url).toString();

    return Response.redirect(redirectUrl);
  } catch (error) {
    // eslint-disable-next-line no-console
    console.error("Authentication error:", error);

    const redirectUrl = new URL("/login", request.url).toString();

    return Response.redirect(redirectUrl);
  }
};

上面代码的功能简单来说就是提取出code, 将这个code交由osuAuth进行处理.

第四步, 使用 OAuth 密钥进行验证

首先, 为了验证第三方应用的身份, 阻止拦截攻击. 用户统一后返回的并不是token,而是用来换取tokencode, 必须要拥有密钥才可以在OAuth提供者那里换取token. 所以接下来的目标就是拿着code和密钥在 OSU 的 OAuth 那里获取token.

// @/lib/auth 里osuAuth函数的部分代码
const accessTokenResponse = await fetch(`https://osu.ppy.sh/oauth/token`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({
    client_id: OSU_CLIENT_ID,
    client_secret: OSU_CLIENT_SECRET,
    code,
    grant_type: "authorization_code", // 比 github 多了这个必选项
  }),
});
const accessTokenData = await accessTokenResponse.json();
const accessToken = accessTokenData.access_token;

这个函数的功能大概就是向https://osu.ppy.sh/oauth/token发起一个POST请求, 请求中携带OAuth应用程序 id(OSU_CLIENT_ID), OAuth应用程序密钥(OSU_CLIENT_SECRET), code(上个步骤中获得的code), grant_type(一个固定的字符串: authorization_code). 最后返回得到的accessToken就是拥有访问权限的Token了, 可以拿着这个token访问用户的相关信息.[3]

比如获取用户的身份就是:

const userResponse = await fetch(`https://osu.ppy.sh/api/v2/me/osu`, {
  headers: {
    Authorization: `${accessTokenType} ${accessToken}`,
    "Content-Type": "application/json",
    Accept: "application/json",
  },
});
const userData = await userResponse.json();

  1. 对应官方文档的章节: Registering an OAuth application

  2. 对应官方文档的章节: Authorization Code Grant

  3. 对应官方文档的章节: Authorization Code Grant中的Exchange this code for an access token这一段