feat(auth): 对接原本项目的单点登录功能(后端回调有问题)

- 新增 SSO 登录相关代码和接口
-重构登录逻辑,支持单点登录
- 添加 token 管理和用户信息存储
- 优化环境变量配置
master
钟良源 5 months ago
parent dd1cdac9f1
commit 95017c0f91

@ -0,0 +1,5 @@
# API基础URL
NEXT_PUBLIC_API_BASE_URL=/
# 开发服务器主机地址
NEXT_PUBLIC_DEV_SERVER_HOST=https://p29.ngsk.tech:7001

@ -9,21 +9,23 @@ const withTM = require('next-transpile-modules')([
'@xyflow/system'
]);
const setting = require("./src/settings.json");
const setting = require('./src/settings.json');
const target = process.env.NEXT_PUBLIC_DEV_SERVER_HOST;
module.exports = withLess(
withTM({
lessLoaderOptions: {
lessOptions: {
modifyVars: {
'arcoblue-6': setting.themeColor,
},
},
'arcoblue-6': setting.themeColor
}
}
},
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
use: ['@svgr/webpack']
});
config.resolve.alias['@/assets'] = path.resolve(
@ -42,10 +44,23 @@ module.exports = withLess(
{
source: '/',
destination: '/dashboard/workplace',
permanent: true,
},
permanent: true
}
];
},
// 添加代理配置
async rewrites() {
return [
{
source: '/api/:path*',
destination: `${target}/api/:path*` // api代理
}
// {
// source: '/ws/:path*',
// destination: `${target.replace('http', 'ws')}/WS/:path*` // WebSocket 代理
// }
];
},
pageExtensions: ['tsx'],
pageExtensions: ['tsx']
})
);

@ -29,6 +29,7 @@
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.2",
"install": "^0.13.0",
"js-md5": "^0.8.3",
"lodash": "^4.17.21",
"mockjs": "^1.1.0",
"next": "12.0.4",

@ -62,6 +62,9 @@ importers:
install:
specifier: ^0.13.0
version: 0.13.0
js-md5:
specifier: ^0.8.3
version: 0.8.3
lodash:
specifier: ^4.17.21
version: 4.17.21
@ -3018,6 +3021,9 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
js-md5@0.8.3:
resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -8480,6 +8486,8 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
js-md5@0.8.3: {}
js-tokens@4.0.0: {}
js-yaml@3.14.1:

@ -0,0 +1,57 @@
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Message } from '@arco-design/web-react';
import { getToken } from '@/utils/auth';
import useUser from '@/hooks/user';
export interface HttpResponse<T = unknown> {
status: number;
msg: string;
code: number;
data: T;
}
if (process.env.NEXT_API_BASE_URL) {
axios.defaults.baseURL = process.env.NEXT_API_BASE_URL;
}
axios.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = getToken();
if (token) {
if (!config.headers) {
config.headers = {};
}
// config.headers.Authorization = `Bearer ${token}`
}
return config;
},
error => {
return Promise.reject(error);
}
);
// add response interceptors
axios.interceptors.response.use(
(response: AxiosResponse<HttpResponse>) => {
const res = response.data;
if (res.code && res.code !== 200) {
Message.error({
content: res.msg || 'Error',
duration: 5 * 1000
});
if ([-401].includes(res.code)) {
const { logout } = useUser();
logout();
}
return Promise.reject(new Error(res.msg || 'Error'));
}
return res;
},
error => {
Message.error({
content: error.msg || '系统异常',
duration: 5 * 1000
});
return Promise.reject(error);
}
);

