在网上找了很多相关资料,发现说的都不是很清楚,就自己尝试写了。
注册 OAuth
首先去 github 注册一个新的 OAuth 应用,请点此去往。以下为填写示例:
注册完之后,点进该实例。其中的
Application logo可以上传应用图片,这里需要生成一个Client secrets,生成后请立即复制(后面需要使用)。如下图:
前端
注册路由
分别注册首页、登录页及重定向页面。
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{ path: "/", component: () => import("@/pages/home/index.vue") }, // 首页
{ path: "/login", component: () => import("@/pages/login/index.vue") }, // 登录页
{ path: "/redirect", component: () => import("@/pages/redirect/index.vue") }, // 重定向页
];
const router = createRouter({
history: createWebHistory(),
routes,
});
const whiteList = ["/login", "/redirect"];
router.beforeEach(async (to, _from, next) => {
if (whiteList.indexOf(to.path) !== -1) return next();
// 存放的用户信息
const github = localStorage.getItem("github");
if (github) {
next();
} else {
next({ path: "/login" });
}
});
export default router;
登录页
这里需要打开跳转至 github 认证页,一般来说这个按钮是放一个 github 的图标,对应接口链接。
<script setup lang="ts">
const login = async () => {
const state = Math.floor(Math.random() * Math.pow(10, 8));
window.location.href =
"https://github.com/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=https://githubauth.vercel.app/redirect&state=" +
state;
};
</script>
<template>
<div class="page">
<button @click="login">登录</button>
</div>
</template>
重定向页
向后端发送获取access_token的请求,请注意这个接口https://github.com/login/oauth/access_token不要在前端调用。这个错误编写的时候找了很久,然后发现不能在前端调用。获取到 access_token后就可以去github换取用户信息。
<script setup lang="ts">
import { onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import axios from "axios";
const router = useRouter();
const loginAction = async (code: string) => {
const res = await axios.get("https://github-admin-sooty.vercel.app/auth", {
params: {
code,
},
});
const { access_token, token_type } = res.data;
const token = `${token_type} ${access_token}`;
const result = await axios.get("https://api.github.com/user", {
headers: {
Authorization: token,
},
});
const { avatar_url, login } = result.data;
localStorage.setItem(
"github",
JSON.stringify({
avatar_url,
login,
token,
})
);
router.replace("/");
};
onMounted(() => {
const route = useRoute();
const code = route.query.code as string;
if (code) {
loginAction(code);
} else {
router.replace("/login");
}
});
</script>
<template>
<div class="page">登录中...</div>
</template>
首页
这里比较常规,就是展示一些数据。
<template>
<div class="home">
<div class="avatar">
<img :src="user?.avatar_url" alt="" />
</div>
<span class="username">{{ username }}</span>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
type userType = {
avatar_url?: string;
login?: string;
};
const user = ref<userType>({});
const username = ref("");
const timers = ref<(number | NodeJS.Timeout)[]>([]); // 用于存储定时器ID
const printText = () => {
if (user.value.login) {
for (let i = 0; i < user.value.login.length; i++) {
const timer = setTimeout(() => {
username.value += user.value.login![i];
}, i * 400);
timers.value.push(timer); // 将定时器ID添加到数组中
}
}
};
onMounted(() => {
const github = localStorage.getItem("github");
if (github) {
const { avatar_url, login } = JSON.parse(github);
user.value = { avatar_url, login };
printText();
}
});
onBeforeUnmount(() => {
// 清除所有的定时器
timers.value.forEach((timer) => {
clearTimeout(timer);
});
});
</script>
<style scoped>
.home {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 1px solid #ccc;
overflow: hidden;
}
.avatar img {
width: 100%;
height: 100%;
}
.username {
margin-top: 10px;
}
</style>
部署到 vercel
文件名为:vercel.json
{
"routes": [
{
"src": "/(.*).css$",
"headers": { "content-type": "text/css" },
"dest": "/$1.css"
},
{
"src": "/(.*).js$",
"headers": { "content-type": "text/javascript" },
"dest": "/$1.js"
},
{ "src": "/(.*)", "dest": "/index.html" }
]
}
后端
这里使用了一个全局错误中间件,用于捕获可能产生的错误。
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const axios = require("axios");
const app = new Koa();
const router = new Router();
// 跨域
app.use(cors());
// 解析参数
app.use(bodyParser());
// 全局错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
code: "-1",
msg: err.message,
};
}
});
router.get("/", (ctx, next) => {
ctx.body = {
code: 0,
msg: "Hello Koa",
};
});
router.get("/error", async (ctx, next) => {
// 抛出一个错误
throw new Error("Something went wrong");
});
router.get("/auth", async (ctx, next) => {
const { code } = ctx.request.query;
try {
const result = await axios({
method: "post",
url: "https://github.com/login/oauth/access_token",
headers: {
accept: "application/json",
},
data: {
client_id: "CLIENT_ID",
client_secret: "CLIENT_SECRET",
code,
},
});
ctx.body = result.data;
} catch (error) {
throw new Error(error);
}
});
app.use(router.routes());
app.use(router.allowedMethods());
module.exports = app.callback();
// 本地运行
// app.listen(3000, () => {
// console.log("服务正在运行:http://localhost:3000");
// });
部署到 vercel
文件名为:vercel.json
{
"version": 2,
"builds": [
{
"src": "./index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/",
"headers": {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
"Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
}
}
]
}