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


    什么是 OAuth2 登录?

    OAuth2 是当下广泛流行的第三方应用授权登录方案,如微信扫码登录

    OAuth2 的工作原理

    在这里插入图片描述

    OAuth2 实战范例

    在这里插入图片描述

    前端第三方登录图标

    以码云 gitee 为例,在登录页面添加登录图标(vue+element)
    在这里插入图片描述
    核心代码

        
        <el-avatar
          class="bnt"
          size="medium"
          :src="giteeUrl"
          @click.native="loginByGitee"
        >el-avatar>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    import giteeLogo from "@/assets/images/giteeLogo.svg";
    
    • 1

    data() 中

    giteeUrl: giteeLogo,
    
    • 1
        loginByGitee() {
          // 后端服务器跳转到gitee登录页的接口
          window.open("http://127.0.0.1:7002/api/users/passport/gotoGitee");
        },
    
    • 1
    • 2
    • 3
    • 4

    后端跳转到第三方应用

    egg.js 搭建的后端服务器为例

    app\router.ts

      // 跳转到 gitee 官网进行登录认证
      router.get('/api/users/passport/gotoGitee', controller.user.gotoGitee);
    
    • 1
    • 2

    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`
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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',
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    .env

    GITEE_CID = "e7440791b41de36978e65b258228e26b5d746**********"
    GITEE_SECRET = "c1744f6a7224182b3c39df2b76f2608e750a46c4f907b3569*****"
    
    • 1
    • 2

    GITEE_CID 和 GITEE_SECRET 来自码云自动生成的密钥,创建方法如下:

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

    自定义需使用 OAuth2 登录的应用名称
    在这里插入图片描述
    上传需使用 OAuth2 登录的应用的logo,填写应用的域名(暂时没购买也没关系),填写应用回调地址,通常对应后端接口。
    在这里插入图片描述
    5. 点击创建应用按钮

    默认登录后可以访问获取用户信息的接口,若需访问其他接口,可根据需要打钩
    在这里插入图片描述
    6. 完成创建后,便会得到 GITEE_CID 和 GITEE_SECRET
    在这里插入图片描述

    后端响应第三方应用授权后的回调生成 token

    app\router.ts

      // 通过gitee登录的回调
      router.get(
        '/api/users/passport/gitee/callback',
        controller.user.oauthByGitee
      );
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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' });
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
      }
    
    • 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

    获取 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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    获取用户信息

      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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    后端渲染页面向前端传递 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>
    
    • 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

    此处使用 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");
            });
          }
        });
      },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    针对需 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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    axios 更详细的使用,详见
    https://blog.csdn.net/weixin_41192489/article/details/113878619

    前端首页获取用户信息更新登录状态

      mounted() {
        this.userName = sessionStorage.getItem("userName");
      },
    
    • 1
    • 2
    • 3
          <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    MySQL5.7高级函数:JSON_ARRAYAGG和JSON_OBJECT的使用
    vue使用qrcodejs2生成二维码
    【day10】接口、多态
    vue3.2的特性
    面向对象编程三⼤特性 --封装、继承、多态
    【UE5.1 角色练习】11-坐骑——Part1(控制大象移动)
    数据结构系列学习(六) - 顺序栈(Stack)
    任意文件读取漏洞
    久运恒远链接品牌和物业建设开拓新赛道
    搞安全开发都是用什么编程语言?
  • 原文地址:https://blog.csdn.net/weixin_41192489/article/details/127764469