@ -0,0 +1,225 @@
// application
export interface CronModel {
appId: string;
cron: string;
}
export interface applicationModel {
id?: string;
name?: string;
description?: string;
identify?: string;
logo?: string;
path?: string;
published?: boolean | number;
sceneId?: string;
createBy?: string;
createTime?: string | number;
updateBy?: string;
updateTime?: string | number;
compNum?: number;
instanceNum?: number;
nodeNum?: number;
cron?: string;
scheduled?: number;
createUser?: string;
instanceId?: string | number;
}
export interface queryParams {
sceneId?: string;
resId?: string;
name?: string;
currPage?: number;
pageSize?: number;
total?: number;
}
export interface apiResData {
list: applicationModel[];
}
export interface FlowDefinition {
id: string;
lineConfigs: any[];
nodeConfigs: any[];
name?: string;
}
export interface appFlowModel {
id?: string;
name?: string;
compIds?: string[];
description?: string;
nodeNum?: number;
main?: FlowDefinition;
subs?: any;
}
export interface publishType {
appId?: string;
flowId?: string;
isMain?: number;
name?: string;
published?: number | string;
tag?: string;
description?: string;
}
export interface publishApi {
appId: string;
needAuthor: number;
time?: number;
}
export interface paramsT {
code: number;
msg: string;
data?: any;
traceId: string | null;
}
export interface flowType {
id: string;
type?: string;
}
// aichat
export interface RecommendResponse {
code: number;
compent_list: string[];
}
// dashboard
export interface ContentDataRecord {
x: string;
y: number;
}
export interface PopularRecord {
key: number;
clickNumber: string;
title: string;
increases: number;
}
// scene
export interface monitorCfg {
label: string;
url: string;
}
export interface sceneModel {
id?: string;
name?: string;
description?: string;
identify?: string;
logo?: string;
published?: boolean | number;
createBy?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
createUser?: string;
configUrls?: Array<monitorCfg>;
}
export interface querySceneParams {
currPage?: number;
name?: string;
pageSize?: number;
}
export interface sceneApiResData {
list: sceneModel[];
totalCount: number;
}
// user
export interface LoginRes {
token: string;
[key: string]: any;
}
export interface authParams {
domain: string;
path: string;
}
export interface getTokenParams extends authParams {
authCode: string;
callbackUrl: string;
}
export interface getLinkParams {
callbackUrl: string;
}
export interface UserState {
account: string;
avatar?: string;
realName: string;
userId: string;
username: string;
}
// message
export interface MessageRecord {
id: number;
type: string;
title: string;
subTitle: string;
avatar?: string;
content: string;
time: string;
status: 0 | 1;
messageType?: number;
}
export type MessageListType = MessageRecord[];
export interface MessageStatus {
ids: number[];
}
export interface ChatRecord {
id: number;
username: string;
content: string;
time: string;
isCollect: boolean;
}
// event
export interface AddEventParams {
name: string,
topic: string,
description: string,
sceneId: string,
}
export interface DeleteEventParams {
id: string;
}
export interface DeleteEventForAppParams {
appId: string,
eventIds: string[]
}
// runtime
export interface StepRunEventParams {
eventId: string;
}
export interface ExecuteCurrentEventParams {
appId: string,
nodeId: string,
socketId: string
}
export interface ReconnectRunParams {
instanceId: string,
newSocketId: string
}

@ -0,0 +1,51 @@
import { md5 } from 'js-md5';
import { ssoHost } from '@/utils/env';
import { LoginRes } from '@/api/interface';
export function post(url: string, params: Record<string, any>) {
const temp = document.createElement('form');
temp.action = url;
temp.method = 'post';
temp.style.display = 'none';
Object.keys(params).forEach(key => {
const opt = document.createElement('textarea');
opt.name = key;
opt.value = params[key];
temp.appendChild(opt);
});
document.body.appendChild(temp);
temp.submit();
return temp;
}
export const ssoLogin = ({ username, password }: Record<string, any>) => {
// 模拟表单提交
const ssoUri = `${ssoHost}/api/blade-auth/oauth/form`;
return post(ssoUri, {
tenant_id: '000000',
username,
password: md5(password),
client_id: 'isdp2',
// 统一认证的地址
sso_uri: `${ssoHost || window.location.origin}/api/blade-auth`,
// 必须加/
redirect_uri: `${window.location.origin}/dashboard/workplace`
});
};
export const verify = ({ username, password }): Promise<LoginRes> => {
return fetch('/api/user-center/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password: md5(password) })
}).then(async (res) => {
const data = await res.json();
// 登录成功后设置用户状态
if (data && res.status === 200) {
localStorage.setItem('userStatus', 'login');
}
return data;
});
};

