【electron+vue3】使用JustAuth实现第三方登录(前后端完整版)

实现过程

  1. 去第三方平台拿到client-id和client-secret,并配置一个能够外网访问回调地址redirect-uri供第三方服务回调
  2. 搭建后端服务,引入justauth-spring-boot-starter直接在配置文件中定义好第一步的三个参数,并提供获取登录页面的接口和回调接口
  3. 前端项目中新建一个登录窗口和一个登录中转页面,登录窗口的url从第二步第一个接口获取,中转页面从第二步的第二个接口返回
  4. 中转页面从url中读取登录成功的用户信息并存放到pinia中,关闭登录窗口并刷新主窗口

1,必要信息获取

第三方平台的client-id和client-secret一般注册开发者平台都能获取。
回调地址需要外网,可以使用花生壳内网穿透随便搞一个,映射到本地的后台服务端口,当后天服务启动成功后确保连接成功
在这里插入图片描述
前端代理也可以直接代理到这个域名,前后端完全分离

2,后台服务搭建

2.1 后台如果使用springboot2.x可以从开源框架直接使用:

https://gitee.com/justauth/justauth-spring-boot-starter
只需将上一步获取的三个参数配置到yml文件中

2.2 AuthRequestFactory错误

如果使用的springboot3.x,可能会报错提示:

‘com.xkcoding.justauth.AuthRequestFactory’ that could not be found.

只需要将AuthRequestFactory、JustAuthProperties、AuthStateRedisCache从源码复制一份到项目中,补全@Configuration、@Component,然后补上一个Bean即可

    @Bean
    public AuthRequestFactory getAuthRequest(JustAuthProperties properties, AuthStateRedisCache authStateCache) {
        return new AuthRequestFactory(properties,authStateCache);
    }
2.3 redis错误

justauth-spring-boot-starter项目中的redis配置是springboot2.x的配置,
如果是3.x的项目需要将 spring:reids改为 spring:data:reids

2.4 代码案例
import com.alibaba.fastjson.JSONObject;
import io.geekidea.springboot.cache.AuthRequestFactory;
import io.geekidea.springboot.service.UserService;
import io.geekidea.springboot.vo.ResponseResult;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import io.geekidea.springboot.cache.JustAuthProperties;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthBaiduRequest;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @Description https://blog.csdn.net/weixin_46684099/article/details/118297276
 * @Date 2024/10/23 16:30
 * @Author 余乐
 **/
