🔵Vue博客[4](后台添加GitHub授权登录)

本文最后更新于:2023年7月28日 凌晨

设计思路

新建OAuth Apps

打开GitHub->setting->Developer settings->OAuth Apps

New OAuth App新建一个应用

博客后台

路由表

router目录下的modules新建login.ts

1
2
3
4
5
6
7
8
9
10
export const Login = [{
// 登录页
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
}, {
// 回调中转页
path: '/login/code',
component: () => import('@/views/login/callback/index.vue')
}]

router/index.ts导入login.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createRouter, createWebHistory } from 'vue-router'
import { Login } from './modules/login'

const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/home',

},
...Login
]

export const router = createRouter({
history: createWebHistory(),
routes
})

登录页

iconfont图标库

注册iconfont

选择图标添加到项目,打开我的项目,选择Symbol

打开链接,复制Javascript代码

assets新建fonts目录,新建fonts/index.js

把上一步链接代码复制到index.js

main.ts导入图标js

1
import '@/assets/fonts'

app.vue设置全局class="icon"css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<router-view></router-view>
</template>

<style lang="scss">
body {
margin: 0;
}

.icon {
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<svg class="icon" aria-hidden="true">
<!-- xlink:href值由图标指定 -->
<use xlink:href="#icon-GitHub"></use>
</svg>
</template>

<style lang="scss" scoped>
/* `width`与`height`调节大小 */
.icon {
width: 2rem;
height: 2rem;
}
</style>

登录页面

views目录下新建login目录,新建index.vue作为登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<div class="container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>管理员认证</span>
<el-button class="button" type="text">使用说明</el-button>
</div>
</template>
<div class="authorization">
<!-- 跳转至授权页 -->
<a
href="https://github.com/login/oauth/authorize?client_id=db7547efa91798d7e956"
title="GitHub授权认证">
<i class="iconfont icon-GitHub"></i>
</a>
</div>
<el-divider>第三方授权认证</el-divider>
</el-card>
</div>
</template>

<style lang="scss" scoped>
.container {
width: 100%;
position: fixed;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}

.authorization {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;

a { color: #000; text-decoration: none; }

a > i { font-size: 40px; }
}

.card-header { display: flex; justify-content: space-between; align-items: center; }

.text { font-size: 14px; }

.item { margin-bottom: 18px; }

.box-card { width: 480px; }
</style>

回调中转页

login文件夹下新建callback文件夹,新建index.vue作为回调中转页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script lang="ts" setup>
import { router } from '@/router'
import { ApiGet } from '@/utils/request'
import { ElMessage, ElLoading } from 'element-plus'
import { onMounted } from 'vue';

onMounted(async () => {
/* 加载动画 */
const loading = ElLoading.service({
lock: true,
text: '正在人海中搜寻...',
background: '#fff',
})

// 核心
/* get携带code参数请求后端 */
const params = new URLSearchParams(location.search)
const code = params.get('code')
/* 注意axios之前已经配置过了baseUrl */
const res = (await ApiGet('/login/token', { code })).data

/* 认证成功跳转app主页 */
if (res.statusCode === 2000) {
// TODO: 信息保存到pinia
ElMessage.success('欢迎回家φ(゜▽゜*)♪')
loading.close()
router.push({ path: '/home' })
} else {
/* 认证失败等两秒再跳转回登录页😂 */
setTimeout(() => {
ElMessage.error('授权失败ψ(`∇´)ψ')
loading.close()
router.push({ path: '/login' })
}, 2000)
}
})
</script>

后端

配置环境变量

.env文件存储client_idclient_secret

1
pnpm i dotenv

根目录新建.env文件

1
2
3
CLIENT_ID: "上一步的client_id"
CLIENT_SECRET: "上一步的client_secret"
USER_ID: 666666<管理员的GitHubID>

.gitignore添加.env,这样上传到仓库就看不到密匙了

1
2
3
node_modules
dist
.env

使用环境变量

1
2
3
import dotenv from 'dotenv'

const config = dotenv.config().parsed // config即为保存了变量的对象

安装axios

第三方授权需要请求信息,所以使用axios请求

1
pnpm i axios

src目录下新建utils文件夹,新建request.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import axios from 'axios'

const ApiGet = (url: string, params: any, config?: any) => {
return axios.get(url, {
...params,
...config
})
}

const ApiPost = (url: string, data: any, config?: any) => {
return axios.post(url, data, config)
}

export { ApiPost, ApiGet }

提供授权

获取token

api文件夹下新建types文件夹,新建index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 获取token请求的参数接口 */
interface Params {
client_id: string,
client_secret: string,
code: string
}

/* 返回请求的res.data.data内容接口 */
interface Message {
login: string,
id: number,
token: string,
avatar_url: string
}

/* 返回接口 */
interface ResponseData {
statusCode: number,
message: string,
data: Message | null
}

export { Params, ResponseData }

login文件夹下新建getGithubToken.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { Request, Response } from 'express'
import { ApiPost, ApiGet } from '../../utils/request'
import { Params, ResponseData } from './types'
import dotenv from 'dotenv'

const config = dotenv.config().parsed

const params: Params = {
client_id: config!.CLIENT_ID,
client_secret: config!.CLIENT_SECRET,
code: '',
}

const responseData: ResponseData = {
statusCode: 2000,
message: 'success',
data: null,
}

const setResponseData = (newData: ResponseData) => {
responseData.statusCode = newData.statusCode
responseData.message = newData.message
responseData.data = newData.data
}

export const getGithubToken = {
GitToken: async (req: Request, res: Response) => {
// 获取GitHub code
params.code = String(req.query.code)

// 获取GitHub token
let token = ''
try {
const data = await ApiPost('https://github.com/login/oauth/access_token', params)
token = data.data.split('&')[0].split('=')[1]
} catch (e: any) {
setResponseData({ statusCode: 5001, message: '获取GitHub token失败', data: null })
}

// 获取用户信息
try {
// 获取access_token
const userData = await ApiGet('https://api.github.com/user', {
headers: {
'Authorization': 'token ' + token
}
})

// 处理请求结果
if (userData.status === 200 && userData.data.login) {
// 用户信息比对
if (userData.data.id != config!.USER_ID)
setResponseData({ statusCode: 4003, message: '用户信息不匹配', data: null })
else
setResponseData({
statusCode: 2000,
message: 'success',
data: {
login: userData.data.login,
id: userData.data.id,
token: token,
avatar_url: userData.data.avatar_url,
}
})
} else
setResponseData({ statusCode: 4001, message: '获取用户信息失败', data: null })
} catch (e: any) {
setResponseData({ statusCode: 5002, message: '无效token', data: null })
}

res.send(responseData)
}
}

router目录下index.ts注册/login/token路径

1
2
3
4
5
6
7
8
9
import express, { Router } from 'express'
import { getGithubToken } from '../api/login/getGithubToken'

const router: Router = express.Router()

// ...
router.get('/login/token', getGithubToken.GitToken)

export default router

最终效果

登录页

授权页

中转页

跳回app首页


🔵Vue博客[4](后台添加GitHub授权登录)
https://qingshaner.com/Vue博客[4](后台添加GitHub授权登录)/
作者
清山
发布于
2022年4月25日
许可协议