什么是 OAuth2 登录?
OAuth2 是当下广泛流行的第三方应用授权登录方案,如微信扫码登录。
OAuth2 的工作原理
OAuth2 实战范例
前端第三方登录图标
以码云 gitee 为例,在登录页面添加登录图标(vue+element)
核心代码
<!-- gitee登录 --> <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 来自码云自动生成的密钥,创建方法如下:
- 登录码云官网,进入设置页
https://gitee.com/
- 打开第三方应用
- 创建应用
- 录入相关信息
自定义需使用 OAuth2 登录的应用名称
上传需使用 OAuth2 登录的应用的logo,填写应用的域名(暂时没购买也没关系),填写应用回调地址,通常对应后端接口。
点击创建应用按钮
默认登录后可以访问获取用户信息的接口,若需访问其他接口,可根据需要打钩
6. 完成创建后,便会得到 GITEE_CID 和 GITEE_SECRET
后端响应第三方应用授权后的回调生成 token
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; }
后端渲染页面向前端传递 token
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
获取到 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>