mirror of https://gitee.com/topiam/eiam
⚡ 优化代码
parent
9727f14278
commit
8a97cd7355
|
@ -28,6 +28,7 @@ import cn.topiam.employee.common.enums.app.FormSubmitType;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
/**
|
||||
* Form 协议配置
|
||||
|
@ -38,6 +39,7 @@ import lombok.experimental.SuperBuilder;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
public class FormProtocolConfig extends AbstractProtocolConfig {
|
||||
|
||||
@Serial
|
||||
|
|
|
@ -26,6 +26,7 @@ import cn.topiam.employee.common.enums.app.JwtIdTokenSubjectType;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
/**
|
||||
* Form 协议配置
|
||||
|
@ -36,6 +37,7 @@ import lombok.experimental.SuperBuilder;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
public class JwtProtocolConfig extends AbstractProtocolConfig {
|
||||
|
||||
@Serial
|
||||
|
|
|
@ -28,6 +28,7 @@ import cn.topiam.employee.application.AbstractProtocolConfig;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
/**
|
||||
* Oidc 协议配置
|
||||
|
@ -38,6 +39,7 @@ import lombok.experimental.SuperBuilder;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
public class OidcProtocolConfig extends AbstractProtocolConfig {
|
||||
|
||||
@Serial
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SortOrder } from 'antd/es/table/interface';
|
||||
import { ReactText } from 'react';
|
||||
import { RequestData } from '@ant-design/pro-components';
|
||||
import { request } from '@@/exports';
|
||||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
|
@ -92,7 +91,7 @@ export async function identitySourceConfigValidator(data: {
|
|||
export async function getIdentitySourceSyncHistoryList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListIdentitySourceSyncHistory>> {
|
||||
return request(`/api/v1/identity_source/sync/history_list`, {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -112,7 +111,7 @@ export async function getIdentitySourceSyncHistoryList(
|
|||
export async function getIdentitySourceSyncRecordList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListIdentitySourceSyncRecord>> {
|
||||
return request(`/api/v1/identity_source/sync/record_list`, {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -132,7 +131,7 @@ export async function getIdentitySourceSyncRecordList(
|
|||
export async function getIdentitySourceEventRecordList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListIdentitySourceEventRecord>> {
|
||||
return request(`/api/v1/identity_source/event/record_list`, {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SortOrder } from 'antd/es/table/interface';
|
||||
import { ReactText } from 'react';
|
||||
import { RequestData } from '@ant-design/pro-components';
|
||||
import { request } from '@@/exports';
|
||||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
|
@ -27,7 +26,7 @@ import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
|||
export async function getIdentityProviderList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListIdentitySource>> {
|
||||
return request<API.ApiResult<AccountAPI.ListIdentitySource>>('/api/v1/identity_source/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
|
|
@ -21,7 +21,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
|
|||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
|
||||
import { Badge, App, Popconfirm, Table } from 'antd';
|
||||
import { App, Badge, Popconfirm, Table } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Avatar, Image, App, Popconfirm, Skeleton } from 'antd';
|
||||
import { App, Avatar, Image, Popconfirm, Skeleton } from 'antd';
|
||||
import {
|
||||
ActionType,
|
||||
ProCard,
|
||||
|
|
|
@ -15,31 +15,31 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {getUserGroup, updateUserGroup} from '@/services/account';
|
||||
import {history} from '@@/core/history';
|
||||
import { getUserGroup, updateUserGroup } from '@/services/account';
|
||||
import { history } from '@@/core/history';
|
||||
|
||||
import {PageContainer, ProDescriptions, RouteContext} from '@ant-design/pro-components';
|
||||
import {useAsyncEffect, useMount} from 'ahooks';
|
||||
import {App, Skeleton} from 'antd';
|
||||
import {useState} from 'react';
|
||||
import { PageContainer, ProDescriptions, RouteContext } from '@ant-design/pro-components';
|
||||
import { useAsyncEffect, useMount } from 'ahooks';
|
||||
import { App, Skeleton } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import MemberList from './components/MemberList';
|
||||
import {UserGroupDetailTabs} from './constant';
|
||||
import { UserGroupDetailTabs } from './constant';
|
||||
import queryString from 'query-string';
|
||||
import {useIntl, useLocation} from '@umijs/max';
|
||||
import useStyles from "./style";
|
||||
import AccessStrategy from "@/pages/account/UserGroupDetail/components/AccessStrategy";
|
||||
import { useIntl, useLocation } from '@umijs/max';
|
||||
import useStyles from './style';
|
||||
import AccessStrategy from '@/pages/account/UserGroupDetail/components/AccessStrategy';
|
||||
|
||||
/**
|
||||
* 用户组详情
|
||||
*/
|
||||
export default () => {
|
||||
const intl = useIntl();
|
||||
const {styles} = useStyles();
|
||||
const {message} = App.useApp();
|
||||
const { styles } = useStyles();
|
||||
const { message } = App.useApp();
|
||||
const location = useLocation();
|
||||
const query = queryString.parse(location.search);
|
||||
const {id} = query as { id: string };
|
||||
const {type} = query as {
|
||||
const { id } = query as { id: string };
|
||||
const { type } = query as {
|
||||
type: UserGroupDetailTabs;
|
||||
};
|
||||
|
||||
|
@ -50,7 +50,7 @@ export default () => {
|
|||
useMount(() => {
|
||||
if (!id) {
|
||||
message
|
||||
.warning(intl.formatMessage({id: 'pages.account.user_group_detail.use_mount.message'}))
|
||||
.warning(intl.formatMessage({ id: 'pages.account.user_group_detail.use_mount.message' }))
|
||||
.then();
|
||||
history.push(`/account/user-group`);
|
||||
return;
|
||||
|
@ -59,7 +59,7 @@ export default () => {
|
|||
setTabActiveKey(UserGroupDetailTabs.member);
|
||||
history.push({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify({type: UserGroupDetailTabs.member, id: id}),
|
||||
search: queryString.stringify({ type: UserGroupDetailTabs.member, id: id }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ export default () => {
|
|||
useAsyncEffect(async () => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
const {success, result} = await getUserGroup(id);
|
||||
const { success, result } = await getUserGroup(id);
|
||||
if (success) {
|
||||
setDetail(result);
|
||||
setLoading(false);
|
||||
|
@ -80,9 +80,9 @@ export default () => {
|
|||
|
||||
const description = (
|
||||
<RouteContext.Consumer>
|
||||
{({isMobile}) =>
|
||||
{({ isMobile }) =>
|
||||
loading ? (
|
||||
<Skeleton active paragraph={{rows: 1}}/>
|
||||
<Skeleton active paragraph={{ rows: 1 }} />
|
||||
) : (
|
||||
<ProDescriptions<Record<string, string>>
|
||||
size="small"
|
||||
|
@ -91,17 +91,17 @@ export default () => {
|
|||
editable={{
|
||||
onSave: async (key, record) => {
|
||||
let success: boolean;
|
||||
const result = await updateUserGroup({...record});
|
||||
const result = await updateUserGroup({ ...record });
|
||||
success = result.success;
|
||||
if (success) {
|
||||
message.success(intl.formatMessage({id: 'app.operation_success'}));
|
||||
setDetail({...record});
|
||||
message.success(intl.formatMessage({ id: 'app.operation_success' }));
|
||||
setDetail({ ...record });
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
}}
|
||||
dataSource={{...detail}}
|
||||
dataSource={{ ...detail }}
|
||||
>
|
||||
<ProDescriptions.Item
|
||||
dataIndex="name"
|
||||
|
@ -125,7 +125,7 @@ export default () => {
|
|||
className={styles.descriptionRemark}
|
||||
dataIndex="remark"
|
||||
valueType={'textarea'}
|
||||
fieldProps={{rows: 2, maxLength: 20}}
|
||||
fieldProps={{ rows: 2, maxLength: 20 }}
|
||||
/>
|
||||
</ProDescriptions>
|
||||
)
|
||||
|
@ -139,13 +139,13 @@ export default () => {
|
|||
history.push('/account/user-group');
|
||||
}}
|
||||
title={
|
||||
loading ? <Skeleton.Input style={{width: 50}} active size={'small'}/> : detail?.name
|
||||
loading ? <Skeleton.Input style={{ width: 50 }} active size={'small'} /> : detail?.name
|
||||
}
|
||||
content={<>{description}</>}
|
||||
tabList={[
|
||||
{
|
||||
key: UserGroupDetailTabs.member,
|
||||
tab: intl.formatMessage({id: 'pages.account.user_group_detail.tab_list.member'}),
|
||||
tab: intl.formatMessage({ id: 'pages.account.user_group_detail.tab_list.member' }),
|
||||
},
|
||||
{
|
||||
key: UserGroupDetailTabs.access_policy,
|
||||
|
@ -159,14 +159,14 @@ export default () => {
|
|||
setTabActiveKey(key);
|
||||
history.replace({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify({id, type: key}),
|
||||
search: queryString.stringify({ id, type: key }),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{/*成员信息*/}
|
||||
{type === UserGroupDetailTabs.member && <MemberList id={id}/>}
|
||||
{type === UserGroupDetailTabs.member && <MemberList id={id} />}
|
||||
{/*授权应用*/}
|
||||
{type === UserGroupDetailTabs.access_policy && <AccessStrategy userGroupId={id}/>}
|
||||
{type === UserGroupDetailTabs.access_policy && <AccessStrategy userGroupId={id} />}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -48,7 +48,9 @@ export default (props: { id: string }) => {
|
|||
*/
|
||||
const columns: ProColumns<AccountAPI.ListUser>[] = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'pages.account.user_group_detail.add_member.columns.full_name' }),
|
||||
title: intl.formatMessage({
|
||||
id: 'pages.account.user_group_detail.add_member.columns.full_name',
|
||||
}),
|
||||
dataIndex: 'fullName',
|
||||
fixed: 'left',
|
||||
ellipsis: true,
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import CreateUser from './CreateUser';
|
||||
|
||||
export default CreateUser;
|
||||
|
|
|
@ -134,7 +134,7 @@ const UpdateUser = (props: UpdateFormProps) => {
|
|||
if (values.phone) {
|
||||
params = { ...params, phone: `${values.phoneAreaCode}${values.phone}` };
|
||||
}
|
||||
if (values.expireDate){
|
||||
if (values.expireDate) {
|
||||
params = { ...params, expireDate: dayjs(values.expireDate).format('YYYY-MM-DD') };
|
||||
}
|
||||
const { success, result } = await updateUser(params).finally(() => {
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import UpdateUser from './UpdateUser';
|
||||
|
||||
export default UpdateUser;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { DesktopOutlined, ProfileOutlined } from '@ant-design/icons';
|
|||
import { GridContent, PageContainer } from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Menu, App } from 'antd';
|
||||
import { App, Menu } from 'antd';
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import AccessPolicy from './components/AccessPolicy';
|
||||
import AppAccount from './components/AppAccount';
|
||||
|
|
|
@ -34,10 +34,11 @@ import {
|
|||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
|
||||
import { Button, Form, App, Popconfirm, Table } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { App, Button, Form, Popconfirm, Table } from 'antd';
|
||||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
/**
|
||||
* 添加授权
|
||||
*
|
||||
|
|
|
@ -21,7 +21,7 @@ import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ModalForm, ProFormText, ProTable } from '@ant-design/pro-components';
|
||||
|
||||
import { Alert, Button, Form, App, Popconfirm, Table } from 'antd';
|
||||
import { Alert, App, Button, Form, Popconfirm, Table } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import { AppProtocolType } from '@/constant';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import { SsoScope } from '@/pages/app/AppConfig/constant';
|
||||
import { ProFormSelect } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
/**
|
||||
* 授权类型组件
|
||||
*
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
import { getAppConfig, saveAppConfig } from '@/services/app';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Alert, Divider, Form, App, Spin } from 'antd';
|
||||
import { Alert, App, Divider, Form, Spin } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EditableProTable,
|
||||
|
@ -36,6 +36,7 @@ import { useIntl } from '@umijs/max';
|
|||
import { AuthorizationType } from '../CommonConfig';
|
||||
import { GetApp } from '../../../data.d';
|
||||
import { generateUUID } from '@/utils/utils';
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
span: 6,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
import { getAppConfig, saveAppConfig } from '@/services/app';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Divider, Form, App, Spin, Alert } from 'antd';
|
||||
import { Alert, App, Divider, Form, Spin } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FooterToolbar,
|
||||
|
@ -32,6 +32,7 @@ import { omit } from 'lodash';
|
|||
import { useIntl } from '@umijs/max';
|
||||
import { AuthorizationType } from '../CommonConfig';
|
||||
import { GetApp } from '../../../data.d';
|
||||
|
||||
const layout = {
|
||||
labelCol: {
|
||||
xs: { span: 24 },
|
||||
|
@ -89,7 +90,7 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
</span>
|
||||
}
|
||||
/>
|
||||
<br/>
|
||||
<br />
|
||||
<ProForm
|
||||
layout={'horizontal'}
|
||||
{...layout}
|
||||
|
|
|
@ -81,11 +81,6 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
form.setFieldsValue({
|
||||
appId: id,
|
||||
...result,
|
||||
//重定向URI
|
||||
redirectUris: result.redirectUris?.length > 0 ? result.redirectUris : [undefined],
|
||||
//登出重定向URI
|
||||
postLogoutRedirectUris:
|
||||
result.postLogoutRedirectUris?.length > 0 ? result.postLogoutRedirectUris : [undefined],
|
||||
});
|
||||
//设置Endpoint相关
|
||||
setProtocolEndpoint(result.protocolEndpoint);
|
||||
|
@ -224,7 +219,7 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
</span>
|
||||
</>
|
||||
),
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormDependency name={['authGrantTypes']}>
|
||||
|
@ -250,18 +245,20 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
{
|
||||
validator: async (_, value) => {
|
||||
if (value && value.length > 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
throw new Error(
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.redirect_uris.rule.0.message',
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove }, {}) => (
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
|
@ -275,12 +272,6 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
})
|
||||
: ''
|
||||
}
|
||||
extra={
|
||||
index === fields.length - 1 &&
|
||||
intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.redirect_uris.extra',
|
||||
})
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
@ -314,13 +305,24 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<DeleteOutlined onClick={() => remove(field.name)} />
|
||||
) : null}
|
||||
</div>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item {...formItemLayoutWithOutLabel}>
|
||||
<Form.Item
|
||||
{...(fields.length === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
required={true}
|
||||
label={
|
||||
fields.length === 0
|
||||
? intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.redirect_uris',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
extra={intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.redirect_uris.extra',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
|
@ -329,27 +331,12 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
>
|
||||
{intl.formatMessage({ id: 'app.add' })}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.List
|
||||
name="postLogoutRedirectUris"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (value && value.length > 0) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.post_logout_redirect_uris.rule.0.message',
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Form.List name="postLogoutRedirectUris">
|
||||
{(fields, { add, remove }, {}) => (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
|
@ -357,7 +344,6 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
<Form.Item
|
||||
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
key={field.key}
|
||||
required={true}
|
||||
label={
|
||||
index === 0
|
||||
? intl.formatMessage({
|
||||
|
@ -365,12 +351,6 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
})
|
||||
: ''
|
||||
}
|
||||
extra={
|
||||
index === fields.length - 1 &&
|
||||
intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.post_logout_redirect_uris.extra',
|
||||
})
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
@ -404,14 +384,24 @@ export default (props: { app: GetApp | Record<string, any> }) => {
|
|||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<DeleteOutlined onClick={() => remove(field.name)} />
|
||||
) : null}
|
||||
</div>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
<Form.Item {...formItemLayoutWithOutLabel}>
|
||||
<Form.Item
|
||||
{...(fields.length === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
|
||||
label={
|
||||
fields.length === 0
|
||||
? intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.post_logout_redirect_uris',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
extra={intl.formatMessage({
|
||||
id: 'pages.app.config.items.login_access.protocol_config.oidc.post_logout_redirect_uris.extra',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { createApp, getAppTemplateList } from '@/services/app';
|
||||
import { history } from '@@/core/history';
|
||||
import {
|
||||
ActionType,
|
||||
|
@ -36,8 +35,9 @@ import { useRef, useState } from 'react';
|
|||
import useStyle from './style';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import classnames from 'classnames';
|
||||
import { ListTemplate } from '@/pages/app/AppCreate/data.d';
|
||||
import { ListTemplate } from './data.d';
|
||||
import { AppType } from '@/constant';
|
||||
import { createApp, getAppTemplateList } from './service';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const prefixCls = 'topiam-create-app';
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
import { ListTemplate } from './data.d';
|
||||
import { RequestData } from '@ant-design/pro-components';
|
||||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import { SortOrder } from 'antd/es/table/interface';
|
||||
|
||||
/**
|
||||
* 获取应用模板列表
|
||||
*/
|
||||
export async function getAppTemplateList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<ListTemplate>> {
|
||||
return request<API.ApiResult<ListTemplate>>('/api/v1/app/template/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
}).then((result: API.ApiResult<ListTemplate>) => {
|
||||
const data: RequestData<ListTemplate> = {
|
||||
data: result ? result?.result : [],
|
||||
success: result?.success,
|
||||
};
|
||||
return Promise.resolve(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Application
|
||||
*/
|
||||
export async function createApp(
|
||||
params: Record<string, string>,
|
||||
): Promise<API.ApiResult<Record<string, string>>> {
|
||||
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/create`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
requestType: 'json',
|
||||
});
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { disableApp, enableApp, getAppList, removeApp } from '@/services/app';
|
||||
import { getAppList } from '@/services/app';
|
||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import type { ActionType } from '@ant-design/pro-components';
|
||||
import { PageContainer, ProList } from '@ant-design/pro-components';
|
||||
|
@ -25,6 +25,7 @@ import { history, useIntl } from '@umijs/max';
|
|||
import useStyle from './style';
|
||||
import classnames from 'classnames';
|
||||
import { AppList } from './data.d';
|
||||
import { disableApp, enableApp, removeApp } from './service';
|
||||
|
||||
const prefixCls = 'app-list';
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/**
|
||||
* 启用应用
|
||||
*/
|
||||
export async function enableApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/app/enable/${id}`, { method: 'PUT' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用应用
|
||||
*/
|
||||
export async function disableApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/app/disable/${id}`, { method: 'PUT' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Application
|
||||
*/
|
||||
export async function removeApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request<API.ApiResult<boolean>>(`/api/v1/app/delete/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
|
@ -25,11 +25,11 @@ import { Badge, DatePicker, Select, Space, Tag } from 'antd';
|
|||
import dayjs from 'dayjs';
|
||||
import { useRef, useState } from 'react';
|
||||
import ExpandedCard from './components/ExpandedCard';
|
||||
import { EventStatus, UserType } from './data.d';
|
||||
import { AuditList, AuditTypeGroup, EventStatus, UserType } from './data.d';
|
||||
import useStyles from './style';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { AuditList, AuditTypeGroup } from './data.d';
|
||||
|
||||
const { OptGroup } = Select;
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
|
|
@ -19,9 +19,10 @@ import { Collapse, Typography } from 'antd';
|
|||
import Paragraph from 'antd/es/typography/Paragraph';
|
||||
import moment from 'moment';
|
||||
import type { UserType } from '../../data.d';
|
||||
import { EventStatus, AuditList } from '../../data.d';
|
||||
import { AuditList, EventStatus } from '../../data.d';
|
||||
import useStyles from './style';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ExpandedCardProps {
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import type { RequestData } from '@ant-design/pro-components';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import type { ReactText } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import { AuditList, AuditTypeGroup } from './data.d';
|
||||
|
||||
|
@ -28,7 +27,7 @@ import { AuditList, AuditTypeGroup } from './data.d';
|
|||
export async function getAuditList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AuditList>> {
|
||||
return request<API.ApiResult<AuditList>>(`/api/v1/audit/list`, {
|
||||
params: {
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import type { RequestData } from '@ant-design/pro-components';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import type { ReactText } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import { GetIdentityProvider, ListIdentityProvider } from './data.d';
|
||||
|
||||
|
@ -28,7 +27,7 @@ import { GetIdentityProvider, ListIdentityProvider } from './data.d';
|
|||
export async function getIdpList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<ListIdentityProvider>> {
|
||||
return request<API.ApiResult<ListIdentityProvider>>('/api/v1/authn/idp/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
|
|
@ -20,7 +20,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
|
|||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||
|
||||
import { Badge, App, Popconfirm, Space, Table } from 'antd';
|
||||
import { App, Badge, Popconfirm, Space, Table } from 'antd';
|
||||
import React, { useRef } from 'react';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { SessionList } from './data.d';
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import CreateAdministrator from './CreateAdministrator';
|
||||
|
||||
export default CreateAdministrator;
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import ResetAdministratorPassword from './ResetAdministratorPassword';
|
||||
|
||||
export default ResetAdministratorPassword;
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import UpdateAdministrator from './UpdateAdministrator';
|
||||
|
||||
export default UpdateAdministrator;
|
||||
|
|
|
@ -15,4 +15,21 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
///
|
||||
/**
|
||||
* 管理员列表
|
||||
*/
|
||||
export interface AdministratorList {
|
||||
id: string;
|
||||
username: string;
|
||||
fullName: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: string;
|
||||
emailVerified: boolean;
|
||||
phoneVerified: boolean;
|
||||
authTotal: number;
|
||||
lastAuthIp: string;
|
||||
lastAuthTime: Date;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SortOrder } from 'antd/es/table/interface';
|
||||
import { ReactText } from 'react';
|
||||
import { RequestData } from '@ant-design/pro-components';
|
||||
import { request } from '@@/exports';
|
||||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import { SortOrder } from 'antd/es/table/interface';
|
||||
import { AdministratorList } from './data';
|
||||
|
||||
/**
|
||||
* 查询管理员列表
|
||||
|
@ -27,15 +27,12 @@ import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
|||
export async function getAdministratorList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
): Promise<RequestData<SettingAPI.AdministratorList>> {
|
||||
return request<API.ApiResult<SettingAPI.AdministratorList>>(
|
||||
'/api/v1/setting/administrator/list',
|
||||
{
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AdministratorList>> {
|
||||
return request<API.ApiResult<AdministratorList>>('/api/v1/setting/administrator/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
},
|
||||
).then((result: API.ApiResult<SettingAPI.AdministratorList>) => {
|
||||
const data: RequestData<SettingAPI.AdministratorList> = {
|
||||
}).then((result: API.ApiResult<AdministratorList>) => {
|
||||
const data: RequestData<AdministratorList> = {
|
||||
data: result?.result?.list ? result?.result?.list : [],
|
||||
success: result?.success,
|
||||
total: result?.result?.pagination ? result?.result?.pagination.total : 0,
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import Basic from './Basic';
|
||||
|
||||
export default Basic;
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import DefensePolicy from './DefensePolicy';
|
||||
|
||||
export default DefensePolicy;
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
ProFormSwitch,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Form, App, Space, Spin } from 'antd';
|
||||
import { App, Form, Space, Spin } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import MaxMind from './components/MaxMind';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Form, App, Space, Spin } from 'antd';
|
||||
import { App, Form, Space, Spin } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
import { disableCustomTemplate, getMailTemplate, saveMailTemplate } from '../../service';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Button, Drawer, Form, Input, App, Spin, Switch } from 'antd';
|
||||
import { App, Button, Drawer, Form, Input, Spin, Switch } from 'antd';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/mode/clike/clike';
|
||||
import 'codemirror/mode/cmake/cmake';
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Form, App, Segmented, Spin } from 'antd';
|
||||
import { App, Form, Segmented, Spin } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import AliCloud from './AliCloud';
|
||||
import QiNiu from './QiNiu';
|
||||
|
|
|
@ -15,25 +15,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* 管理员列表
|
||||
*/
|
||||
export interface AdministratorList {
|
||||
id: string;
|
||||
username: string;
|
||||
fullName: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: string;
|
||||
emailVerified: boolean;
|
||||
phoneVerified: boolean;
|
||||
authTotal: number;
|
||||
lastAuthIp: string;
|
||||
lastAuthTime: Date;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件列表
|
||||
*/
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
ProFormSwitch,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { Form, App, Space, Spin } from 'antd';
|
||||
import { App, Form, Space, Spin } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import AliCloudOss from './components/AliCloud';
|
||||
import MinIO from './components/MinIo';
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
*/
|
||||
import { LOGIN_PATH } from '@/utils/utils';
|
||||
import { history } from '@@/core/history';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { useIntl, useLocation, useModel } from '@umijs/max';
|
||||
import { useMount } from 'ahooks';
|
||||
import { App } from 'antd';
|
||||
import queryString from 'query-string';
|
||||
import { useIntl, useLocation } from '@umijs/max';
|
||||
|
||||
export default () => {
|
||||
const { setInitialState } = useModel('@@initialState');
|
||||
|
|
|
@ -19,7 +19,7 @@ import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
|||
import type { RequestData } from '@ant-design/pro-components';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import qs from 'qs';
|
||||
import type { Key, ReactText } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import { ParamCheckType } from '@/constant';
|
||||
|
||||
|
@ -129,7 +129,7 @@ export async function moveOrganization(
|
|||
export async function getUserList(
|
||||
params: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, ReactText[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListUser>> {
|
||||
return request<API.ApiResult<AccountAPI.ListUser>>('/api/v1/user/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -171,7 +171,7 @@ export async function batchGetUser(ids: string[]): Promise<API.ApiResult<Account
|
|||
export async function getLoginAuditList(
|
||||
params: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, ReactText[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.UserLoginAuditList>> {
|
||||
return request<API.ApiResult<AccountAPI.UserLoginAuditList>>('/api/v1/user/login_audit/list', {
|
||||
method: 'GET',
|
||||
|
@ -334,7 +334,7 @@ export async function disableUser(id: string): Promise<API.ApiResult<boolean>> {
|
|||
export async function getUserListNotInGroup(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListUser>> {
|
||||
return request<API.ApiResult<AccountAPI.ListUser>>(`/api/v1/user/notin_group_list`, {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -354,7 +354,7 @@ export async function getUserListNotInGroup(
|
|||
export async function getUserGroupList(
|
||||
params: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, ReactText[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListUserGroup>> {
|
||||
return request<API.ApiResult<AccountAPI.ListUserGroup>>('/api/v1/user_group/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -434,7 +434,7 @@ export async function removeUserGroupMember(
|
|||
export async function getUserGroupMemberList(
|
||||
params: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, ReactText[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AccountAPI.ListUser>> {
|
||||
return request<API.ApiResult<AccountAPI.ListUser>>(
|
||||
`/api/v1/user_group/${params.id}/member_list`,
|
||||
|
|
|
@ -21,25 +21,6 @@ import type { SortOrder } from 'antd/es/table/interface';
|
|||
import { request } from '@umijs/max';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
|
||||
/**
|
||||
* 获取应用模板列表
|
||||
*/
|
||||
export async function getAppTemplateList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, string[] | null>,
|
||||
): Promise<RequestData<AppAPI.ListTemplate>> {
|
||||
return request<API.ApiResult<AppAPI.ListTemplate>>('/api/v1/app/template/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
}).then((result: API.ApiResult<AppAPI.ListTemplate>) => {
|
||||
const data: RequestData<AppAPI.ListTemplate> = {
|
||||
data: result ? result?.result : [],
|
||||
success: result?.success,
|
||||
};
|
||||
return Promise.resolve(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Application Template FormSchema
|
||||
*/
|
||||
|
@ -58,7 +39,7 @@ export async function getAppTemplateFormSchema(
|
|||
export async function getAppList(
|
||||
params?: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, string[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AppAPI.AppList>> {
|
||||
return request<API.ApiResult<AppAPI.AppList>>('/api/v1/app/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -73,19 +54,6 @@ export async function getAppList(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Application
|
||||
*/
|
||||
export async function createApp(
|
||||
params: Record<string, string>,
|
||||
): Promise<API.ApiResult<Record<string, string>>> {
|
||||
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/create`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
requestType: 'json',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Application
|
||||
*/
|
||||
|
@ -97,15 +65,6 @@ export async function updateApp(params: Record<string, string>): Promise<API.Api
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Application
|
||||
*/
|
||||
export async function removeApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request<API.ApiResult<boolean>>(`/api/v1/app/delete/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Application
|
||||
*/
|
||||
|
@ -115,20 +74,6 @@ export async function getApp(id: string): Promise<API.ApiResult<AppAPI.GetApp>>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用应用
|
||||
*/
|
||||
export async function enableApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/app/enable/${id}`, { method: 'PUT' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用应用
|
||||
*/
|
||||
export async function disableApp(id: string): Promise<API.ApiResult<boolean>> {
|
||||
return request(`/api/v1/app/disable/${id}`, { method: 'PUT' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Config
|
||||
*/
|
||||
|
@ -189,7 +134,7 @@ export async function getCertList(
|
|||
export async function getAppAccountList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, string[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AppAPI.AppAccountList>> {
|
||||
return request<API.ApiResult<AppAPI.AppAccountList>>('/api/v1/app/account/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
@ -231,7 +176,7 @@ export async function removeAccount(id: string): Promise<API.ApiResult<boolean>>
|
|||
export async function getAppAccessPolicyList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, string[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AppAPI.AppAccessPolicyList>> {
|
||||
return request<API.ApiResult<AppAPI.AppAccessPolicyList>>('/api/v1/app/access_policy/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
|
|
@ -423,18 +423,6 @@ declare namespace AppAPI {
|
|||
remark: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用模板
|
||||
*/
|
||||
export type ListTemplate = {
|
||||
protocol: string;
|
||||
type: string;
|
||||
code: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用信息
|
||||
*/
|
||||
|
|
|
@ -20,6 +20,10 @@ package cn.topiam.employee.portal.configuration.security;
|
|||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
|
@ -44,6 +48,7 @@ import cn.topiam.employee.portal.handler.PortalAccessDeniedHandler;
|
|||
import cn.topiam.employee.portal.handler.PortalAuthenticationEntryPoint;
|
||||
import cn.topiam.employee.portal.handler.PortalLogoutSuccessHandler;
|
||||
import cn.topiam.employee.portal.listener.PortalSessionInformationExpiredStrategy;
|
||||
import cn.topiam.employee.support.redis.KeyStringRedisSerializer;
|
||||
import cn.topiam.employee.support.security.csrf.SpaCsrfTokenRequestHandler;
|
||||
import static org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK;
|
||||
import static org.springframework.web.cors.CorsConfiguration.ALL;
|
||||
|
@ -202,6 +207,18 @@ public class AbstractSecurityConfiguration {
|
|||
return configurer -> configurer.requireExplicitSave(false);
|
||||
}
|
||||
|
||||
public RedisTemplate<String, String> getRedisTemplate(RedisConnectionFactory redisConnectionFactory,
|
||||
CacheProperties cacheProperties) {
|
||||
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
KeyStringRedisSerializer keyStringRedisSerializer = new KeyStringRedisSerializer(
|
||||
cacheProperties.getRedis().getKeyPrefix());
|
||||
redisTemplate.setKeySerializer(keyStringRedisSerializer);
|
||||
redisTemplate.setValueSerializer(StringRedisSerializer.UTF_8);
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
private final SettingRepository settingRepository;
|
||||
|
||||
public AbstractSecurityConfiguration(SettingRepository settingRepository) {
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
*/
|
||||
package cn.topiam.employee.portal.configuration.security;
|
||||
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
|
@ -33,6 +36,7 @@ import cn.topiam.employee.audit.event.AuditEventPublish;
|
|||
import cn.topiam.employee.common.repository.setting.SettingRepository;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationFailureEventListener;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationSuccessEventListener;
|
||||
import cn.topiam.employee.protocol.jwt.authorization.RedisJwtAuthorizationService;
|
||||
import cn.topiam.employee.protocol.jwt.configurers.JwtAuthorizationServerConfigurer;
|
||||
import static cn.topiam.employee.common.constant.ConfigBeanNameConstants.JWT_PROTOCOL_SECURITY_FILTER_CHAIN;
|
||||
|
||||
|
@ -103,4 +107,12 @@ public class JwtProtocolSecurityConfiguration extends AbstractSecurityConfigurat
|
|||
return new JwtAuthenticationFailureEventListener(auditEventPublish);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisJwtAuthorizationService redisJwtAuthorizationService(RedisConnectionFactory redisConnectionFactory,
|
||||
CacheProperties cacheProperties,
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
return new RedisJwtAuthorizationService(
|
||||
getRedisTemplate(redisConnectionFactory, cacheProperties), beanFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -146,14 +146,8 @@ public class OidcProtocolSecurityConfiguration extends AbstractSecurityConfigura
|
|||
CacheProperties cacheProperties,
|
||||
RegisteredClientRepository clientRepository,
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
KeyStringRedisSerializer keyStringRedisSerializer = new KeyStringRedisSerializer(
|
||||
cacheProperties.getRedis().getKeyPrefix());
|
||||
redisTemplate.setKeySerializer(keyStringRedisSerializer);
|
||||
redisTemplate.setValueSerializer(StringRedisSerializer.UTF_8);
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return new RedisOAuth2AuthorizationServiceWrapper(redisTemplate, clientRepository,
|
||||
return new RedisOAuth2AuthorizationServiceWrapper(
|
||||
getRedisTemplate(redisConnectionFactory, cacheProperties), clientRepository,
|
||||
beanFactory);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -59,7 +59,7 @@ const GlobalHeaderRight: React.FC = () => {
|
|||
return (
|
||||
<div className={styles.main}>
|
||||
<Helmet>
|
||||
<link rel="icon" href={initialState?.globalConfig?.appearance?.favicon} />
|
||||
<link rel="icon" href={'/favicon.ico'} />
|
||||
</Helmet>
|
||||
<About />
|
||||
<SelectLang className={styles.action} />
|
||||
|
|
|
@ -28,8 +28,7 @@ import { AccountSettingsStateKey } from './data.d';
|
|||
import classnames from 'classnames';
|
||||
import useStyle from './style';
|
||||
import queryString from 'query-string';
|
||||
import { useLocation } from '@umijs/max';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useIntl, useLocation } from '@umijs/max';
|
||||
|
||||
const prefixCls = 'account';
|
||||
|
||||
|
|
|
@ -16,8 +16,13 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { ProForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
|
||||
import { Avatar, Button, Form, App, Skeleton, Upload } from 'antd';
|
||||
import {
|
||||
ProForm,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
useStyle as useAntdStyle,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Avatar, Button, Form, Skeleton, Upload } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { changeBaseInfo } from '@/pages/Account/service';
|
||||
|
@ -28,7 +33,6 @@ import ImgCrop from 'antd-img-crop';
|
|||
import { uploadFile } from '@/services/upload';
|
||||
import { useModel } from '@umijs/max';
|
||||
import classnames from 'classnames';
|
||||
import { useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
const prefixCls = 'account-base';
|
||||
|
|
|
@ -22,13 +22,17 @@ import { aesEcbEncrypt } from '@/utils/aes';
|
|||
import { onGetEncryptSecret, phoneIsValidNumber } from '@/utils/utils';
|
||||
import { FormattedMessage } from '@@/plugin-locale/localeExports';
|
||||
import type { CaptFieldRef, ProFormInstance } from '@ant-design/pro-components';
|
||||
import { ModalForm, ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
|
||||
import { ConfigProvider, App, Spin } from 'antd';
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormCaptcha,
|
||||
ProFormText,
|
||||
useStyle as useAntdStyle,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, ConfigProvider, Spin } from 'antd';
|
||||
import { omit } from 'lodash';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { FormLayout } from './constant';
|
||||
import classnames from 'classnames';
|
||||
import { useStyle as useAntdStyle } from '@ant-design/pro-components';
|
||||
import { ConfigContext } from 'antd/es/config-provider';
|
||||
import { useIntl } from '@@/exports';
|
||||
import PhoneAreaCodeSelect from '@/components/PhoneAreaCodeSelect';
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import Account from './Account';
|
||||
|
||||
export default Account;
|
||||
|
|
|
@ -16,22 +16,26 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import type { ActionType } from '@ant-design/pro-components';
|
||||
import { PageContainer, ProList } from '@ant-design/pro-components';
|
||||
import { Alert, Avatar, Card, Input, App, Typography } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { PageContainer, ProCard, ProList } from '@ant-design/pro-components';
|
||||
import { Alert, App, Avatar, Badge, Card, Input, Typography } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { AppList } from './data.d';
|
||||
import { InitLoginType } from './data.d';
|
||||
import { queryAppList } from './service';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
import useStyle from './style';
|
||||
import classnames from 'classnames';
|
||||
const { Paragraph } = Typography;
|
||||
const prefixCls = 'topiam-app-list';
|
||||
|
||||
const CardList = () => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyle(prefixCls);
|
||||
const [activeKey, setActiveKey] = useState<React.Key | undefined>('tab1');
|
||||
|
||||
const { message } = App.useApp();
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [searchParams, setSearchParams] = useState<{ name: string }>();
|
||||
|
||||
const content = (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Input.Search
|
||||
|
@ -60,13 +64,45 @@ const CardList = () => {
|
|||
document.body.removeChild(div);
|
||||
};
|
||||
|
||||
const renderBadge = (count: number, active = false) => {
|
||||
return (
|
||||
<PageContainer content={content}>
|
||||
<Alert message={intl.formatMessage({ id: 'pages.application.alert' })} showIcon />
|
||||
<Badge
|
||||
count={count}
|
||||
style={{
|
||||
marginBlockStart: -2,
|
||||
marginInlineStart: 4,
|
||||
color: active ? '#1890FF' : '#999',
|
||||
backgroundColor: active ? '#E6F7FF' : '#eee',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles}>
|
||||
<PageContainer
|
||||
className={classnames(`${prefixCls}`)}
|
||||
tabList={[
|
||||
{
|
||||
tab: '应用列表',
|
||||
key: 'list',
|
||||
},
|
||||
{
|
||||
tab: '应用账号',
|
||||
key: 'account',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Alert
|
||||
banner
|
||||
type={'info'}
|
||||
message={intl.formatMessage({ id: 'pages.application.alert' })}
|
||||
showIcon
|
||||
/>
|
||||
<br />
|
||||
<ProList<AppList>
|
||||
rowKey="id"
|
||||
ghost
|
||||
split
|
||||
grid={{
|
||||
xs: 1,
|
||||
sm: 2,
|
||||
|
@ -77,15 +113,39 @@ const CardList = () => {
|
|||
}}
|
||||
request={queryAppList}
|
||||
pagination={{}}
|
||||
toolbar={{
|
||||
menu: {
|
||||
type: 'tab',
|
||||
activeKey,
|
||||
items: [
|
||||
{
|
||||
key: 'tab1',
|
||||
label: <span>开发分组{renderBadge(99, activeKey === 'tab1')}</span>,
|
||||
},
|
||||
{
|
||||
key: 'tab2',
|
||||
label: <span>运维分组{renderBadge(32, activeKey === 'tab2')}</span>,
|
||||
},
|
||||
],
|
||||
onChange(key) {
|
||||
setActiveKey(key);
|
||||
},
|
||||
},
|
||||
}}
|
||||
params={searchParams}
|
||||
actionRef={actionRef}
|
||||
tableExtraRender={() => {
|
||||
return <ProCard>{content}</ProCard>;
|
||||
}}
|
||||
renderItem={(item: AppList) => {
|
||||
return (
|
||||
item &&
|
||||
item.id && (
|
||||
<Card
|
||||
style={{ margin: 5 }}
|
||||
style={{ margin: 8 }}
|
||||
className={`${prefixCls}-item-card`}
|
||||
hoverable
|
||||
bordered={false}
|
||||
onClick={async () => {
|
||||
if (item.initLoginType === InitLoginType.PORTAL_OR_APP_INIT_SSO) {
|
||||
initSso(item.initLoginUrl);
|
||||
|
@ -96,19 +156,28 @@ const CardList = () => {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={<Avatar key={item.id} shape="square" size={50} src={item.icon} />}
|
||||
title={item.name}
|
||||
description={
|
||||
<Paragraph ellipsis={{ rows: 2, tooltip: true }}>{item.description}</Paragraph>
|
||||
}
|
||||
/>
|
||||
<div className={`${prefixCls}-item-content-wrapper`} key={item.id}>
|
||||
<div className={`${prefixCls}-item-avatar`}>
|
||||
<Avatar key={item.icon} shape="square" src={item.icon} size={45} />
|
||||
</div>
|
||||
<div className={`${prefixCls}-item-content`}>
|
||||
<span className={`${prefixCls}-item-content-title`}>{item.name}</span>
|
||||
<Paragraph
|
||||
className={`${prefixCls}-item-content-desc`}
|
||||
ellipsis={{ tooltip: item.description, rows: 2 }}
|
||||
title={item.description}
|
||||
>
|
||||
{item.description ? item.description : <> </>}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import type { RequestData } from '@ant-design/pro-components';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import type { ReactText } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import type { AppList } from './data.d';
|
||||
|
||||
|
@ -28,7 +27,7 @@ import type { AppList } from './data.d';
|
|||
export async function queryAppList(
|
||||
params?: Record<string, any>,
|
||||
sort?: Record<string, SortOrder>,
|
||||
filter?: Record<string, ReactText[] | null>,
|
||||
filter?: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AppList>> {
|
||||
const { result, success } = await request<API.ApiResult<AppList>>('/api/v1/app/list', {
|
||||
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* eiam-portal - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(({ token, css, prefixCls }, prop) => {
|
||||
const prefixClassName = prop as string;
|
||||
return css`
|
||||
.${prefixClassName} {
|
||||
border: none;
|
||||
border-radius: ${token.borderRadius};
|
||||
.${prefixCls}-pro-table-list-toolbar-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&-item-card {
|
||||
.${prefixCls}-card-body {
|
||||
padding: 16px !important;
|
||||
background: #f7f8fa;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
}
|
||||
}
|
||||
|
||||
&-item-content-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-item-avatar {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
|
||||
& .${prefixCls}-avatar {
|
||||
width: 54px !important;
|
||||
height: 54px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
|
||||
> img {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
min-width: 0;
|
||||
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
color: #293350;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&-type {
|
||||
color: #9fabcb;
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
&-desc {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
});
|
||||
export default useStyles;
|
|
@ -18,9 +18,10 @@
|
|||
import { Collapse, Typography } from 'antd';
|
||||
import Paragraph from 'antd/es/typography/Paragraph';
|
||||
import moment from 'moment';
|
||||
import { EventStatus, AuditList } from '../../data.d';
|
||||
import { AuditList, EventStatus } from '../../data.d';
|
||||
import useStyles from './style';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ExpandedCardProps {
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
import { filterParamConverter, sortParamConverter } from '@/utils/utils';
|
||||
import type { RequestData } from '@ant-design/pro-components';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import type { ReactText } from 'react';
|
||||
import { request } from '@umijs/max';
|
||||
import type { AuditList, AuditTypeGroup } from './data.d';
|
||||
|
||||
|
@ -28,7 +27,7 @@ import type { AuditList, AuditTypeGroup } from './data.d';
|
|||
export async function getAuditList(
|
||||
params: Record<string, any>,
|
||||
sort: Record<string, SortOrder>,
|
||||
filter: Record<string, ReactText[] | null>,
|
||||
filter: Record<string, (string | number)[] | null>,
|
||||
): Promise<RequestData<AuditList>> {
|
||||
return request<API.ApiResult<AuditList>>(`/api/v1/audit/list`, {
|
||||
params: {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { getCurrentStatus, getLoginEncryptSecret } from '@/services';
|
|||
import { aesEcbEncrypt } from '@/utils/aes';
|
||||
import { ProCard, ProForm, ProFormCheckbox, ProFormInstance } from '@ant-design/pro-components';
|
||||
import { useAsyncEffect, useRequest, useSafeState } from 'ahooks';
|
||||
import { Alert, Avatar, App, Skeleton, Space, Spin, Tabs, Tooltip } from 'antd';
|
||||
import { Alert, App, Avatar, Skeleton, Space, Spin, Tabs, Tooltip } from 'antd';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useRef, useState } from 'react';
|
||||
import { FormattedMessage, Helmet, history, useIntl } from '@umijs/max';
|
||||
|
|
|
@ -24,7 +24,7 @@ import { history } from '@@/core/history';
|
|||
import { LockTwoTone, UserOutlined } from '@ant-design/icons';
|
||||
import { ProForm, ProFormText } from '@ant-design/pro-components';
|
||||
import { useMount, useSafeState } from 'ahooks';
|
||||
import { Col, Form, App, Row, Spin, Typography, Alert } from 'antd';
|
||||
import { Alert, App, Col, Form, Row, Spin, Typography } from 'antd';
|
||||
import { idpBindUser } from '../../service';
|
||||
import { useIntl } from '@@/plugin-locale';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
|
|
@ -22,7 +22,7 @@ import { ProFormCaptcha, ProFormText } from '@ant-design/pro-components';
|
|||
import { App } from 'antd';
|
||||
import { useImperativeHandle, useRef } from 'react';
|
||||
import { sendLoginCaptchaOpt } from '../service';
|
||||
import { phoneIsValidNumber, phoneParseNumber, emailValidator } from '@/utils/utils';
|
||||
import { emailValidator, phoneIsValidNumber, phoneParseNumber } from '@/utils/utils';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyle = createStyles(({ token }) => {
|
||||
|
|
|
@ -30,6 +30,7 @@ import { forgetPasswordCode } from '@/pages/Login/service';
|
|||
import Title from '@/components/Title';
|
||||
import { useRef } from 'react';
|
||||
import { phoneIsValidNumber, phoneParseNumber } from '@/utils/utils';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const Code = (props: ProFormProps) => {
|
||||
const intl = useIntl();
|
||||
|
|
|
@ -21,6 +21,7 @@ import { FormattedMessage, useIntl } from '@@/exports';
|
|||
import useStyle from './style';
|
||||
import Title from '@/components/Title';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
const prefixCls = 'topiam-forget-password';
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import { Button } from 'antd';
|
||||
import useStyle from './style';
|
||||
import { useIntl } from '@@/exports';
|
||||
|
||||
const prefixCls = 'topiam-forget-password';
|
||||
|
||||
const Success = ({ close }: { close: () => void }) => {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||
import { Badge, App, Popconfirm, Space, Table } from 'antd';
|
||||
import { App, Badge, Popconfirm, Space, Table } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import type { SessionList } from './data.d';
|
||||
import { getSessions, removeSessions } from './service';
|
||||
|
|
|
@ -26,6 +26,8 @@ import org.springframework.util.Assert;
|
|||
import cn.topiam.employee.application.form.model.FormProtocolConfig;
|
||||
import cn.topiam.employee.application.jwt.model.JwtProtocolConfig;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
|
@ -41,16 +43,19 @@ public class FormAuthenticationToken extends AbstractAuthenticationToken {
|
|||
/**
|
||||
* 账户名称
|
||||
*/
|
||||
@Getter
|
||||
private final String accountUsername;
|
||||
|
||||
/**
|
||||
* 账户凭据
|
||||
*/
|
||||
@Getter
|
||||
private final String accountCredential;
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
@Getter
|
||||
private final FormProtocolConfig config;
|
||||
|
||||
/**
|
||||
|
@ -105,15 +110,4 @@ public class FormAuthenticationToken extends AbstractAuthenticationToken {
|
|||
return this.principal;
|
||||
}
|
||||
|
||||
public String getAccountUsername() {
|
||||
return accountUsername;
|
||||
}
|
||||
|
||||
public String getAccountCredential() {
|
||||
return accountCredential;
|
||||
}
|
||||
|
||||
public FormProtocolConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import org.springframework.util.Assert;
|
|||
|
||||
import cn.topiam.employee.application.form.model.FormProtocolConfig;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
|
@ -33,6 +35,7 @@ import cn.topiam.employee.application.form.model.FormProtocolConfig;
|
|||
public class FormRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private final Authentication principal;
|
||||
|
||||
@Getter
|
||||
private final FormProtocolConfig config;
|
||||
|
||||
/**
|
||||
|
@ -79,7 +82,4 @@ public class FormRequestAuthenticationToken extends AbstractAuthenticationToken
|
|||
return this.principal;
|
||||
}
|
||||
|
||||
public FormProtocolConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,6 @@ import jakarta.servlet.FilterChain;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.GRANT_TYPE;
|
||||
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE;
|
||||
import static cn.topiam.employee.support.util.HttpRequestUtils.getRequestHeaders;
|
||||
|
||||
|
@ -85,7 +83,6 @@ public final class FormAuthorizationServerContextFilter extends OncePerRequestFi
|
|||
return;
|
||||
}
|
||||
try {
|
||||
request.getParameterValues(GRANT_TYPE);
|
||||
//@formatter:off
|
||||
Map<String, String> variables = matcher.getVariables();
|
||||
String appCode = variables.get(APP_CODE);
|
||||
|
|
|
@ -195,6 +195,13 @@ public final class FormAuthenticationEndpointFilter extends OncePerRequestFilter
|
|||
LogMessage.format("Authorization request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
} catch (Exception ex) {
|
||||
FormError error = new FormError(SERVER_ERROR,ex.getMessage(),FORM_ERROR_URI);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(error, ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new FormAuthenticationException(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package cn.topiam.employee.protocol.jwt.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtAuthenticationException;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtError;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtErrorCodes;
|
||||
import cn.topiam.employee.protocol.jwt.http.converter.JwtErrorHttpMessageConverter;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/9/4 13:03
|
||||
*/
|
||||
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
/**
|
||||
* Called when an authentication attempt fails.
|
||||
*
|
||||
* @param request the request during which the authentication attempt occurred.
|
||||
* @param response the response.
|
||||
* @param exception the exception which was thrown to reject the authentication
|
||||
* request.
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException,
|
||||
ServletException {
|
||||
if (exception instanceof JwtAuthenticationException) {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
JwtError error = ((JwtAuthenticationException) exception).getError();
|
||||
if (error.getErrorCode().equals(JwtErrorCodes.SERVER_ERROR)) {
|
||||
httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
if (error.getErrorCode().equals(JwtErrorCodes.INVALID_REQUEST)) {
|
||||
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应处理器
|
||||
*/
|
||||
private final HttpMessageConverter<JwtError> errorHttpResponseConverter = new JwtErrorHttpMessageConverter();
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
package cn.topiam.employee.protocol.jwt.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
@ -25,6 +26,8 @@ import org.springframework.security.core.Authentication;
|
|||
import cn.topiam.employee.application.jwt.model.JwtProtocolConfig;
|
||||
import cn.topiam.employee.protocol.jwt.token.IdToken;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
|
@ -32,6 +35,9 @@ import cn.topiam.employee.protocol.jwt.token.IdToken;
|
|||
*/
|
||||
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Getter
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* principal
|
||||
*/
|
||||
|
@ -40,11 +46,13 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
|||
/**
|
||||
* idToken
|
||||
*/
|
||||
@Getter
|
||||
private final IdToken idToken;
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
@Getter
|
||||
private final JwtProtocolConfig config;
|
||||
|
||||
/**
|
||||
|
@ -60,6 +68,7 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
|||
this.principal = principal;
|
||||
this.idToken = idToken;
|
||||
this.config = config;
|
||||
this.id = UUID.randomUUID().toString();
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
@ -93,11 +102,4 @@ public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
|||
return this.principal;
|
||||
}
|
||||
|
||||
public IdToken getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
public JwtProtocolConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package cn.topiam.employee.protocol.jwt.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/9/4 13:43
|
||||
*/
|
||||
public class JwtLogoutAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Authentication principal;
|
||||
|
||||
@Getter
|
||||
private final String sessionId;
|
||||
|
||||
public JwtLogoutAuthenticationToken(Authentication principal, String sessionId) {
|
||||
super(new ArrayList<>());
|
||||
this.principal = principal;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The credentials that prove the principal is correct. This is usually a password,
|
||||
* but could be anything relevant to the <code>AuthenticationManager</code>. Callers
|
||||
* are expected to populate the credentials.
|
||||
*
|
||||
* @return the credentials that prove the identity of the <code>Principal</code>
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity of the principal being authenticated. In the case of an authentication
|
||||
* request with username and password, this would be the username. Callers are
|
||||
* expected to populate the principal for an authentication request.
|
||||
* <p>
|
||||
* The <tt>AuthenticationManager</tt> implementation will often return an
|
||||
* <tt>Authentication</tt> containing richer information as the principal for use by
|
||||
* the application. Many of the authentication providers will create a
|
||||
* {@code UserDetails} object as the principal.
|
||||
*
|
||||
* @return the <code>Principal</code> being authenticated or the authenticated
|
||||
* principal after authentication.
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if {@link #getPrincipal()} is authenticated, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if {@link #getPrincipal()} is authenticated, {@code false} otherwise
|
||||
*/
|
||||
public boolean isPrincipalAuthenticated() {
|
||||
return !AnonymousAuthenticationToken.class.isAssignableFrom(this.principal.getClass()) &&
|
||||
this.principal.isAuthenticated();
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ import org.springframework.security.core.Authentication;
|
|||
|
||||
import cn.topiam.employee.application.jwt.model.JwtProtocolConfig;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
|
@ -39,25 +41,21 @@ public class JwtRequestAuthenticationToken extends AbstractAuthenticationToken {
|
|||
/**
|
||||
* 目标URL
|
||||
*/
|
||||
@Getter
|
||||
private String targetUrl;
|
||||
|
||||
/**
|
||||
* 协议配置
|
||||
*/
|
||||
@Getter
|
||||
private final JwtProtocolConfig config;
|
||||
|
||||
/**
|
||||
* 额外参数
|
||||
*/
|
||||
@Getter
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
public JwtRequestAuthenticationToken(Authentication principal, JwtProtocolConfig config,
|
||||
Map<String, Object> additionalParameters) {
|
||||
super(new ArrayList<>());
|
||||
this.principal = principal;
|
||||
this.config = config;
|
||||
this.additionalParameters = additionalParameters;
|
||||
}
|
||||
|
||||
public JwtRequestAuthenticationToken(Authentication principal, String targetUrl,
|
||||
JwtProtocolConfig config,
|
||||
|
@ -99,19 +97,8 @@ public class JwtRequestAuthenticationToken extends AbstractAuthenticationToken {
|
|||
return principal;
|
||||
}
|
||||
|
||||
public JwtProtocolConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setTargetUrl(String targetUrl) {
|
||||
this.targetUrl = targetUrl;
|
||||
}
|
||||
|
||||
public String getTargetUrl() {
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return additionalParameters;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import cn.topiam.employee.protocol.jwt.token.IdToken;
|
|||
import cn.topiam.employee.protocol.jwt.token.IdTokenContext;
|
||||
import cn.topiam.employee.protocol.jwt.token.IdTokenGenerator;
|
||||
import cn.topiam.employee.protocol.jwt.token.JwtIdTokenGenerator;
|
||||
import cn.topiam.employee.support.security.authentication.WebAuthenticationDetails;
|
||||
import cn.topiam.employee.support.security.userdetails.UserDetails;
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE_VARIABLE;
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.JwtEndpointConstants.JWT_SSO_PATH;
|
||||
|
@ -80,11 +81,12 @@ public final class JwtRequestAuthenticationTokenProvider implements Authenticati
|
|||
JwtProtocolConfig config = requestAuthenticationToken.getConfig();
|
||||
String issuer = ServerHelp.getPortalPublicBaseUrl() + JWT_SSO_PATH.replace(APP_CODE_VARIABLE, config.getAppCode());
|
||||
String subject = getSubject(config,(UserDetails) principal.getPrincipal());
|
||||
|
||||
WebAuthenticationDetails details = (WebAuthenticationDetails) requestAuthenticationToken.getDetails();
|
||||
IdTokenContext tokenContext = IdTokenContext.builder()
|
||||
.issuer(issuer)
|
||||
.subject(subject)
|
||||
.audience(config.getAppCode())
|
||||
.sessionId(details.getSessionId())
|
||||
.idTokenTimeToLive(config.getIdTokenTimeToLive())
|
||||
.privateKey(config.getJwtPrivateKey())
|
||||
.build();
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/9/4 16:11
|
||||
*/
|
||||
public final class OidcLogoutAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
JwtLogoutAuthenticationToken logoutAuthenticationToken= (JwtLogoutAuthenticationToken) authentication;
|
||||
SessionInformation sessionInformation=sessionRegistry.getSessionInformation(logoutAuthenticationToken.getSessionId());
|
||||
if (sessionInformation.isExpired()){
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return JwtLogoutAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
public OidcLogoutAuthenticationProvider(SessionRegistry sessionRegistry) {
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
}
|
|
@ -17,5 +17,37 @@
|
|||
*/
|
||||
package cn.topiam.employee.protocol.jwt.authorization;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationToken;
|
||||
|
||||
/**
|
||||
* 内存
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn
|
||||
*/
|
||||
public class InMemoryJwtAuthorizationService implements JwtAuthorizationService {
|
||||
/**
|
||||
* save
|
||||
*
|
||||
* @param token {@link JwtAuthenticationToken}
|
||||
*/
|
||||
@Override
|
||||
public void save(JwtAuthenticationToken token) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(JwtAuthenticationToken authorization) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtAuthenticationToken findById(String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtAuthenticationToken findByToken(String token) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,25 @@
|
|||
*/
|
||||
package cn.topiam.employee.protocol.jwt.authorization;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationToken;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/7/8 00:23
|
||||
*/
|
||||
public interface JwtAuthorizationService {
|
||||
|
||||
void save(JwtAuthenticationToken token);
|
||||
|
||||
void remove(JwtAuthenticationToken authorization);
|
||||
|
||||
@Nullable
|
||||
JwtAuthenticationToken findById(String id);
|
||||
|
||||
@Nullable
|
||||
JwtAuthenticationToken findByToken(String token);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* eiam-protocol-jwt - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.authorization;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.http.converter.json.SpringHandlerInstantiator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationToken;
|
||||
import cn.topiam.employee.protocol.jwt.jackson.JwtAuthorizationModule;
|
||||
import cn.topiam.employee.support.jackjson.SupportJackson2Module;
|
||||
|
||||
import lombok.Setter;
|
||||
import static cn.topiam.employee.protocol.jwt.constant.JwtProtocolConstants.JWT_PROTOCOL_CACHE_PREFIX;
|
||||
|
||||
/**
|
||||
* redis
|
||||
*
|
||||
* @author TopIAM
|
||||
*
|
||||
* Created by support@topiam.cn / 2689170096@qq.com on 2023/9/1 12:51
|
||||
*/
|
||||
public class RedisJwtAuthorizationService implements JwtAuthorizationService {
|
||||
private static final String CID_TO_AUTHORIZATIONS = "cid_to_authorizations:";
|
||||
private static final String UID_TO_AUTHORIZATIONS = "uid_to_authorizations:";
|
||||
private static final String ID_TO_AUTHORIZATION = "id_to_authorization:";
|
||||
private static final String ID_TO_CORRELATIONS = "id_to_correlations:";
|
||||
|
||||
public RedisJwtAuthorizationService(RedisOperations<String, String> redisOperations,
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
Assert.notNull(redisOperations, "redisOperations mut not be null");
|
||||
this.redisOperations = redisOperations;
|
||||
ClassLoader classLoader = this.getClass().getClassLoader();
|
||||
objectMapper.registerModules(SupportJackson2Module.getModules(classLoader));
|
||||
objectMapper.registerModule(new JwtAuthorizationModule());
|
||||
objectMapper.setHandlerInstantiator(new SpringHandlerInstantiator(beanFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* save
|
||||
*
|
||||
* @param token {@link JwtAuthenticationToken}
|
||||
*/
|
||||
@Override
|
||||
public void save(JwtAuthenticationToken token) {
|
||||
String authorizationId = token.getId();
|
||||
String uidToAuthorizationsKey = getIdTokenToAuthorization(authorizationId);
|
||||
String idToAuthorizationKey = getIdToAuthorizationKey(authorizationId);
|
||||
String idToCorrelationsKey = getIdToCorrelations(authorizationId);
|
||||
String cidToAuthorizationsKey = getCidToAuthorizations(token.getConfig().getClientId());
|
||||
//过期时间
|
||||
Duration timeToLive = Duration.of(token.getConfig().getIdTokenTimeToLive(),
|
||||
ChronoUnit.SECONDS);
|
||||
Set<String> correlationValues = new HashSet<>();
|
||||
//add client authorizations
|
||||
correlationValues.add(cidToAuthorizationsKey);
|
||||
redisOperations.opsForSet().add(cidToAuthorizationsKey, authorizationId);
|
||||
redisOperations.expire(cidToAuthorizationsKey, timeToLive);
|
||||
//save authorization
|
||||
correlationValues.add(idToAuthorizationKey);
|
||||
redisOperations.opsForValue().set(idToAuthorizationKey, write(token));
|
||||
redisOperations.expire(idToAuthorizationKey, timeToLive);
|
||||
//save id_token
|
||||
correlationValues.add(uidToAuthorizationsKey);
|
||||
redisOperations.opsForValue().set(uidToAuthorizationsKey,
|
||||
token.getIdToken().getTokenValue());
|
||||
redisOperations.expire(uidToAuthorizationsKey, timeToLive);
|
||||
//save correlations
|
||||
correlationValues.add(idToCorrelationsKey);
|
||||
redisOperations.opsForSet().add(idToCorrelationsKey,
|
||||
correlationValues.toArray(String[]::new));
|
||||
redisOperations.expire(idToCorrelationsKey, timeToLive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(JwtAuthenticationToken authorization) {
|
||||
String authorizationId = authorization.getId();
|
||||
String uidToAuthorizationsKey = getIdTokenToAuthorization(authorizationId);
|
||||
String idToAuthorizationKey = getIdToAuthorizationKey(authorizationId);
|
||||
String idToCorrelationsKey = getIdToCorrelations(authorizationId);
|
||||
String cidToAuthorizationsKey = getCidToAuthorizations(
|
||||
authorization.getConfig().getClientId());
|
||||
redisOperations.delete(idToAuthorizationKey);
|
||||
redisOperations.delete(idToCorrelationsKey);
|
||||
redisOperations.delete(uidToAuthorizationsKey);
|
||||
redisOperations.opsForSet().remove(cidToAuthorizationsKey, authorization.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtAuthenticationToken findById(String id) {
|
||||
return Optional.ofNullable(redisOperations.opsForValue().get(getIdToAuthorizationKey(id)))
|
||||
.map(this::parse).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtAuthenticationToken findByToken(String token) {
|
||||
return Optional
|
||||
.ofNullable(redisOperations.opsForValue().get(getIdTokenToAuthorization(token)))
|
||||
.map(this::parse).orElse(null);
|
||||
}
|
||||
|
||||
private String getIdToCorrelations(String authorizationId) {
|
||||
return prefix + ID_TO_CORRELATIONS + authorizationId;
|
||||
}
|
||||
|
||||
private String write(Object data) {
|
||||
try {
|
||||
return this.objectMapper.writeValueAsString(data);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private JwtAuthenticationToken parse(String data) {
|
||||
try {
|
||||
return this.objectMapper.readValue(data, new TypeReference<>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCidToAuthorizations(String clientId) {
|
||||
return prefix + CID_TO_AUTHORIZATIONS + clientId;
|
||||
}
|
||||
|
||||
private String getIdToAuthorizationKey(String authorizationId) {
|
||||
return prefix + ID_TO_AUTHORIZATION + authorizationId;
|
||||
}
|
||||
|
||||
private String getIdTokenToAuthorization(String idToken) {
|
||||
return prefix + UID_TO_AUTHORIZATIONS + generateKey(idToken);
|
||||
}
|
||||
|
||||
protected static String generateKey(String rawKey) {
|
||||
byte[] bytes = DIGEST.digest(rawKey.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format("%032x", new BigInteger(1, bytes));
|
||||
}
|
||||
|
||||
private final RedisOperations<String, String> redisOperations;
|
||||
|
||||
@Setter
|
||||
private String prefix = JWT_PROTOCOL_CACHE_PREFIX;
|
||||
|
||||
@Setter
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final MessageDigest DIGEST;
|
||||
|
||||
static {
|
||||
try {
|
||||
DIGEST = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import cn.topiam.employee.protocol.code.configurer.AbstractConfigurer;
|
||||
import cn.topiam.employee.protocol.code.util.ProtocolConfigUtils;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtRequestAuthenticationTokenProvider;
|
||||
import cn.topiam.employee.protocol.jwt.authorization.JwtAuthorizationService;
|
||||
import cn.topiam.employee.protocol.jwt.endpoint.JwtAuthenticationEndpointFilter;
|
||||
|
@ -73,9 +74,11 @@ public class JwtAuthorizationEndpointConfigurer extends AbstractConfigurer {
|
|||
.getSharedObject(AuthenticationManager.class);
|
||||
JwtAuthorizationService authorizationService = JwtAuthenticationUtils
|
||||
.getAuthorizationService(httpSecurity);
|
||||
JwtAuthenticationEndpointFilter initSingleSignOnEndpointFilter = new JwtAuthenticationEndpointFilter(
|
||||
JwtAuthenticationEndpointFilter jwtAuthenticationEndpointFilter = new JwtAuthenticationEndpointFilter(
|
||||
requestMatcher, authenticationManager, authorizationService);
|
||||
httpSecurity.addFilterBefore(postProcess(initSingleSignOnEndpointFilter),
|
||||
jwtAuthenticationEndpointFilter.setAuthenticationDetailsSource(
|
||||
ProtocolConfigUtils.getAuthenticationDetailsSource(httpSecurity));
|
||||
httpSecurity.addFilterBefore(postProcess(jwtAuthenticationEndpointFilter),
|
||||
AuthorizationFilter.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
*/
|
||||
package cn.topiam.employee.protocol.jwt.constant;
|
||||
|
||||
import static cn.topiam.employee.common.constant.AuthorizeConstants.AUTHORIZE_PATH;
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE_VARIABLE;
|
||||
import static cn.topiam.employee.protocol.code.constant.ProtocolConstants.PROTOCOL_CACHE_PREFIX;
|
||||
import static cn.topiam.employee.support.constant.EiamConstants.COLON;
|
||||
|
||||
/**
|
||||
* 协议常量
|
||||
|
@ -29,10 +29,9 @@ import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE_VARI
|
|||
public class JwtProtocolConstants {
|
||||
|
||||
/**
|
||||
* JWT IDP SSO 发起
|
||||
* 协议缓存前缀
|
||||
*/
|
||||
public static final String IDP_JWT_SSO_INITIATOR = AUTHORIZE_PATH + "/jwt/" + APP_CODE_VARIABLE
|
||||
+ "/initiator";
|
||||
public static final String JWT_PROTOCOL_CACHE_PREFIX = PROTOCOL_CACHE_PREFIX + "jwt" + COLON;
|
||||
|
||||
public static final String TARGET_URL = "target_url";
|
||||
public static final String ID_TOKEN = "id_token";
|
||||
|
@ -43,4 +42,6 @@ public class JwtProtocolConstants {
|
|||
|
||||
public static final String JWT_ERROR_URI = "https://eiam.topiam.cn";
|
||||
|
||||
public static final String S_ID = "sid";
|
||||
|
||||
}
|
||||
|
|
|
@ -46,8 +46,6 @@ import jakarta.servlet.FilterChain;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.GRANT_TYPE;
|
||||
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE;
|
||||
import static cn.topiam.employee.support.util.HttpRequestUtils.getRequestHeaders;
|
||||
|
||||
|
@ -85,7 +83,6 @@ public final class JwtAuthorizationServerContextFilter extends OncePerRequestFil
|
|||
return;
|
||||
}
|
||||
try {
|
||||
request.getParameterValues(GRANT_TYPE);
|
||||
//@formatter:off
|
||||
Map<String, String> variables = matcher.getVariables();
|
||||
String appCode = variables.get(APP_CODE);
|
||||
|
|
|
@ -24,15 +24,11 @@ import org.apache.commons.compress.utils.CharsetNames;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
@ -44,6 +40,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||
|
||||
import cn.topiam.employee.application.jwt.model.JwtProtocolConfig;
|
||||
import cn.topiam.employee.protocol.code.exception.TemplateNotExistException;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationFailureHandler;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationToken;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtRequestAuthenticationToken;
|
||||
import cn.topiam.employee.protocol.jwt.authorization.JwtAuthorizationService;
|
||||
|
@ -51,7 +48,6 @@ import cn.topiam.employee.protocol.jwt.endpoint.authentication.JwtRequestAuthent
|
|||
import cn.topiam.employee.protocol.jwt.exception.JwtAuthenticationException;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtError;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtErrorCodes;
|
||||
import cn.topiam.employee.protocol.jwt.http.converter.JwtErrorHttpMessageConverter;
|
||||
import cn.topiam.employee.protocol.jwt.token.IdToken;
|
||||
|
||||
import freemarker.cache.ClassTemplateLoader;
|
||||
|
@ -62,6 +58,7 @@ import jakarta.servlet.ServletException;
|
|||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import static cn.topiam.employee.protocol.jwt.constant.JwtProtocolConstants.*;
|
||||
import static cn.topiam.employee.protocol.jwt.endpoint.JwtAuthenticationEndpointUtils.throwError;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
|
@ -89,11 +86,6 @@ public final class JwtAuthenticationEndpointFilter extends OncePerRequestFilter
|
|||
*/
|
||||
private final JwtAuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* 错误响应处理器
|
||||
*/
|
||||
private final HttpMessageConverter<JwtError> errorHttpResponseConverter = new JwtErrorHttpMessageConverter();
|
||||
|
||||
/**
|
||||
* 授权端点匹配器
|
||||
*/
|
||||
|
@ -107,7 +99,7 @@ public final class JwtAuthenticationEndpointFilter extends OncePerRequestFilter
|
|||
/**
|
||||
* 身份验证失败处理程序
|
||||
*/
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = new JwtAuthenticationFailureHandler();
|
||||
|
||||
/**
|
||||
* 会话身份策略
|
||||
|
@ -198,6 +190,13 @@ public final class JwtAuthenticationEndpointFilter extends OncePerRequestFilter
|
|||
LogMessage.format("Authorization request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
} catch (Exception ex) {
|
||||
JwtError error = new JwtError(JwtErrorCodes.SERVER_ERROR,ex.getMessage(),JWT_ERROR_URI);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(error, ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new JwtAuthenticationException(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,36 +228,15 @@ public final class JwtAuthenticationEndpointFilter extends OncePerRequestFilter
|
|||
data.put(ID_TOKEN, idToken.getTokenValue());
|
||||
data.put(TARGET_URL, targetUri);
|
||||
template.process(data, response.getWriter());
|
||||
|
||||
//save
|
||||
authorizationService.save(authenticationToken);
|
||||
} catch (Exception e) {
|
||||
JwtError error = new JwtError(JwtErrorCodes.SERVER_ERROR,e.getMessage(),JWT_ERROR_URI);
|
||||
throw new JwtAuthenticationException(error);
|
||||
throwError(error);
|
||||
}
|
||||
//@formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送异常响应
|
||||
*
|
||||
* @param request {@link HttpServletRequest}
|
||||
* @param response {@link HttpServletResponse}
|
||||
* @param exception {@link AuthenticationException}
|
||||
*/
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException {
|
||||
if (exception instanceof JwtAuthenticationException) {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
JwtError error = ((JwtAuthenticationException) exception).getError();
|
||||
if (error.getErrorCode().equals(JwtErrorCodes.SERVER_ERROR)) {
|
||||
httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
if (error.getErrorCode().equals(JwtErrorCodes.INVALID_REQUEST)) {
|
||||
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private void configFreemarkerTemplate() {
|
||||
try {
|
||||
//模板存放路径
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package cn.topiam.employee.protocol.jwt.endpoint;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtAuthenticationException;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtError;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/9/4 13:05
|
||||
*/
|
||||
public class JwtAuthenticationEndpointUtils {
|
||||
|
||||
public static void throwError(JwtError jwtError) {
|
||||
throw new JwtAuthenticationException(jwtError);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* eiam-protocol-jwt - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.endpoint;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationFailureHandler;
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtLogoutAuthenticationToken;
|
||||
import cn.topiam.employee.protocol.jwt.endpoint.authentication.JwtLogoutAuthenticationConverter;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtAuthenticationException;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtError;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtErrorCodes;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import static cn.topiam.employee.protocol.jwt.constant.JwtProtocolConstants.JWT_ERROR_URI;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/9/4 20:14
|
||||
*/
|
||||
public final class JwtLogoutAuthenticationEndpointFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* 端点匹配器
|
||||
*/
|
||||
private final RequestMatcher requestMatcher;
|
||||
|
||||
/**
|
||||
* 身份验证失败处理程序
|
||||
*/
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = new JwtAuthenticationFailureHandler();
|
||||
|
||||
/**
|
||||
* AuthenticationSuccessHandler
|
||||
*/
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler=this::sendAuthorizationResponse;
|
||||
|
||||
/**
|
||||
* LogoutHandler
|
||||
*/
|
||||
private final LogoutHandler logoutHandler;
|
||||
|
||||
/**
|
||||
* 认证转换器
|
||||
*/
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
|
||||
/**
|
||||
* AuthenticationDetailsSource
|
||||
*/
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
|
||||
public JwtLogoutAuthenticationEndpointFilter(RequestMatcher requestMatcher,
|
||||
SessionRegistry sessionRegistry, AuthenticationManager authenticationManager) {
|
||||
Assert.notNull(requestMatcher, "requestMatcher cannot be empty");
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be empty");
|
||||
Assert.notNull(sessionRegistry, "authenticationManager cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.logoutHandler = new SecurityContextLogoutHandler();
|
||||
this.requestMatcher = requestMatcher;
|
||||
authenticationConverter=new JwtLogoutAuthenticationConverter();
|
||||
}
|
||||
|
||||
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
|
||||
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
|
||||
this.authenticationFailureHandler = authenticationFailureHandler;
|
||||
}
|
||||
|
||||
public void setAuthenticationFailureHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationFailureHandler, "authenticationSuccessHandler cannot be null");
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}.
|
||||
*
|
||||
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}
|
||||
*/
|
||||
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
|
||||
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
|
||||
this.authenticationDetailsSource = authenticationDetailsSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same contract as for {@code doFilter}, but guaranteed to be
|
||||
* just invoked once per request within a single request thread.
|
||||
* See {@link #shouldNotFilterAsyncDispatch()} for details.
|
||||
* <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
|
||||
* default ServletRequest and ServletResponse ones.
|
||||
*
|
||||
* @param request {@link HttpServletRequest}
|
||||
* @param response {@link HttpServletResponse}
|
||||
* @param filterChain {@link FilterChain}
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(@NotNull HttpServletRequest request,
|
||||
@NotNull HttpServletResponse response,
|
||||
@NotNull FilterChain filterChain) throws ServletException,
|
||||
IOException {
|
||||
if (!requestMatcher.matches(request)) {
|
||||
doFilter(request, response, filterChain);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Authentication authentication= authenticationConverter.convert(request);
|
||||
if (authentication instanceof AbstractAuthenticationToken) {
|
||||
((AbstractAuthenticationToken) authentication)
|
||||
.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
}
|
||||
Authentication authenticationResult= authenticationManager.authenticate(authentication);
|
||||
authenticationSuccessHandler.onAuthenticationSuccess(request,response,authenticationResult);
|
||||
} catch (JwtAuthenticationException ex) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("JWT logout request failed: %s", ex.getError()),
|
||||
ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
} catch (Exception ex) {
|
||||
JwtError error = new JwtError(JwtErrorCodes.SERVER_ERROR,ex.getMessage(),JWT_ERROR_URI);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(error, ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new JwtAuthenticationException(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送成功响应
|
||||
*
|
||||
* @param request {@link HttpServletRequest}
|
||||
* @param response {@link HttpServletResponse}
|
||||
* @param authentication {@link Authentication}
|
||||
*/
|
||||
private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
JwtLogoutAuthenticationToken jwtLogoutAuthentication= (JwtLogoutAuthenticationToken) authentication;
|
||||
// Check for active user session
|
||||
if (jwtLogoutAuthentication.isPrincipalAuthenticated() &&
|
||||
StringUtils.hasText(jwtLogoutAuthentication.getSessionId())) {
|
||||
// Perform logout
|
||||
this.logoutHandler.logout(request, response,
|
||||
(Authentication) jwtLogoutAuthentication.getPrincipal());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.endpoint.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtLogoutAuthenticationToken;
|
||||
import cn.topiam.employee.protocol.jwt.exception.JwtError;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import static cn.topiam.employee.protocol.jwt.constant.JwtProtocolConstants.S_ID;
|
||||
import static cn.topiam.employee.protocol.jwt.endpoint.JwtAuthenticationEndpointUtils.throwError;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author SanLi
|
||||
* Created by qinggang.zuo@gmail.com / 2689170096@qq.com on 2023/9/4 16:01
|
||||
*/
|
||||
public final class JwtLogoutAuthenticationConverter implements AuthenticationConverter {
|
||||
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
|
||||
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
if (request.getParameterValues(S_ID).length != 1) {
|
||||
throwError(new JwtError(OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"JWT Logout Request Parameter: " + S_ID));
|
||||
}
|
||||
|
||||
String sessionId = request.getParameter(S_ID);
|
||||
if (!StringUtils.hasText(sessionId)) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
sessionId = session.getId();
|
||||
}
|
||||
}
|
||||
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (principal == null) {
|
||||
principal = ANONYMOUS_AUTHENTICATION;
|
||||
}
|
||||
|
||||
return new JwtLogoutAuthenticationToken(principal,sessionId);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* eiam-protocol-jwt - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.*;
|
||||
|
||||
/**
|
||||
* JwtAuthenticationTokenMixin
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/6/30 21:07
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract class JwtAuthenticationTokenMixin {
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* eiam-protocol-jwt - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.jwt.jackson;
|
||||
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
import cn.topiam.employee.protocol.jwt.authentication.JwtAuthenticationToken;
|
||||
|
||||
/**
|
||||
* JwtAuthorizationModule
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/6/30 21:07
|
||||
*/
|
||||
public class JwtAuthorizationModule extends SimpleModule {
|
||||
|
||||
public JwtAuthorizationModule() {
|
||||
super(JwtAuthorizationModule.class.getName(), new Version(1, 0, 0, null, null, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupModule(SetupContext context) {
|
||||
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
|
||||
context.setMixInAnnotations(JwtAuthenticationToken.class,
|
||||
JwtAuthenticationTokenMixin.class);
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
|||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -31,6 +32,7 @@ import lombok.NonNull;
|
|||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Jacksonized
|
||||
public class IdToken {
|
||||
|
||||
@NonNull
|
||||
|
|
|
@ -38,6 +38,9 @@ public class IdTokenContext {
|
|||
@NonNull
|
||||
private String audience;
|
||||
|
||||
@NonNull
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* Token 过期时间(秒)
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,7 @@ import cn.topiam.employee.protocol.jwt.exception.IdTokenGenerateException;
|
|||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import static cn.topiam.employee.protocol.jwt.constant.JwtProtocolConstants.S_ID;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -50,6 +51,7 @@ public class JwtIdTokenGenerator implements IdTokenGenerator {
|
|||
.setAudience(context.getAudience())
|
||||
.setExpiration(new Date(expiresAt.toEpochMilli()))
|
||||
.signWith(rsaPrivateKey, SignatureAlgorithm.RS256)
|
||||
.claim(S_ID,context.getSessionId())
|
||||
.compact();
|
||||
return IdToken.builder().tokenValue(tokenValue)
|
||||
.issuedAt(issuedAt)
|
||||
|
|
|
@ -50,7 +50,6 @@ import jakarta.servlet.FilterChain;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.GRANT_TYPE;
|
||||
import static org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT;
|
||||
|
||||
import static cn.topiam.employee.common.constant.ProtocolConstants.APP_CODE;
|
||||
|
@ -91,7 +90,6 @@ public final class OidcAuthorizationServerContextFilter extends OncePerRequestFi
|
|||
return;
|
||||
}
|
||||
try {
|
||||
request.getParameterValues(GRANT_TYPE);
|
||||
//@formatter:off
|
||||
Map<String, String> variables = matcher.getVariables();
|
||||
String appCode = variables.get(APP_CODE);
|
||||
|
|
Loading…
Reference in New Issue