@Slf4j
@Controller
@RequestMapping("/oauth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class JustAuthController {

    private final UserService userService;
    private final AuthRequestFactory factory;
    private final JustAuthProperties properties;

    @GetMapping
    public List<String> list() {
        return factory.oauthList();
    }

    @RequestMapping("/render/{source}")
    @ResponseBody
    public ResponseResult renderAuth(@PathVariable("source") String source) {
        AuthRequest authRequest = null;
        //特定平台需要自定义参数的可以单独写AuthConfig
        if ("baidu".equals(source)) {
            //百度账号默认只有basic,需要网盘权限需要单独定义
            List<String> list = new ArrayList<>();
            list.add("basic");
            list.add("netdisk");
            Map<String,AuthConfig> configMap = properties.getType();
            AuthConfig authConfig = configMap.get("BAIDU");
            authConfig.setScopes(list);
            authRequest = new AuthBaiduRequest(authConfig);
        } else {
            //其他平台账号登录
            authRequest = factory.get(source);
        }
        String state = AuthStateUtils.createState();
        String authorizeUrl = authRequest.authorize(state);
        return ResponseResult.success(authorizeUrl);
    }

    /**
     * oauth平台中配置的授权回调地址,以本项目为例,在创建github授权应用时的回调地址应为:http://127.0.0.1:8444/oauth/callback/github
     */
    @RequestMapping("/callback/{source}")
    public void login(@PathVariable("source") String source, AuthCallback callback, HttpServletResponse response2) throws IOException {
        log.info("进入callback:{},callback params:{}", source, JSONObject.toJSONString(callback));
        AuthRequest authRequest = null;
        //特定平台需要自定义参数的可以单独写AuthConfig
        if ("baidu".equals(source)) {
            //百度账号默认只有basic,需要网盘权限需要单独定义
            List<String> list = new ArrayList<>();
            list.add("basic");
            list.add("netdisk");
            Map<String,AuthConfig> configMap = properties.getType();
            AuthConfig authConfig = configMap.get("BAIDU");
            authConfig.setScopes(list);
            authRequest = new AuthBaiduRequest(authConfig);
        } else {
            //其他平台账号登录
            authRequest = factory.get(source);
        }
        AuthResponse<AuthUser> response = authRequest.login(callback);
        String userInfo = JSONObject.toJSONString(response.getData());
        log.info("回调用户信息:{}", userInfo);
        if (response.ok()) {
            userService.save(response.getData());
            String userInfoParam = URLEncoder.encode(userInfo, "UTF-8");
            //将用户信息放到中转页面的路由参数中,前端从路由参数获取登陆结果
            response2.sendRedirect("http://localhost:5173/loginback?data=" + userInfoParam);
        }
    }

    /**
     * 注销登录 (前端需要同步清理用户缓存)
     *
     * @param source
     * @param uuid
     * @return
     * @throws IOException
     */
    @RequestMapping("/revoke/{source}/{uuid}")
    @ResponseBody
    public ResponseResult revokeAuth(@PathVariable("source") String source, @PathVariable("uuid") String uuid) throws IOException {
        AuthRequest authRequest = factory.get(source.toLowerCase());

        AuthUser user = userService.getByUuid(uuid);
        if (null == user) {
            return ResponseResult.fail("用户不存在");
        }
        AuthResponse<AuthToken> response = null;
        try {
            response = authRequest.revoke(user.getToken());
            if (response.ok()) {
                userService.remove(user.getUuid());
                return ResponseResult.success("用户 [" + user.getUsername() + "] 的 授权状态 已收回!");
            }
            return ResponseResult.fail("用户 [" + user.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
        } catch (AuthException e) {
            return ResponseResult.fail(e.getErrorMsg());
        }
    }

    /**
     * 刷新token
     *
     * @param source
     * @param uuid
     * @return
     */
    @RequestMapping("/refresh/{source}/{uuid}")
    @ResponseBody
    public ResponseResult<String> refreshAuth(@PathVariable("source") String source, @PathVariable("uuid") String uuid) {
        AuthRequest authRequest = factory.get(source.toLowerCase());

        AuthUser user = userService.getByUuid(uuid);
        if (null == user) {
            return ResponseResult.fail("用户不存在");
        }
        AuthResponse<AuthToken> response = null;
        try {
            response = authRequest.refresh(user.getToken());
            if (response.ok()) {
                user.setToken(response.getData());
                userService.save(user);
                return ResponseResult.success("用户 [" + user.getUsername() + "] 的 access token 已刷新!新的 accessToken: " + response.getData().getAccessToken());
            }
            return ResponseResult.fail("用户 [" + user.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
        } catch (AuthException e) {
            return ResponseResult.fail(e.getErrorMsg());
        }
    }
}

3 新建登录窗口和中转页面

3.1 在src/main/index.ts中新增登录窗口
let loginWindow

//监听打开登录窗口的事件
ipcMain.on('openLoginWin', (event, url) => {
  console.log('打开登录窗口', url)
  createLoginWindow(url)
})


// 创建登录窗口
function createLoginWindow(url: string) {
  loginWindow = new BrowserWindow({
    width: 800,
    height: 600,
    frame: false,
    titleBarStyle: 'hidden',   
    autoHideMenuBar: true,
    parent: mainWindow,   //父窗口为主窗口
    modal: true,
    show: false,
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      nodeIntegration: true,
      contextIsolation: true
    }
  })
  // 加载登录 URL
  loginWindow.loadURL(url)
  loginWindow.on('ready-to-show', () => {
    loginWindow.show()
  })
}

// 关闭登录窗口并刷新主窗口
ipcMain.handle('close-login', () => {
  if (loginWindow) {
    loginWindow.close()
  }
  if (mainWindow) {
    mainWindow.reload() // 刷新主窗口 }
  }
})
3.2 新增中转页面并配置路由

@/views/setting/LoginBack.vue

<template>
  <el-row justify="center">
    <cl-col :span="17">
      <h2>登陆结果</h2>
      <el-icon style="color:#00d28c;font-size: 50px">
        <i-mdi-check-circle />
      </el-icon>
    </cl-col>
  </el-row>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { onMounted } from 'vue'
import { useThemeStore } from '@/store/themeStore'

const route = useRoute()
const data = route.query.data
const themeStore = useThemeStore()
//登陆成功自动关闭窗口
onMounted(() => {
  console.log("登陆结果",data)
  themeStore.setCurrentUser(JSON.parse(data))
  setTimeout(() => {
    //关闭当前登录回调的窗口,并且刷新主窗口页面
    window.electron.ipcRenderer.invoke('close-login')
  }, 1000)
})
</script>

3.3 新增路由
{
    path: 'loginback', component: ()=>import("@/views/setting/LoginBack.vue"),
 },

这里的路由对应的就是后台/callback 接口重定向的地址

4.管理用户登录信息

后端用户登录信息保存在redis中,如果过期可以使用客户端中缓存的用户uuid刷新token

前端的一般是使用pinia做持久化维护,安装piniad 插件
pinia-plugin-persistedstate
新增用户themeStore.ts

import { defineStore } from 'pinia';

export const useThemeStore = defineStore('userInfoStore', {
    state: () => {
        // 从 localStorage 获取主题,如果没有则使用默认值
        //const localTheme = localStorage.getItem('localTheme') || 'cool-black';
        return {
            currentTheme: 'cool-black',
            userInfo: {}
        };
    },
    actions: {
        setCurrentThemeId(theme: string) {
            console.log("修改主题", theme);
            this.currentTheme = theme; // 更新当前主题
            document.body.setAttribute('data-theme', theme); // 更新 data-theme
        },
        setCurrentUser(user: any) {
            console.log("修改账号", user);
            this.userInfo = user; // 更新当前账号
        },
    },
    //开启持久化 = 》 localStorage
    persist: {
        key: 'userInfoStore',
        onstorage: localStorage,
        path: ['currentTheme','userInfo']
    }
});

5. 运行调试

5.1 在顶部登录页面
<div v-if="userInfo.avatar">
        <el-avatar :src="userInfo.avatar" :size="30"/>
        <el-popover :width="300" trigger="click">
          <template #reference>
            <p>{{userInfo.nickname}}</p>
          </template>
          <template #default>
            <div  class="demo-rich-conent" style="display: flex; gap: 16px; flex-direction: column">
              <el-avatar
                :size="60"
                src="https://avatars.githubusercontent.com/u/72015883?v=4"
                style="margin-bottom: 8px"
              />
              <el-divider />
              <h5 @click="logout(userInfo.uuid)">退出登录</h5>
            </div>
          </template>
        </el-popover>
      </div>
      <div v-else @click.stop="openLoginCard">
        <el-avatar :icon="UserFilled" :size="30"/>
        <p>未登录</p>
      </div>

<script lang="ts" setup>
import {ref} from 'vue'
import { LoginOut } from '@/api/baidu'
import {useThemeStore} from "@/store/themeStore";
import { UserFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { getLoginPageUrl } from '../../api/baidu'
const themeStore = useThemeStore();
const router = useRouter()
let searchVal = ref('')
let userInfo=ref({})

if (themeStore.userInfo){
  userInfo.value = themeStore.userInfo
}
//打开登录弹窗
function openLoginCard(){
  getLoginPageUrl().then(resp => {
    console.log("获取登陆地址",resp.data)
    window.electron.ipcRenderer.send('openLoginWin',resp.data.data)
  });
}
//退出登录
function logout(uuid:string){
  LoginOut(uuid).then(resp => {
    console.log("注销登录",resp.data)
    themeStore.setCurrentUser({})
    window.location.reload()
  });
}
</script>

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/907774.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一次线程池使用错误导致的问题

记录一次服务线程数量异常问题的排查过程 背景 通过监控发现一个服务的线程数异常多 同期CPU 内存 网络连接都没有什么异常。 排查 第一个反应就是查看线程栈 "pool-2493-thread-3" #3718833 prio5 os_prio0 tid0x00007f1610041000 nid0x38bff6 waiting on con…

我为何要用wordpress搭建一个自己的独立博客

我在csdn有一个博客&#xff0c;这个博客是之前学习编程时建立的。 博客有哪些好处呢&#xff1f; 1&#xff0c;可以写自己的遇到的问题和如何解决的步骤 2&#xff0c;心得体会&#xff0c;经验&#xff0c;和踩坑 3&#xff0c;可以转载别人的好的技术知识 4&#xff0c;宝贵…

java毕业设计之基于Bootstrap的常州地方旅游管理系统的设计与实现(springboot)

项目简介 基于Bootstrap的常州地方旅游管理系统的设计与实现有下功能&#xff1a; 基于Bootstrap的常州地方旅游管理系统的设计与实现的主要使用者分为用户功能模块和管理员功能模块两大部分&#xff0c;用户可查看景点信息、景点资讯等&#xff0c;注册登录后可进行景点订票…

面试经典 150 题:189、383

189. 轮转数组 【参考代码】 class Solution { public:void rotate(vector<int>& nums, int k) {int size nums.size();if(1 size){return;}vector<int> temp(size);//k k % size;for(int i0; i<size; i){temp[(i k) % size] nums[i];}nums temp; }…

mysql--多表查询

一、联合查询 作用&#xff1a;合并结果集就是把两个select语句的查询结果合并到一起&#xff01; 合并结果集有两种方式&#xff1a; UNION&#xff1a;合并并去除重复记录&#xff0c;例如&#xff1a;SELECT * FROM t1 UNION SELECT * FROM t2&#xff1b; UNION ALL&a…

什么是严肃游戏,严肃游戏本地化的特点是什么?

“严肃游戏”是一种交互式数字体验&#xff0c;不仅用于娱乐&#xff0c;还用于教育、培训或解决问题。与主要关注乐趣和参与度的传统游戏不同&#xff0c;严肃游戏的目标不仅仅是娱乐&#xff0c;比如教授特定技能、模拟现实生活场景或提高对重要问题的认识。它们用于医疗保健…

ADI常规SHARC音频处理器性能对比

1、 ADSP-2156x:是基于SHARC+ DSP架构的单核32位/40位/64位浮点处理器,不仅具有灵活的音频连接性和性能可扩展性,还提供多个引脚兼容版本(400MHz至1GHz)和多种片内存储器选项,数据手册链接:https://www.analog.com/media/en/technical-documentation/data-sheets/adsp-2…

springboot 整合 抖音 移动应用 授权

后端开发&#xff0c;因为没有JavaSDK&#xff0c;maven依赖&#xff0c;用到的是API接口去调用 抖音API开发文档 开发前先申请好移动应用&#xff0c;抖音控制台-移动应用 之后还需要开通所有能开通的能力 拿到应用的 clientKey 和 clientSecret&#xff0c;就可以进入开发了 …

Python 三维图表绘制指南

Python 三维图表绘制指南 在数据可视化中&#xff0c;三维图表可以更直观地展示数据之间的关系&#xff0c;尤其是当数据具有多个维度时。Python 提供了多个库来绘制三维图表&#xff0c;其中最常用的就是 Matplotlib。本文将介绍如何使用 Matplotlib 绘制三维图表&#xff0c…

Node.js:Express 服务 路由

Node.js&#xff1a;Express 服务 & 路由 创建服务处理请求req对象 静态资源托管托管多个资源挂载路径前缀 路由模块化 Express是Node.js上的一个第三方框架&#xff0c;可以快速开发一个web框架。本质是一个包&#xff0c;可以通过npm直接下载。 创建服务 Express创建一…

计算机网络-以太网小结

前导码与帧开始分界符有什么区别? 前导码--解决帧同步/时钟同步问题 帧开始分界符-解决帧对界问题 集线器 集线器通过双绞线连接终端, 学校机房的里面就有集线器 这种方式仍然属于共享式以太网, 传播方式依然是广播 网桥: 工作特点: 1.如果转发表中存在数据接收方的端口信息…

学生成绩查询系统设计与实现

学生成绩查询系统设计与实现 1. 系统概述 学生成绩查询系统是一个基于PHP和SQL的Web应用程序&#xff0c;旨在为学校提供一个高效的学生成绩管理和查询平台。该系统可以帮助教师录入成绩、学生查询成绩、管理员管理用户和成绩数据&#xff0c;提高教育管理的效率和透明度。 2…

Rust 力扣 - 2653. 滑动子数组的美丽值

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们遍历长度为k的的窗口 因为数据范围比较小&#xff0c;所以我们可以通过计数排序找到窗口中第k小的数 如果小于0&#xff0c;则该窗口的美丽值为第k小的数如果大于等于0&#xff0c;则该窗口的美丽值为0 题…

VisualStudio远程编译调试linux_c++程序(二)

前章讲述了gdb相关&#xff0c;这章主要讲述用VisualStudio调试编译linux_c程序 1&#xff1a;环境 win10 VisualStudio 2022 Community ubuntu22.04 2:安装 1>vs安装时&#xff0c;勾选 使用c进行linux 和嵌入式开发 (这里以vs2022为例) OR VS安装好了&#xff0c; 选择工…

音视频听译:助力多维度沟通与发展的大门

在全球经济一体化的大背景下&#xff0c;企业之间的跨国合作愈发频繁。在商务会议、谈判和产品演示等活动中&#xff0c;语言的多样性成为了一大挑战。而音视频听译服务能够将不同语言的音频准确转换为目标语言文字&#xff0c;确保信息的精准传达&#xff0c;避免因语言障碍引…

基于MATLAB人脸检测的汽车疲劳驾驶检测

课题介绍 疲劳驾驶导致汽车交通事故逐年增加&#xff0c;为了提升驾车的安全性&#xff0c;需对驾驶员疲劳状态实时监测并及时提醒. 为了提高疲劳驾驶判断效率及准确率&#xff0c;本文运用Viola-Jones 框架特征矩阵进行人脸预判断&#xff1b;预判断过程中为了减少Haar 值计算…

论文阅读(三十二):EGNet: Edge Guidance Network for Salient Object Detection

文章目录 1.Introduction2.Related Works3.Salient Edge Guidance Network3.1Complementary information modeling3.1.1Progressive salient object features extraction3.1.2Non-local salient edge features extraction 3.2One-to-one guidance module 4.Experiments4.1Imple…

MySQL超大分页怎么优化处理?limit 1000000,10 和 limit 10区别?覆盖索引、面试题

1. limit 100000,10 和 limit 10区别 LIMIT 100000, 10&#xff1a; 这个语句的意思是&#xff0c;从查询结果中跳过前100000条记录&#xff0c;然后返回接下来的10条记录。这通常用于分页查询中&#xff0c;当你需要跳过大量的记录以获取后续的记录时。例如&#xff0c;如果你…

源码侦探:理解 numpy 中的 tile 方法

文章目录 pre &#xff1a;先来一张源码的切片1. 参数和基本定义&#xff1a;2. 将 reps 转换为元组&#xff1a;3. 提升数组维度&#xff1a;4. 特殊情况检查&#xff1a;5. 处理数组维度的不同情况&#xff1a;6. 计算输出数组的形状&#xff1a;7. 通过重复构造数组&#xf…

单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构

目录 一、合并两个有序链表 二、链表分割 三、链表的回文结构 u解题的总体思路&#xff1a; 合并两个有序链表&#xff1a;首先创建新链表的头节点&#xff08;哨兵位&#xff1a;本质上是占位子&#xff09;&#xff0c;为了减少一些判断情况&#xff0c;简化操作。然后我们…