@ -0,0 +1,45 @@
import axios from 'axios';
import {
getTokenParams,
getLinkParams,
UserState,
LoginRes,
authParams
} from '@/api/interface';
// 修复window is not defined错误确保只在浏览器环境中访问window对象
const defaultReqData: authParams = typeof window !== 'undefined' ? {
domain: window.location.hostname,
path: '/'
} : {
domain: 'localhost',
path: '/'
};
const urlPrefix = '/api/v1/bpms-workbench';
export function getToken(params: getTokenParams) {
return axios.get<LoginRes>(`${urlPrefix}/sessions/callback`, {
params: { ...params, ...defaultReqData }
});
}
export function getAuthLink(params: getLinkParams) {
return axios.get<string>(`${urlPrefix}/sessions/auth/url`, {
params
});
}
export function getUserInfo() {
return axios.get<UserState>(`${urlPrefix}/sessions/info`);
}
export function logout() {
return axios.get<LoginRes>(`${urlPrefix}/sessions/logout`, {
params: defaultReqData
});
}
export function getUserToken() {
return axios.get<LoginRes>(`${urlPrefix}/sessions/validate`, {});
}

@ -0,0 +1,44 @@
import { Message } from '@arco-design/web-react';
import { useDispatch } from 'react-redux';
import axios from 'axios';
interface UseUserReturnType {
logout: () => Promise<void>;
}
export default function useUser(): UseUserReturnType {
const dispatch = useDispatch();
const logout = async () => {
try {
// 调用后端登出接口
await axios.post('/api/user/logout');
} catch (error) {
// 即使后端登出失败,也要清除本地状态
console.error('Logout error:', error);
} finally {
// 清除用户信息状态
dispatch({
type: 'update-userInfo',
payload: {
userInfo: { permissions: {} },
userLoading: false
}
});
// 清除本地存储的用户状态
localStorage.removeItem('userStatus');
// 显示成功消息
Message.success('退出成功');
// 重定向到登录页面
const { protocol, host } = window.location;
window.location.href = `${protocol}//${host}/login`;
}
};
return {
logout
};
}

