mirror of
https://github.com/certd/certd.git
synced 2026-04-14 12:30:54 +08:00
perf: 支持微信扫码登录
This commit is contained in:
@@ -766,7 +766,10 @@ export default {
|
||||
oauthProviders: "OAuth2 Login Providers",
|
||||
oauthType: "OAuth2 Login Type",
|
||||
oauthConfig: "OAuth2 Login Config",
|
||||
oauthProviderSelectorPlaceholder: "Please select OAuth2 login provider",
|
||||
oauthProviderSelectorPlaceholder: "Not Configured",
|
||||
oauthCallback: "Callback URL",
|
||||
oauthCallbackHelper: "Copy this URL to the callback address of the OAuth2 login provider",
|
||||
oauthCallbackCopy: "Copy Callback URL",
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
|
||||
@@ -767,7 +767,10 @@ export default {
|
||||
oauthProviders: "第三方登录提供商",
|
||||
oauthType: "第三方登录类型",
|
||||
oauthConfig: "第三方登录配置",
|
||||
oauthProviderSelectorPlaceholder: "请选择第三方登录提供商",
|
||||
oauthProviderSelectorPlaceholder: "未配置",
|
||||
oauthCallback: "回调地址",
|
||||
oauthCallbackHelper: "复制回调地址,配置到对应提供商的回调地址中",
|
||||
oauthCallbackCopy: "复制回调地址",
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="addon-selector">
|
||||
<div class="flex-o w-100">
|
||||
<div class="flex-o w-100 inner">
|
||||
<!-- <fs-dict-select class="flex-1" :value="modelValue" :dict="optionsDictRef" :disabled="disabled" :render-label="renderLabel" :slots="selectSlots" :allow-clear="true" v-bind="select" @update:value="onChange" />-->
|
||||
<span v-if="modelValue" class="mr-5 cd-flex-inline">
|
||||
<a-tag class="mr-5" color="green">{{ target?.name || modelValue }}</a-tag>
|
||||
@@ -175,5 +175,9 @@ async function doRefresh() {
|
||||
<style lang="less">
|
||||
.addon-selector {
|
||||
width: 100%;
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,7 +121,14 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any, a
|
||||
},
|
||||
editForm: {
|
||||
component: {
|
||||
disabled: false,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
addForm: {
|
||||
component: {
|
||||
disabled: compute(({ form }) => {
|
||||
return form.type ? true : false;
|
||||
}),
|
||||
},
|
||||
},
|
||||
form: {
|
||||
|
||||
@@ -40,6 +40,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
search: {
|
||||
initialForm: {
|
||||
addonType: addonType,
|
||||
type: type,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
labelCol: {
|
||||
//固定label宽度
|
||||
|
||||
@@ -2,13 +2,14 @@ import { request } from "/src/api/service";
|
||||
|
||||
const apiPrefix = "/oauth";
|
||||
|
||||
export async function OauthLogin(type: string, forType?: string) {
|
||||
export async function OauthLogin(type: string, forType?: string, from?: string) {
|
||||
return await request({
|
||||
url: apiPrefix + `/login`,
|
||||
method: "post",
|
||||
data: {
|
||||
type,
|
||||
forType: forType || "login",
|
||||
from: from || "web",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<div class="oauth-title">
|
||||
<div class="oauth-title-text">其他方式登录</div>
|
||||
</div>
|
||||
<div v-for="item in oauthList" :key="item.type">
|
||||
<div class="oauth-icon-button pointer" @click="goOauthLogin(item.name)">
|
||||
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
|
||||
<div>{{ item.title }}</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-4">
|
||||
<template v-for="item in oauthProviderList" :key="item.type">
|
||||
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item.name)">
|
||||
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
|
||||
<div>{{ item.addonTitle || item.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,15 +17,16 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import * as api from "./api";
|
||||
|
||||
const oauthList = ref([]);
|
||||
const oauthProviderList = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
oauthList.value = await api.GetOauthProviders();
|
||||
oauthProviderList.value = await api.GetOauthProviders();
|
||||
});
|
||||
|
||||
async function goOauthLogin(type: string) {
|
||||
//获取第三方登录URL
|
||||
const res = await api.OauthLogin(type);
|
||||
const from = "web";
|
||||
const res = await api.OauthLogin(type, from);
|
||||
const loginUrl = res.loginUrl;
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function GetSmsTypeDefine(type: string) {
|
||||
|
||||
export async function GetOauthProviders() {
|
||||
return await request({
|
||||
url: apiPrefix + "/oauth/providers",
|
||||
url: "/oauth/providers",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,8 +67,9 @@
|
||||
<table class="w-full table-auto border-collapse border border-gray-400">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthType") }}</th>
|
||||
<th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthConfig") }}</th>
|
||||
<th class="border border-gray-300 px-4 py-2 w-1/3">{{ t("certd.sys.setting.oauthType") }}</th>
|
||||
<th class="border border-gray-300 px-4 py-2 w-1/3">{{ t("certd.sys.setting.oauthCallback") }}</th>
|
||||
<th class="border border-gray-300 px-4 py-2 w-1/3">{{ t("certd.sys.setting.oauthConfig") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -79,6 +80,11 @@
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-4 py-2 overflow-ellipsis" :title="t('certd.sys.setting.oauthCallbackHelper')">
|
||||
<fs-copyable :model-value="buildCallbackUrl(item.name)">
|
||||
{{ t("certd.sys.setting.oauthCallbackCopy") }}
|
||||
</fs-copyable>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-4 py-2">
|
||||
<AddonSelector v-model:model-value="item.addonId" addon-type="oauth" from="sys" :type="item.name" :placeholder="t('certd.sys.setting.oauthProviderSelectorPlaceholder')" />
|
||||
</td>
|
||||
@@ -96,14 +102,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { computed, reactive, ref, Ref } from "vue";
|
||||
import { GetSmsTypeDefine, SysSettings } from "/@/views/sys/settings/api";
|
||||
import * as api from "/@/views/sys/settings/api";
|
||||
import { merge } from "lodash-es";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { merge } from "lodash-es";
|
||||
import { reactive, ref, Ref } from "vue";
|
||||
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import * as api from "/@/views/sys/settings/api";
|
||||
import { SysSettings } from "/@/views/sys/settings/api";
|
||||
import { useI18n } from "/src/locales";
|
||||
const { t } = useI18n();
|
||||
|
||||
defineOptions({
|
||||
@@ -192,15 +198,7 @@ async function loadTypeDefine(type: string) {
|
||||
|
||||
const oauthProviders = ref([]);
|
||||
async function loadOauthProviders() {
|
||||
let list: any = await api.GetOauthProviders();
|
||||
oauthProviders.value = list;
|
||||
for (const item of list) {
|
||||
const type = item.name;
|
||||
const provider = formState.public.oauthProviders?.[type];
|
||||
if (provider) {
|
||||
item.addonId = provider.addonId;
|
||||
}
|
||||
}
|
||||
oauthProviders.value = await api.GetOauthProviders();
|
||||
}
|
||||
|
||||
function fillOauthProviders(form: any) {
|
||||
@@ -251,8 +249,19 @@ const onFinish = async (form: any) => {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function buildCallbackUrl(type: string) {
|
||||
return `${window.location.origin}/api/oauth/callback/${type}`;
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.sys-settings-site {
|
||||
.sys-settings-register {
|
||||
width: 1000px !important;
|
||||
|
||||
.addon-selector {
|
||||
.inner {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addonRegistry, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server";
|
||||
import { addonRegistry, AddonService, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server";
|
||||
import { ALL, Body, Controller, Get, Inject, Param, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js";
|
||||
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
|
||||
@@ -31,6 +31,9 @@ export class ConnectController extends BaseController {
|
||||
@Inject()
|
||||
oauthBoundService: OauthBoundService;
|
||||
|
||||
@Inject()
|
||||
addonService: AddonService;
|
||||
|
||||
|
||||
|
||||
private async getOauthProvider(type: string) {
|
||||
@@ -51,14 +54,14 @@ export class ConnectController extends BaseController {
|
||||
}
|
||||
|
||||
@Post('/login', { summary: Constants.per.guest })
|
||||
public async login(@Body(ALL) body: { type: string, forType?:string }) {
|
||||
public async login(@Body(ALL) body: { type: string, forType?:string ,from?:string }) {
|
||||
|
||||
const addon = await this.getOauthProvider(body.type);
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
const bindUrl = installInfo?.bindUrl || "";
|
||||
//构造登录url
|
||||
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
|
||||
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType });
|
||||
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType ,from: body.from || "web" });
|
||||
const ticket = this.codeService.setValidationValue(ticketValue)
|
||||
this.ctx.cookies.set("oauth_ticket", ticket, {
|
||||
httpOnly: true,
|
||||
@@ -217,10 +220,32 @@ export class ConnectController extends BaseController {
|
||||
return this.ok(bounds);
|
||||
}
|
||||
|
||||
@Post('/providers', { summary: Constants.per.guest })
|
||||
|
||||
@Post('/providers', { summary: Constants.per.guest })
|
||||
public async providers() {
|
||||
const list = addonRegistry.getDefineList("oauth");
|
||||
const defineList = addonRegistry.getDefineList("oauth");
|
||||
|
||||
const publicSetting = await this.sysSettingsService.getPublicSettings();
|
||||
const oauthProviders = publicSetting.oauthProviders || {};
|
||||
const list = [];
|
||||
|
||||
for (const item of defineList) {
|
||||
const type = item.name
|
||||
const conf = oauthProviders[type];
|
||||
const provider:any = {
|
||||
...item,
|
||||
}
|
||||
delete provider.input
|
||||
if (conf && conf.addonId) {
|
||||
const addonEntity = await this.addonService.info(conf.addonId);
|
||||
if (addonEntity) {
|
||||
provider.addonId = conf.addonId;
|
||||
provider.addonTitle = addonEntity.name;
|
||||
}
|
||||
}
|
||||
list.push(provider);
|
||||
}
|
||||
|
||||
return this.ok(list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,13 @@ export type LoginUrlReply = {
|
||||
ticketValue: any;
|
||||
}
|
||||
|
||||
export type BuildLoginUrlReq = {
|
||||
redirectUri: string;
|
||||
forType?: string;
|
||||
from?:string;
|
||||
}
|
||||
|
||||
export interface IOauthProvider {
|
||||
buildLoginUrl: (params: { redirectUri: string, forType?: string }) => Promise<LoginUrlReply>;
|
||||
buildLoginUrl: (params: BuildLoginUrlReq) => Promise<LoginUrlReply>;
|
||||
onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './api.js'
|
||||
export * from './oidc/plugin-oidc.js'
|
||||
export * from './wx/plugin-wx.js'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
||||
import { IOauthProvider, OnCallbackReq } from "../api.js";
|
||||
import { BuildLoginUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
|
||||
|
||||
@IsAddon({
|
||||
addonType: "oauth",
|
||||
@@ -56,7 +56,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async buildLoginUrl(params: { redirectUri: string, forType?: string }) {
|
||||
async buildLoginUrl(params: BuildLoginUrlReq) {
|
||||
const { config, client } = await this.getClient()
|
||||
|
||||
let redirect_uri = new URL(params.redirectUri)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
||||
import { BuildLoginUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
|
||||
|
||||
@IsAddon({
|
||||
addonType: "oauth",
|
||||
name: 'wx',
|
||||
title: '微信登录',
|
||||
desc: '微信网站应用登录',
|
||||
icon: "mdi:wechat",
|
||||
showTest: false,
|
||||
})
|
||||
export class WxOauthProvider extends BaseAddon implements IOauthProvider {
|
||||
|
||||
@AddonInput({
|
||||
title: "AppId",
|
||||
required: true,
|
||||
helper: "在[微信开放平台](https://open.weixin.qq.com/cgi-bin/index)注册网站应用后获取",
|
||||
})
|
||||
appId = "";
|
||||
|
||||
@AddonInput({
|
||||
title: "AppSecretKey",
|
||||
component: {
|
||||
placeholder: "AppSecretKey",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
appSecretKey = "";
|
||||
|
||||
|
||||
wxAccessToken?: { access_token: string, expires_at: number }
|
||||
|
||||
async buildLoginUrl(params: BuildLoginUrlReq) {
|
||||
|
||||
const from = params.from || "web";
|
||||
const appId = this.appId;
|
||||
const redirect_uri = encodeURIComponent(params.redirectUri);
|
||||
let state: any = {
|
||||
forType: params.forType,
|
||||
from
|
||||
}
|
||||
state = this.ctx.utils.hash.base64(JSON.stringify(state))
|
||||
let scope = "snsapi_userinfo";
|
||||
let loginUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
|
||||
if (from === "web") {
|
||||
scope = "snsapi_login";
|
||||
loginUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
|
||||
}
|
||||
|
||||
return {
|
||||
loginUrl,
|
||||
ticketValue: {
|
||||
state,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// async getWxAccessToken() {
|
||||
// if (this.wxAccessToken && this.wxAccessToken.expires_at > Date.now()) {
|
||||
// return this.wxAccessToken
|
||||
// }
|
||||
// const res = await this.http.request({
|
||||
// url: "https://api.weixin.qq.com/cgi-bin/token",
|
||||
// method: "GET",
|
||||
// params: {
|
||||
// appid: this.appId,
|
||||
// secret: this.appSecretKey,
|
||||
// grant_type: "client_credential",
|
||||
// },
|
||||
// })
|
||||
// this.checkRet(res)
|
||||
// this.wxAccessToken = {
|
||||
// access_token: res.access_token,
|
||||
// expires_at: Date.now() + res.expires_in * 1000,
|
||||
// }
|
||||
// return this.wxAccessToken
|
||||
// }
|
||||
|
||||
checkRet(res: any) {
|
||||
if (res.errcode) {
|
||||
throw new Error(res.errmsg)
|
||||
}
|
||||
}
|
||||
|
||||
async onCallback(req: OnCallbackReq) {
|
||||
|
||||
|
||||
// GET https://api.weixin.qq.com/sns/oauth2/access_token?appid=wx520c15f417810387&secret=SECRET&code=CODE&grant_type=authorization_code
|
||||
const res = await this.http.request({
|
||||
url: "https://api.weixin.qq.com/sns/oauth2/access_token",
|
||||
method: "GET",
|
||||
params: {
|
||||
appid: this.appId,
|
||||
secret: this.appSecretKey,
|
||||
code: req.code,
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
})
|
||||
this.checkRet(res)
|
||||
const accessToken = res.access_token
|
||||
|
||||
|
||||
// GET https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
|
||||
const userInfoRes = await this.http.request({
|
||||
url: "https://api.weixin.qq.com/sns/userinfo",
|
||||
method: "GET",
|
||||
params: {
|
||||
access_token:accessToken,
|
||||
openid: res.openid,
|
||||
lang: "zh_CN",
|
||||
},
|
||||
})
|
||||
this.checkRet(userInfoRes)
|
||||
|
||||
return {
|
||||
token: {
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
expiresIn: res.expires_in,
|
||||
},
|
||||
userInfo: {
|
||||
openId: res.unionid || res.openid,
|
||||
nickName: userInfoRes.nickname || "",
|
||||
avatar: userInfoRes.headimgurl,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user