OAuth2 完成用户登录【详解】(含码云 gitee 的实现范例)

简介: OAuth2 完成用户登录【详解】(含码云 gitee 的实现范例)

什么是 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 来自码云自动生成的密钥,创建方法如下:

  1. 登录码云官网,进入设置页
    https://gitee.com/
  2. 打开第三方应用
  3. 创建应用
  4. 录入相关信息

自定义需使用 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>


目录
相关文章
|
存储 安全 Java
OAuth2实现单点登录SSO完整教程,其实不难!(上)
OAuth2实现单点登录SSO完整教程,其实不难!
4716 1
OAuth2实现单点登录SSO完整教程,其实不难!(上)
|
Java 应用服务中间件 nginx
GitLab 配置 OAuth2 实现第三方登录,简直太方便了!
GitLab 配置 OAuth2 实现第三方登录,简直太方便了!
GitLab 配置 OAuth2 实现第三方登录,简直太方便了!
|
SQL JSON 安全
Spring Authorization Server OAuth2授权服务器配置详解
Spring Authorization Server OAuth2授权服务器配置详解
4264 0
|
10月前
|
存储 NoSQL MongoDB
Docker中安装MongoDB并配置数据、日志、配置文件持久化。
现在,你有了一个运行在Docker中的MongoDB,它拥有自己的小空间,对高楼大厦的崩塌视而不见(会话丢失和数据不持久化的问题)。这个MongoDB的数据、日志、配置文件都会妥妥地保存在你为它精心准备的地方,天旋地转,它也不会失去一丁点儿宝贵的记忆(即使在容器重启后)。
1231 4
|
Java Spring
SpringBoot: 启动Banner在线生成工具
SpringBoot: 启动Banner在线生成工具
39967 1
SpringBoot: 启动Banner在线生成工具
|
存储 缓存 Java
谷粒商城笔记+踩坑汇总篇
环境的搭建、商品服务-三级分类、品牌服务、阿里云云存储+JSR303数字校验+统一异常处理、spu+sku、分页拦截器、商品服务、仓库服务、Nginx反向代理,thymeleaf+动态展示三级分类、缓存与分布式锁,Redisson+缓存数据一致性、ElasticSearch检索服务、异步和线程池、商品详情搭建+异步编排、认证服务、阿里云短信+验证码防刷+BCrypt加密、用户名密码登录+微博社交登录+SpringSession+xxl-sso单点登录、购物车、订单服务、幂等性、库存自动解锁。MQ延迟队列
谷粒商城笔记+踩坑汇总篇
|
JavaScript 前端开发 API
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
本文介绍了如何在Vue 3项目中使用v-md-editor组件库来创建markdown编辑器和预览组件。文章提供了安装步骤、如何在main.js中进行全局配置、以及如何在页面中使用VMdEditor和VMdPreview组件的示例代码。此外,还提供了一个完整示例的链接,包括编辑器和预览组件的使用效果和代码。
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
|
存储 安全 Java
Spring Security 6.x OAuth2登录认证源码分析
上一篇介绍了Spring Security框架中身份认证的架构设计,本篇就OAuth2客户端登录认证的实现源码做一些分析。
1352 2
Spring Security 6.x OAuth2登录认证源码分析
|
消息中间件 传感器 负载均衡
消息队列 MQ使用问题之如何配置一主一从的同步复制模式
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
消息队列 MQ使用问题之如何配置一主一从的同步复制模式
|
前端开发 开发者
基于.net8 和 Vue3 的前后端分离管理系统搭建
一款名为PurestAdmin的前后端分离系统,基于.net8和Vue3,实现了真正意义上的分离,服务端仅关注功能接口,客户端通过功能编码控制路由。选用Vue3提升交互体验,利用ABP框架保证后端性能。项目特点是无业务功能、单Token刷新、接口权限验证和在线用户管理。源码可在Gitee、GitHub找到,提供在线预览和文档支持。历经多次重构,旨在促进快速开发,降低框架搭建难度。
1239 1