@ -9,7 +9,7 @@ import {
import { FormInstance } from '@arco-design/web-react/es/Form';
import { IconLock, IconUser } from '@arco-design/web-react/icon';
import React, { useEffect, useRef, useState } from 'react';
import axios from 'axios';
import { ssoLogin, verify } from '@/api/sso';
import useStorage from '@/utils/useStorage';
import useLocale from '@/utils/useLocale';
import locale from './locale';
@ -26,7 +26,7 @@ export default function LoginForm() {
const [rememberPassword, setRememberPassword] = useState(!!loginParams);
function afterLoginSuccess(params) {
function afterLoginSuccess(params, token?: string) {
// 记住密码
if (rememberPassword) {
setLoginParams(JSON.stringify(params));
@ -37,31 +37,29 @@ export default function LoginForm() {
// 记录登录状态
localStorage.setItem('userStatus', 'login');
// 跳转首页
window.location.href = '/dashboard/workplace';
// window.location.href = '/dashboard/workplace';
}
function login(params) {
async function loginRequest(params) {
setErrorMessage('');
setLoading(true);
axios
.post('/api/user/login', params)
.then((res) => {
const { status, msg } = res.data;
if (status === 'ok') {
afterLoginSuccess(params);
}
else {
setErrorMessage(msg || t['login.form.login.errMsg']);
}
})
.finally(() => {
setLoading(false);
});
try {
const res = await verify(params as any);
if (res.code === 200) {
await ssoLogin(params);
afterLoginSuccess(params);
}
else {
setErrorMessage(res.msg);
}
} finally {
setLoading(false);
}
}
function onSubmitClick() {
formRef.current.validate().then((values) => {
login(values);
loginRequest(values);
});
}
@ -86,10 +84,10 @@ export default function LoginForm() {
className={styles['login-form']}
layout="vertical"
ref={formRef}
initialValues={{ userName: 'admin', password: 'admin' }}
initialValues={{ username: 'admin', password: 'admin' }}
>
<Form.Item
field="userName"
field="username"
rules={[{ required: true, message: t['login.form.userName.errMsg'] }]}
>
<Input

@ -1,14 +1,64 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Footer from '@/components/Footer';
import Logo from '@/assets/logo.svg';
import LoginForm from './form';
import LoginBanner from './banner';
import styles from './style/index.module.less';
import { getToken } from '@/api/user';
import { setToken } from '@/utils/auth';
import { localGet, localRemove, openWindow } from '@/utils/common';
function Login() {
const router = useRouter();
const [isNeedLogin, setIsNeedLogin] = useState(false);
const handleLogin = async () => {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const { origin } = window.location;
setIsNeedLogin(code === null);
console.log('code:', code);
if (code) {
const callbackUrl = `${origin}`;
try {
// 根据code向后端获取token
const res: any = await getToken({
authCode: code,
callbackUrl
} as any);
if (res && res.code === 200) {
// 保存Token
setToken(res.data as string);
// 保存用户信息
// await userStore.info(); // 如果有对应的Redux操作可以在这里dispatch
if (localGet('system_name') === 'pc') {
openWindow(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/isdp/`, {
target: '_self'
});
localRemove('system_name');
return;
}
else {
// 使用Next.js标准路由跳转方式
router.push('/dashboard/workplace');
}
}
} catch (error) {
console.error('Login error:', error);
}
}
};
useEffect(() => {
document.body.setAttribute('arco-theme', 'light');
}, []);
// 模拟Vue的onMounted行为在组件挂载后执行
handleLogin();
}, [router]); // 依赖数组包含router确保router变化时能正确执行
return (
<div className={styles.container}>
@ -32,6 +82,7 @@ function Login() {
</div>
);
}
Login.displayName = 'LoginPage';
export default Login;

@ -0,0 +1,37 @@
const TOKEN_KEY = 'token';
const ISDP_KEY = 'saber-token';
const isLogin = () => {
return !!localStorage.getItem(TOKEN_KEY);
};
const getToken = () => {
return localStorage.getItem(TOKEN_KEY);
};
const setToken = (token: string) => {
localStorage.setItem(TOKEN_KEY, token);
const obj = {
dataType: typeof token,
content: token,
datetime: new Date().getTime()
};
localStorage.setItem(ISDP_KEY, JSON.stringify(obj));
document.cookie = `saber-access-token=${token};`;
};
const clearToken = () => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(ISDP_KEY);
};
const USER_INFO_KEY = 'userInfo';
export const setSessionUserInfo = data => {
sessionStorage.setItem(USER_INFO_KEY, JSON.stringify(data));
};
export const getSessionUserInfo = () => {
const data = sessionStorage.getItem(USER_INFO_KEY);
return data ? JSON.parse(data) : null;
};
export { isLogin, getToken, setToken, clearToken };

@ -1,3 +1,32 @@
import { isLogin, getToken } from '@/utils/auth';
export default function checkLogin() {
return localStorage.getItem('userStatus') === 'login';
// 检查基础登录状态
const userStatus = localStorage.getItem('userStatus');
if (!userStatus || userStatus !== 'login') {
return false;
}
// 检查token是否存在
if (!isLogin()) {
return false;
}
// 检查token是否过期如果有过期时间的话
const token = getToken();
if (token) {
try {
// 如果token是JWT可以解析并检查过期时间
// 这里只是一个示例实际实现取决于token格式
// const payload = JSON.parse(atob(token.split('.')[1]));
// if (payload.exp && Date.now() >= payload.exp * 1000) {
// return false; // token已过期
// }
} catch (e) {
// 如果token不是JWT格式忽略解析错误
console.warn('Failed to parse token', e);
}
}
return true;
}

@ -23,6 +23,47 @@ export function openWindow(url: string, opts?: OpenWindowOptions) {
window.open(fullUrl, target);
}
/**
* @description localStorage
* @param {String} key Storage
* @returns {String}
*/
export function localGet(key: string) {
const value = window.localStorage.getItem(key);
try {
return JSON.parse(window.localStorage.getItem(key) as string);
} catch (error) {
return value;
}
}
/**
* @description localStorage
* @param {String} key Storage
* @param {*} value Storage
* @returns {void}
*/
export function localSet(key: string, value: any) {
window.localStorage.setItem(key, JSON.stringify(value));
}
/**
* @description localStorage
* @param {String} key Storage
* @returns {void}
*/
export function localRemove(key: string) {
window.localStorage.removeItem(key);
}
/**
* @description localStorage
* @returns {void}
*/
export function localClear() {
window.localStorage.clear();
}
/**
* URL
* @param url - URL

@ -0,0 +1,5 @@
const debug = process.env.NODE_ENV !== 'production';
// 使用NEXT_PUBLIC_前缀的环境变量才能在客户端访问
export const ssoHost = debug ? process.env.NEXT_PUBLIC_DEV_SERVER_HOST : '';
export default debug;
Loading…
Cancel
Save