什么是 OAuth2 登录?
OAuth2 是当下广泛流行的第三方应用授权登录方案,如微信扫码登录。
OAuth2 的工作原理

OAuth2 实战范例

以码云 gitee 为例,在登录页面添加登录图标(vue+element)

核心代码
<el-avatar
class="bnt"
size="medium"
:src="giteeUrl"
@click.native="loginByGitee"
>el-avatar>
import giteeLogo from "@/assets/images/giteeLogo.svg";
data() 中
giteeUrl: giteeLogo,
loginByGitee() {
// 后端服务器跳转到gitee登录页的接口
window.open("http://127.0.0.1:7002/api/users/passport/gotoGitee");
},
以 egg.js 搭建的后端服务器为例
app\router.ts
// 跳转到 gitee 官网进行登录认证
router.get('/api/users/passport/gotoGitee', controller.user.gotoGitee);
app\controller\user.ts
async gotoGitee() {
const { app, ctx } = this;
const { cid, redirectURL } = app.config.giteeOauthConfig;
ctx.redirect(
`https://gitee.com/oauth/authorize?client_id=${cid}&redirect_uri=${redirectURL}&response_type=code`
);
}
config\config.default.ts
// gitee 的认证配置
const giteeOauthConfig = {
cid: process.env.GITEE_CID,
secret: process.env.GITEE_SECRET,
redirectURL: 'http://localhost:7002/api/users/passport/gitee/callback',
authURL: 'https://gitee.com/oauth/token?grant_type=authorization_code',
giteeUserAPI: 'https://gitee.com/api/v5/user',
};
.env
GITEE_CID = "e7440791b41de36978e65b258228e26b5d746**********"
GITEE_SECRET = "c1744f6a7224182b3c39df2b76f2608e750a46c4f907b3569*****"
GITEE_CID 和 GITEE_SECRET 来自码云自动生成的密钥,创建方法如下:



自定义需使用 OAuth2 登录的应用名称

上传需使用 OAuth2 登录的应用的logo,填写应用的域名(暂时没购买也没关系),填写应用回调地址,通常对应后端接口。

5. 点击创建应用按钮
默认登录后可以访问获取用户信息的接口,若需访问其他接口,可根据需要打钩

6. 完成创建后,便会得到 GITEE_CID 和 GITEE_SECRET

app\router.ts
// 通过gitee登录的回调
router.get(
'/api/users/passport/gitee/callback',
controller.user.oauthByGitee
);
app\controller\user.ts
async oauthByGitee() {
const { ctx } = this;
const { code } = ctx.request.query;
try {
// 生成 token
const token = await ctx.service.user.loginByGitee(code);
// 渲染授权成功页面(向前端传递token)
await ctx.render('success.nj', { token });
} catch (e) {
return ctx.helper.error({ ctx, errorType: 'giteeOauthError' });
}
}
app\service\user.ts
async loginByGitee(code: string) {
const { ctx, app } = this;
// 获取 access_token
const accessToken = await this.getAccessToken(code);
// 获取用户的信息
const user = await this.getGiteeUserData(accessToken);
// 检查用户是否已注册
const { id, name, avatar_url, email } = user;
// id 默认为数字类型,此处转换为字符串类型
const stringId = id.toString();
// 为避免不同平台id相同,此处在id前加上平台名称,如Gitee + id,Github + id
// 假如已经存在,直接返回token
const existUser = await this.findByAccount(`Gitee${stringId}`);
if (existUser) {
const token = app.jwt.sign(
{ account: existUser.account, _id: existUser._id },
app.config.jwt.secret,
{ expiresIn: app.config.jwtExpires }
);
return token;
}
// 假如不存在,新建用户后返回 token
const userCreatedData: Partial<UserProps> = {
oauthID: stringId,
provider: 'gitee',
account: `Gitee${stringId}`,
picture: avatar_url,
nickName: name,
email,
type: 'oauth',
};
const newUser = await ctx.model.User.create(userCreatedData);
const token = app.jwt.sign(
{ account: newUser.account, _id: newUser._id },
app.config.jwt.secret,
{ expiresIn: app.config.jwtExpires }
);
return token;
}
获取 access_token
async getAccessToken(code: string) {
const { ctx, app } = this;
const { cid, secret, redirectURL, authURL } = app.config.giteeOauthConfig;
const { data } = await ctx.curl(authURL, {
method: 'POST',
contentType: 'json',
dataType: 'json',
data: {
code,
client_id: cid,
redirect_uri: redirectURL,
client_secret: secret,
},
});
app.logger.info(data);
return data.access_token;
}
获取用户信息
async getGiteeUserData(access_token: string) {
const { ctx, app } = this;
const { giteeUserAPI } = app.config.giteeOauthConfig;
const { data } = await ctx.curl<GiteeUserResp>(
`${giteeUserAPI}?access_token=${access_token}`,
{
dataType: 'json',
}
);
return data;
}
app\view\success.nj
doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<title>授权成功title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
head>
<body>
<h1>授权成功h1>
body>
<script>
window.onload = function() {
setTimeout(() => {
const message = {
type: 'oauth-token',
token: '{{token}}'
}
window.opener.postMessage(message, 'http://localhost:8080')
window.close()
}, 1000)
}
script>
html>
此处使用 window.opener.postMessage 实现了跨域传参,其详细用法可参考
https://juejin.cn/post/7041363060522975246/
获取到 token 后存入sessionStorage,并获取用户信息
mounted() {
window.addEventListener("message", (res) => {
const { type, token } = res.data;
sessionStorage.setItem("token", token);
if (type === "oauth-token") {
this.$http.get("/api/api/users/getUserInfo").then((res) => {
this.userInfo = res;
sessionStorage.setItem("userName", res.data.data.nickName);
this.$router.push("/index");
});
}
});
},
针对需 token 的接口,统一添加 token
src\axios.js
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
// 无需带token的请求路径,正则校验(/public 和 /login 开头的api 无需token )
let publicPath = [/^\/public/, /^\/login/];
// 是否是公开接口(公开接口无需token)
let isPublic = false;
// 判断当前api是否是公开接口
publicPath.map((path) => {
isPublic = isPublic || path.test(config.url);
});
// 从sessionStorage中获取token
const token = sessionStorage.getItem("token");
if (!isPublic && token) {
// 若当前api不是公开接口,并且token存在,则向headers中添加token
config.headers.Authorization = "Bearer " + token;
}
axios 更详细的使用,详见
https://blog.csdn.net/weixin_41192489/article/details/113878619
mounted() {
this.userName = sessionStorage.getItem("userName");
},
<div class="loginBox" v-if="!userName">
<el-button type="text" class="btn" @click="gotoLogin">登录el-button>
<el-divider direction="vertical">el-divider>
<el-button type="text" class="btn" @click="gotoRegister">注册el-button>
div>
<div class="helloBox" v-else>
<span>欢迎你,{{ userName }} !span>
<el-button @click="logout" type="text" class="btn">退出el-button>
div>
div>