feat(login): switch login to google oauth flow
- add typed OauthTokenRequest interface for oauth token API - remove unused AgreePopup component and all agreement-related logic - rewrite login page to implement full Google OAuth2 login flow including state generation, callback validation and redirect handling
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { request } from "../utils/request";
|
||||
|
||||
// 获取oauth token
|
||||
export function oauthToken(args: any) {
|
||||
export interface OauthTokenRequest {
|
||||
clientId: string;
|
||||
grant_type?: string;
|
||||
openIdCode?: string[];
|
||||
scope?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export function oauthToken(args: OauthTokenRequest) {
|
||||
return request({
|
||||
url: "/auth/oauth2/token",
|
||||
method: "post",
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<van-popup ref="popup" position="center" v-model:show="show">
|
||||
<div class="w-[327px] bg-white rounded-[12px]">
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="relative px-[20px] pt-[20px]">
|
||||
<div class="text-[18px] font-[600] text-[#333] text-center leading-[24px]">{{ title }}</div>
|
||||
<div class="absolute top-[16px] right-[16px] w-[28px] h-[28px] flex items-center justify-center"
|
||||
@click="handleClose">
|
||||
<van-icon name="cross" size="24" color="#999" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="p-[12px] max-h-[400px] overflow-y-auto">
|
||||
<div class="content-span">
|
||||
<vue3-markdown-it :source="agreement" :html="true" :linkify="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<div class="p-[20px] flex justify-center items-center">
|
||||
<div
|
||||
class="w-[148px] h-[44px] bg-[#22a7ff] rounded-[50px] flex items-center justify-center text-white text-[16px] font-[500]"
|
||||
@click="handleClose">我知道了</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, defineProps, defineEmits, defineExpose } from "vue";
|
||||
import Vue3MarkdownIt from 'vue3-markdown-it';
|
||||
|
||||
// Props定义
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "温馨提示",
|
||||
},
|
||||
agreement: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Events定义
|
||||
const emits = defineEmits(["agree", "close", "cancel"]);
|
||||
|
||||
// 响应式数据
|
||||
const popup = ref(null);
|
||||
const show = ref(false)
|
||||
|
||||
// 方法定义
|
||||
const open = () => {
|
||||
show.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
show.value = false
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
close();
|
||||
emits("close");
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
@@ -1,149 +1,56 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col items-center justify-center">
|
||||
<!-- 卡通形象 -->
|
||||
<img class="w-[200px] h-[200px]" :src="logo" />
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<div class="w-[304px] mt-[80px]">
|
||||
<van-checkbox v-model="isAgree">
|
||||
<span class="text-[12px] text-ink-600">我已阅读并同意</span>
|
||||
<span class="text-[12px] text-[#2D91FF] mx-[4px]" @click.stop="handleAgreeClick('service')">《服务协议》</span>
|
||||
<span class="text-[12px] text-ink-600">和</span>
|
||||
<span class="text-[12px] text-[#2D91FF] mx-[4px]" @click.stop="handleAgreeClick('privacy')">《隐私协议》</span>
|
||||
<span class="text-[12px] text-ink-600 ml-[30px]">授权与账号关联操作</span>
|
||||
</van-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<div class="w-[304px] mt-[20px]">
|
||||
<button class="w-full h-[44px] bg-[#0ccd58] text-white rounded-[10px] text-center" @click="getPhoneNumber">
|
||||
手机号快捷登录
|
||||
<button class="w-full h-[44px] bg-[#0ccd58] text-white rounded-[10px] text-center" @click="loginWithGoogle">
|
||||
Continue with Google
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgreePopup ref="agreePopup" :agreement="computedAgreement" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { getServiceAgreement, getPrivacyAgreement } from "@/api/login";
|
||||
import { onLogin, goBack, onCheckPhoneLogin, onAppleLogin } from "@/hooks/useGoLogin";
|
||||
import AgreePopup from "./components/AgreePopup/index.vue";
|
||||
import { oauthToken } from '@/api/login'
|
||||
|
||||
// 是否需要微信手机号授权登录
|
||||
const needWxAuthLogin = ref(false);
|
||||
const isAgree = ref(false);
|
||||
const visible = ref(false);
|
||||
const serviceAgreement = ref("");
|
||||
const privacyAgreement = ref("");
|
||||
// 协议类型
|
||||
const AgreeType = ref("service");
|
||||
const logo = ref('');
|
||||
const isAppleLogin = ref(false);
|
||||
const generateState = () => {
|
||||
const array = new Uint32Array(4);
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
// 同意隐私协议并获取手机号
|
||||
const handleAgreeAndGetPhone = () => {
|
||||
// 如果需要微信登录,直接返回
|
||||
if (needWxAuthLogin.value && !isAppleLogin.value) {
|
||||
return Array.from(array, dec => dec.toString(16).padStart(8, '0')).join('');
|
||||
}
|
||||
|
||||
// 用Google登录
|
||||
const loginWithGoogle = () => {
|
||||
const client_id = '你的Google客户端ID';
|
||||
const redirect_uri = encodeURIComponent('http://10.10.3.1:5174/login');
|
||||
const scope = encodeURIComponent('openid email profile');
|
||||
const response_type = 'code'; // 推荐 code 模式(授权码模式,更安全);如果不需要后端参与,可填 'token' (隐式模式)
|
||||
const state = generateState();
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
|
||||
const authUrl = `https://google.com{client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 1. 从 URL 中获取谷歌传回的 state
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const returnedState = urlParams.get('state');
|
||||
|
||||
// 2. 从本地缓存取出之前存的 state
|
||||
const originalState = sessionStorage.getItem('oauth_state');
|
||||
|
||||
// 3. 严格比对
|
||||
if (!returnedState || returnedState !== originalState) {
|
||||
// 清除缓存并拒绝后续登录逻辑
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAgree.value) {
|
||||
showToast({
|
||||
title: "请先同意服务协议和隐私协议",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAppleLogin.value) {
|
||||
onAppleLogin()
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
loginSuccess();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast({
|
||||
title: "Apple ID登录失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
onCheckPhoneLogin()
|
||||
.then((res) => {
|
||||
if (!res) {
|
||||
loginSuccess();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast({
|
||||
title: "手机号登录失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// 获取授权后绑定手机号登录
|
||||
const getPhoneNumber = (e) => {
|
||||
if (!isAgree.value) {
|
||||
showToast({
|
||||
title: "请先同意服务协议和隐私协议",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
onLogin(e)
|
||||
.then(() => loginSuccess())
|
||||
.catch(() => {
|
||||
showToast({
|
||||
title: "获取登录手机号失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/// 登录成功返回上一页
|
||||
const loginSuccess = () => {
|
||||
showToast({
|
||||
title: "登录成功",
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
goBack();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 处理同意协议点击事件
|
||||
const handleAgreeClick = (type) => {
|
||||
visible.value = true;
|
||||
AgreeType.value = type;
|
||||
};
|
||||
|
||||
// 计算协议类型
|
||||
const computedAgreement = computed(() => {
|
||||
if (AgreeType.value === "service") {
|
||||
return serviceAgreement.value;
|
||||
} else {
|
||||
return privacyAgreement.value;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取服务协议数据
|
||||
const getServiceAgreementData = async () => {
|
||||
const { data } = await getServiceAgreement();
|
||||
serviceAgreement.value = data;
|
||||
};
|
||||
getServiceAgreementData();
|
||||
|
||||
// 获取隐私协议数据
|
||||
const getPrivacyAgreementData = async () => {
|
||||
const { data } = await getPrivacyAgreement();
|
||||
privacyAgreement.value = data;
|
||||
};
|
||||
getPrivacyAgreementData();
|
||||
|
||||
// 4. 比对成功,清除使用过的 state,继续执行后续拿 code 换 token 的业务
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
const code = urlParams.get('code');
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user