Browse Source

feat: oauth2.0

zw-oauth
cp3hnu 1 year ago
parent
commit
bb48515532
12 changed files with 277 additions and 137 deletions
  1. +10
    -0
      react-ui/config/routes.ts
  2. +11
    -8
      react-ui/src/app.tsx
  3. +3
    -0
      react-ui/src/components/IFramePage/index.tsx
  4. +7
    -0
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  5. +0
    -0
      react-ui/src/pages/Authorize/index.less
  6. +50
    -0
      react-ui/src/pages/Authorize/index.tsx
  7. +7
    -0
      react-ui/src/pages/GitLink/index.tsx
  8. +147
    -128
      react-ui/src/pages/User/Login/index.tsx
  9. +16
    -0
      react-ui/src/services/auth/index.js
  10. +12
    -0
      react-ui/src/types.ts
  11. +2
    -0
      react-ui/src/utils/sessionStorage.ts
  12. +12
    -1
      react-ui/src/utils/ui.tsx

+ 10
- 0
react-ui/config/routes.ts View File

@@ -27,6 +27,16 @@ export default [
},
],
},
{
path: '/authorize',
layout: false,
component: './Authorize/index',
},
{
path: '/gitlink',
layout: true,
component: './GitLink/index',
},
{
path: '/user',
layout: false,


+ 11
- 8
react-ui/src/app.tsx View File

@@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css';
import { getAccessToken } from './access';
import './dayjsConfig';
import { PageEnum } from './enums/pagesEnums';
import './global.less';
import { removeAllPageCacheState } from './hooks/pageCacheState';
import {
@@ -23,6 +22,7 @@ export { requestConfig as request } from './requestConfig';
import { type GlobalInitialState } from '@/types';
import { menuItemRender } from '@/utils/menuRender';
import ErrorBoundary from './components/ErrorBoundary';
import { needAuth } from './utils';
import { gotoLoginPage } from './utils/ui';

/**
@@ -40,14 +40,17 @@ export async function getInitialState(): Promise<GlobalInitialState> {
roleNames: response.user.roles,
} as API.CurrentUser;
} catch (error) {
console.error(error);
console.error('1111', error);
gotoLoginPage();
}
return undefined;
};

// 如果不是登录页面,执行
const { location } = history;
if (location.pathname !== PageEnum.LOGIN) {

console.log('getInitialState', needAuth(location.pathname));
if (needAuth(location.pathname)) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
@@ -94,7 +97,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) {
if (!initialState?.currentUser && needAuth(location.pathname)) {
gotoLoginPage();
}
},
@@ -159,8 +162,8 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => {
const { location } = e;
const menus = getRemoteMenu();
// console.log('onRouteChange', e);
if (menus === null && location.pathname !== PageEnum.LOGIN) {
console.log('onRouteChange', menus);
if (menus === null && needAuth(location.pathname)) {
history.go(0);
}
};
@@ -170,12 +173,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => {
};

export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => {
//console.log('patchClientRoutes', e);
console.log('patchClientRoutes', e);
patchRouteWithRemoteMenus(e.routes);
};

export function render(oldRender: () => void) {
// console.log('render');
console.log('render');
const token = getAccessToken();
if (!token || token?.length === 0) {
oldRender();


+ 3
- 0
react-ui/src/components/IFramePage/index.tsx View File

@@ -12,6 +12,7 @@ export enum IframePageType {
DatasetAnnotation = 'DatasetAnnotation', // 数据标注
AppDevelopment = 'AppDevelopment', // 应用开发
DevEnv = 'DevEnv', // 开发环境
GitLink = 'GitLink',
}

const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
@@ -26,6 +27,8 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
code: 200,
data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '',
});
case IframePageType.GitLink:
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' });
}
};



+ 7
- 0
react-ui/src/components/RightContent/AvatarDropdown.tsx View File

@@ -1,6 +1,8 @@
import { clearSessionToken } from '@/access';
import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types';
import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components';
@@ -64,6 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage();
const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
if (clientInfo) {
const { logoutUri } = clientInfo;
location.replace(logoutUri);
}
};
const actionClassName = useEmotionCss(({ token }) => {
return {


+ 0
- 0
react-ui/src/pages/Authorize/index.less View File


+ 50
- 0
react-ui/src/pages/Authorize/index.tsx View File

@@ -0,0 +1,50 @@
import { setSessionToken } from '@/access';
import { loginByOauth2Req } from '@/services/auth';
import { to } from '@/utils/promise';
import { history, useModel, useSearchParams } from '@umijs/max';
import { message } from 'antd';
import { useEffect } from 'react';
import { flushSync } from 'react-dom';
import styles from './index.less';

function Authorize() {
const { initialState, setInitialState } = useModel('@@initialState');
const [searchParams] = useSearchParams();
const code = searchParams.get('code');
const redirect = searchParams.get('redirect');
useEffect(() => {
loginByOauth2();
}, []);

// 登录
const loginByOauth2 = async () => {
const params = {
code,
};
const [res] = await to(loginByOauth2Req(params));
debugger;
if (res && res.data) {
const { access_token, expires_in } = res.data;
setSessionToken(access_token, access_token, expires_in);
message.success('登录成功!');
await fetchUserInfo();
history.push(redirect || '/');
}
};

const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};

return <div className={styles.container}></div>;
}

export default Authorize;

+ 7
- 0
react-ui/src/pages/GitLink/index.tsx View File

@@ -0,0 +1,7 @@
import IframePage, { IframePageType } from '@/components/IFramePage';

function GitLink() {
return <IframePage type={IframePageType.GitLink}></IframePage>;
}

export default GitLink;

+ 147
- 128
react-ui/src/pages/User/Login/index.tsx View File

@@ -1,11 +1,12 @@
import { clearSessionToken, setSessionToken } from '@/access';
import { getClientInfoReq } from '@/services/auth';
import { getCaptchaImg, login } from '@/services/system/auth';
import { parseJsonText } from '@/utils';
import { safeInvoke } from '@/utils/functional';
import LocalStorage from '@/utils/localStorage';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { gotoOAuth2 } from '@/utils/ui';
import { history, useModel } from '@umijs/max';
import { Button, Checkbox, Flex, Form, Image, Input, message, type InputRef } from 'antd';
import { Form, message, type InputRef } from 'antd';
import CryptoJS from 'crypto-js';
import { useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
@@ -31,25 +32,35 @@ const Login = () => {
const captchaInputRef = useRef<InputRef>(null);

useEffect(() => {
getCaptchaCode();
const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false';
if (autoLogin === 'true') {
const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey);
const userJson = safeInvoke((text: string) =>
CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8),
)(userStorage);
const user = safeInvoke(parseJsonText)(userJson);
if (user && typeof user === 'object' && user.version === VERSION) {
const { username, password } = user;
form.setFieldsValue({ username: username, password: password, autoLogin: true });
} else {
form.setFieldsValue({ username: '', password: '', autoLogin: true });
LocalStorage.removeItem(LocalStorage.loginUserKey);
}
} else {
form.setFieldsValue({ username: '', password: '', autoLogin: false });
}
// getCaptchaCode();
// const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false';
// if (autoLogin === 'true') {
// const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey);
// const userJson = safeInvoke((text: string) =>
// CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8),
// )(userStorage);
// const user = safeInvoke(parseJsonText)(userJson);
// if (user && typeof user === 'object' && user.version === VERSION) {
// const { username, password } = user;
// form.setFieldsValue({ username: username, password: password, autoLogin: true });
// } else {
// form.setFieldsValue({ username: '', password: '', autoLogin: true });
// LocalStorage.removeItem(LocalStorage.loginUserKey);
// }
// } else {
// form.setFieldsValue({ username: '', password: '', autoLogin: false });
// }
getClientInfo();
}, []);
const getClientInfo = async () => {
const [res] = await to(getClientInfoReq());
if (res && res.data) {
const clientInfo = res.data;
SessionStorage.setItem(SessionStorage.clientInfoKey, clientInfo, true);
gotoOAuth2();
}
};

const getCaptchaCode = async () => {
const [res] = await to(getCaptchaImg());
if (res) {
@@ -71,6 +82,12 @@ const Login = () => {
}
};

const handleSubmit2 = async (values: API.LoginParams) => {
const url =
'http://172.20.32.106:8080/oauth/authorize?client_id=ci4s&response_type=code&grant_type=authorization_code';
window.location.href = url;
};

// 登录
const handleSubmit = async (values: API.LoginParams) => {
const [res, error] = await to(login({ ...values, uuid }));
@@ -109,113 +126,115 @@ const Login = () => {
}
};

return (
<div className={styles['user-login']}>
<div className={styles['user-login__left']}>
<div className={styles['user-login__left__top']}>
<img
src={require('@/assets/img/logo.png')}
style={{ width: '32px', marginRight: '12px' }}
draggable={false}
alt=""
/>
智能材料科研平台
</div>
<div className={styles['user-login__left__title']}>
<span>智能材料科研平台</span>
<img
src={require('@/assets/img/login-ai-logo.png')}
className={styles['user-login__left__title__img']}
draggable={false}
alt=""
/>
</div>
<div className={styles['user-login__left__message']}>
<span>大语言模型运维 统一管理平台</span>
</div>
<img
className={styles['user-login__left__bottom-img']}
src={require('@/assets/img/login-left-image.png')}
draggable={false}
alt=""
/>
</div>
<div className={styles['user-login__right']}>
<div>
<div className={styles['user-login__right__title']}>
<span style={{ color: '#111111' }}>欢迎登录</span>
<span>智能材料科研平台</span>
</div>
<div className={styles['user-login__right__content']}>
<div className={styles['user-login__right__content__title']}>账号登录</div>
<div className={styles['user-login__right__content__form']}>
<Form
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
initialValues={{ autoLogin: true }}
onFinish={handleSubmit}
autoComplete="off"
form={form}
>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input
placeholder="请输入用户名"
prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />}
allowClear
/>
</Form.Item>

<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password
placeholder="请输入密码"
prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />}
allowClear
/>
</Form.Item>

<Flex align="start" style={{ height: '98px' }}>
<div style={{ flex: 1 }}>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
<Input
placeholder="请输入验证码"
prefix={
<LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} />
}
ref={captchaInputRef}
allowClear
/>
</Form.Item>
</div>
<Image
className={styles['user-login__right__content__form__captcha']}
src={captchaCode}
alt="验证码"
preview={false}
onClick={() => getCaptchaCode()}
/>
</Flex>

<Form.Item
name="autoLogin"
valuePropName="checked"
labelCol={{ span: 0 }}
wrapperCol={{ span: 16 }}
>
<Checkbox>记住密码</Checkbox>
</Form.Item>

<Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}>
<Button type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
</div>
</div>
);
return <div className={styles['user-login']}></div>;

// return (
// <div className={styles['user-login']}>
// <div className={styles['user-login__left']}>
// <div className={styles['user-login__left__top']}>
// <img
// src={require('@/assets/img/logo.png')}
// style={{ width: '32px', marginRight: '12px' }}
// draggable={false}
// alt=""
// />
// 智能材料科研平台
// </div>
// <div className={styles['user-login__left__title']}>
// <span>智能材料科研平台</span>
// <img
// src={require('@/assets/img/login-ai-logo.png')}
// className={styles['user-login__left__title__img']}
// draggable={false}
// alt=""
// />
// </div>
// <div className={styles['user-login__left__message']}>
// <span>大语言模型运维 统一管理平台</span>
// </div>
// <img
// className={styles['user-login__left__bottom-img']}
// src={require('@/assets/img/login-left-image.png')}
// draggable={false}
// alt=""
// />
// </div>
// <div className={styles['user-login__right']}>
// <div>
// <div className={styles['user-login__right__title']}>
// <span style={{ color: '#111111' }}>欢迎登录</span>
// <span>智能材料科研平台</span>
// </div>
// <div className={styles['user-login__right__content']}>
// <div className={styles['user-login__right__content__title']}>账号登录</div>
// <div className={styles['user-login__right__content__form']}>
// <Form
// labelCol={{ span: 0 }}
// wrapperCol={{ span: 24 }}
// initialValues={{ autoLogin: true }}
// onFinish={handleSubmit2}
// autoComplete="off"
// form={form}
// >
// <Form.Item name="username" rules={[{ required: false, message: '请输入用户名' }]}>
// <Input
// placeholder="请输入用户名"
// prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />}
// allowClear
// />
// </Form.Item>

// <Form.Item name="password" rules={[{ required: false, message: '请输入密码' }]}>
// <Input.Password
// placeholder="请输入密码"
// prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />}
// allowClear
// />
// </Form.Item>

// <Flex align="start" style={{ height: '98px' }}>
// <div style={{ flex: 1 }}>
// <Form.Item name="code" rules={[{ required: false, message: '请输入验证码' }]}>
// <Input
// placeholder="请输入验证码"
// prefix={
// <LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} />
// }
// ref={captchaInputRef}
// allowClear
// />
// </Form.Item>
// </div>
// <Image
// className={styles['user-login__right__content__form__captcha']}
// src={captchaCode}
// alt="验证码"
// preview={false}
// onClick={() => getCaptchaCode()}
// />
// </Flex>

// <Form.Item
// name="autoLogin"
// valuePropName="checked"
// labelCol={{ span: 0 }}
// wrapperCol={{ span: 16 }}
// >
// <Checkbox>记住密码</Checkbox>
// </Form.Item>

// <Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}>
// <Button type="primary" htmlType="submit">
// 登录
// </Button>
// </Form.Item>
// </Form>
// </div>
// </div>
// </div>
// </div>
// </div>
// );
};

export default Login;

+ 16
- 0
react-ui/src/services/auth/index.js View File

@@ -0,0 +1,16 @@
import { request } from '@umijs/max';

// 单点登录
export function loginByOauth2Req(data) {
return request(`/api/auth/loginByOauth2`, {
method: 'POST',
data,
});
}

// 登录获取客户端信息
export function getClientInfoReq() {
return request(`/api/auth/oauth2ClientInfo`, {
method: 'GET',
});
}

+ 12
- 0
react-ui/src/types.ts View File

@@ -7,6 +7,17 @@
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';

export type ClientInfo = {
accessTokenUri: string;
checkTokenUri: string;
clientId: string;
clientSecret: string;
loginPage: string;
logoutUri: string;
redirectUri: string;
userAuthorizationUri: string;
};

// 全局初始状态类型
export type GlobalInitialState = {
settings?: Partial<LayoutSettings>;
@@ -14,6 +25,7 @@ export type GlobalInitialState = {
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
loading?: boolean;
collapsed?: boolean;
clientInfo?: ClientInfo;
};

// 流水线全局参数


+ 2
- 0
react-ui/src/utils/sessionStorage.ts View File

@@ -9,6 +9,8 @@ export default class SessionStorage {
static readonly serviceVersionInfoKey = 'service-version-info';
// 编辑器 url
static readonly editorUrlKey = 'editor-url';
// 客户端信息
static readonly clientInfoKey = 'client-info';

static getItem(key: string, isObject: boolean = false) {
const jsonStr = sessionStorage.getItem(key);


+ 12
- 1
react-ui/src/utils/ui.tsx View File

@@ -6,9 +6,11 @@
import { PageEnum } from '@/enums/pagesEnums';
import { removeAllPageCacheState } from '@/hooks/pageCacheState';
import themes from '@/styles/theme.less';
import { type ClientInfo } from '@/types';
import { history } from '@umijs/max';
import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd';
import { closeAllModals } from './modal';
import SessionStorage from './sessionStorage';

type ModalConfirmProps = ModalFuncProps & {
isDelete?: boolean;
@@ -75,7 +77,7 @@ export const gotoLoginPage = (toHome: boolean = true) => {
const { pathname, search } = location;
const urlParams = new URLSearchParams();
urlParams.append('redirect', pathname + search);
const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString();
const newSearch = toHome || pathname === '/' ? '' : urlParams.toString();
// console.log('pathname', pathname);
// console.log('search', search);
if (pathname !== PageEnum.LOGIN) {
@@ -88,6 +90,15 @@ export const gotoLoginPage = (toHome: boolean = true) => {
}
};

export const gotoOAuth2 = () => {
const clientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true) as ClientInfo;
if (clientInfo) {
const { clientId, userAuthorizationUri } = clientInfo;
const url = `${userAuthorizationUri}?client_id=${clientId}&response_type=code&grant_type=authorization_code`;
location.replace(url);
}
};

/**
* 验证文件上传
*


Loading…
Cancel
Save