【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即可

java 复制代码
    @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 代码案例
java 复制代码
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中新增登录窗口
javascript 复制代码
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

javascript 复制代码
<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 新增路由
javascript 复制代码
{
    path: 'loginback', component: ()=>import("@/views/setting/LoginBack.vue"),
 },

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

4.管理用户登录信息

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

前端的一般是使用pinia做持久化维护,安装piniad 插件

pinia-plugin-persistedstate

新增用户themeStore.ts

javascript 复制代码
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 在顶部登录页面
javascript 复制代码
<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>


相关推荐
今天也想MK代码1 小时前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
yqcoder4 小时前
electron 中 ipcRenderer 的常用方法有哪些?
前端·javascript·electron
yqcoder6 小时前
electron 中 ipcRenderer 作用
前端·javascript·electron
虞泽1 天前
鸢尾博客项目开源
java·spring boot·vue·vue3·博客
伍嘉源1 天前
关于electron进程管理的一些认识
前端·javascript·electron
yqcoder1 天前
electron 设置最小窗口缩放
前端·javascript·electron
前端杂货铺2 天前
简记Vue3(三)—— ref、props、生命周期、hooks
vue.js·vue3
静谧的美3 天前
vue3-element-admin 去掉登录
vue.js·前端框架·vue3·去掉登录
朝阳393 天前
vue3【组件封装】确认对话框 Modal
vue3·组件封装