优化代码

pull/42/head
smallbun 2023-09-05 19:56:06 +08:00
parent 9727f14278
commit 8a97cd7355
92 changed files with 1421 additions and 396 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) },

View File

@ -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) },

View File

@ -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';

View File

@ -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,

View File

@ -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>
);
};

View File

@ -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,

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import CreateUser from './CreateUser';
export default CreateUser;

View File

@ -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(() => {

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import UpdateUser from './UpdateUser';
export default UpdateUser;

View File

@ -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';

View File

@ -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';
/**
*
*

View File

@ -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';

View File

@ -18,6 +18,7 @@
import { SsoScope } from '@/pages/app/AppConfig/constant';
import { ProFormSelect } from '@ant-design/pro-components';
import { useIntl } from '@@/exports';
/**
*
*

View File

@ -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,

View File

@ -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}

View File

@ -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(
intl.formatMessage({
id: 'pages.app.config.items.login_access.protocol_config.oidc.redirect_uris.rule.0.message',
}),
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}
<DeleteOutlined onClick={() => remove(field.name)} />
</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}
<DeleteOutlined onClick={() => remove(field.name)} />
</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()}

View File

@ -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';

View File

@ -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',
});
}

View File

@ -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';

View File

@ -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',
});
}

View File

@ -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;

View File

@ -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 {

View File

@ -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: {

View File

@ -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) },

View File

@ -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';

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import CreateAdministrator from './CreateAdministrator';
export default CreateAdministrator;

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import ResetAdministratorPassword from './ResetAdministratorPassword';
export default ResetAdministratorPassword;

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import UpdateAdministrator from './UpdateAdministrator';
export default UpdateAdministrator;

View File

@ -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;
}

View File

@ -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',
{
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
},
).then((result: API.ApiResult<SettingAPI.AdministratorList>) => {
const data: RequestData<SettingAPI.AdministratorList> = {
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<AdministratorList>) => {
const data: RequestData<AdministratorList> = {
data: result?.result?.list ? result?.result?.list : [],
success: result?.success,
total: result?.result?.pagination ? result?.result?.pagination.total : 0,

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Basic from './Basic';
export default Basic;

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import DefensePolicy from './DefensePolicy';
export default DefensePolicy;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;
}
/**
*
*/

View File

@ -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';

View File

@ -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');

View File

@ -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`,

View File

@ -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) },

View File

@ -423,18 +423,6 @@ declare namespace AppAPI {
remark: string;
};
/**
*
*/
export type ListTemplate = {
protocol: string;
type: string;
code: string;
icon: string;
name: string;
desc: string;
};
/**
*
*/

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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} />

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -16,4 +16,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Account from './Account';
export default Account;

View File

@ -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,55 +64,120 @@ const CardList = () => {
document.body.removeChild(div);
};
return (
<PageContainer content={content}>
<Alert message={intl.formatMessage({ id: 'pages.application.alert' })} showIcon />
<br />
<ProList<AppList>
rowKey="id"
ghost
grid={{
xs: 1,
sm: 2,
md: 2,
lg: 3,
xl: 4,
xxl: 5,
}}
request={queryAppList}
pagination={{}}
params={searchParams}
actionRef={actionRef}
renderItem={(item: AppList) => {
return (
item &&
item.id && (
<Card
style={{ margin: 5 }}
hoverable
onClick={async () => {
if (item.initLoginType === InitLoginType.PORTAL_OR_APP_INIT_SSO) {
initSso(item.initLoginUrl);
return;
}
message.warning(
`${item.name}${intl.formatMessage({ id: 'pages.application.init.warning' })}`,
);
}}
>
<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>
}
/>
</Card>
)
);
const renderBadge = (count: number, active = false) => {
return (
<Badge
count={count}
style={{
marginBlockStart: -2,
marginInlineStart: 4,
color: active ? '#1890FF' : '#999',
backgroundColor: active ? '#E6F7FF' : '#eee',
}}
/>
</PageContainer>
);
};
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"
split
grid={{
xs: 1,
sm: 2,
md: 2,
lg: 3,
xl: 4,
xxl: 5,
}}
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: 8 }}
className={`${prefixCls}-item-card`}
hoverable
bordered={false}
onClick={async () => {
if (item.initLoginType === InitLoginType.PORTAL_OR_APP_INIT_SSO) {
initSso(item.initLoginUrl);
return;
}
message.warning(
`${item.name}${intl.formatMessage({ id: 'pages.application.init.warning' })}`,
);
}}
>
<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 : <>&nbsp;</>}
</Paragraph>
</div>
</div>
</Card>
)
);
}}
/>
</PageContainer>
</div>
);
};

View File

@ -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) },

View File

@ -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;

View File

@ -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 {

View File

@ -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: {

View File

@ -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';

View File

@ -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';

View File

@ -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 }) => {

View File

@ -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();

View File

@ -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;

View File

@ -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 }) => {

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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,18 +29,19 @@ 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";
public static final String NONCE = "nonce";
public static final String TARGET_URL = "target_url";
public static final String ID_TOKEN = "id_token";
public static final String NONCE = "nonce";
public static final String URL = "url";
public static final String BINDING_TYPE = "binding_type";
public static final String URL = "url";
public static final String BINDING_TYPE = "binding_type";
public static final String JWT_ERROR_URI = "https://eiam.topiam.cn";
public static final String JWT_ERROR_URI = "https://eiam.topiam.cn";
public static final String S_ID = "sid";
}

View File

@ -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);

View File

@ -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 {
//模板存放路径

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -38,6 +38,9 @@ public class IdTokenContext {
@NonNull
private String audience;
@NonNull
private String sessionId;
/**
* Token
*/

View File

@ -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)

View File

@ -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);