Page:
OAuth配置
Table of Contents
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
OAuth
概述
实现了完整的 OAuth 2.0 授权框架,支持双重角色:
- OAuth 提供方(Provider):系统作为授权服务器,为第三方应用提供用户授权
- OAuth 应用方(Application):系统作为客户端,接入第三方 OAuth 提供商(如 GitHub、Gitea 等)
主要特性
- 完整的 OAuth 2.0 Authorization Code Flow 实现
- 符合 RFC 6749 标准规范
- 支持 Access Token 和 Refresh Token
- Token 撤销机制(RFC 7009)
- 用户授权管理
- 可扩展的第三方平台接入
- 完善的安全校验机制
在项目中接入OAuth
新增OAuth需要的namespace
- 在web下新增namespace:oauth
- 文件列表如下:
---web/src/app/namespaces
-->-oauth
-->------index.json
-->------index.module.less
-->------index.tsx
- index.json:
{
"path": "/oauth",
"first": "/common/notAuthorized",
"notFound": "/common/notFound"
}
- index.module.less
.mixPanel {
height: 100% !important;
background: var(--oak-bg-color-page) !important;
min-height: unset !important;
}
.mixMain {
background: unset !important;
flex-direction: row !important;
flex: 1 !important;
}
.mixLayout {
overflow: auto !important;
min-height: unset !important;
background: unset !important;
}
.mixContent {
background: unset !important;
min-height: unset !important;
min-width: 792px;
}
.mixContent_xs {
background: unset !important;
min-height: unset !important;
min-width: unset;
}
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loadingBox {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
}
- index.tsx
import React, { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import Styles from './index.module.less';
import useFeatures from '@project/hooks/useFeatures';
import ErrorPage from '@project/components/common/errorPage';
import { ECode } from '@project/types/ErrorPage';
import Spin from '@project/components/common/spin';
import {
OakTokenExpiredException,
OakUserInfoLoadingException,
} from 'oak-general-business';
import { OakUnloggedInException } from 'oak-domain/lib/types/Exception';
type Status = 'waiting' | 'success' | 'fail';
function Frontend(props: { namespace: string }) {
const { namespace } = props;
const features = useFeatures();
features.navigator.setNamespace(namespace);
const [status, setStatus] = useState<Status>('waiting');
// const [needSet, setNeedSet] = useState(false);
const init = async () => {
try {
const id = features.token.getUserId(true);
if (id) {
const userInfo = features.token.getUserInfo();
const hasLoginName = userInfo?.loginName$user?.length! > 0;
const hasPassword = userInfo?.hasPassword;
if (!hasLoginName || !hasPassword) {
setStatus('success');
return
}
}
setStatus('success');
} catch (err) {
if (err instanceof OakUserInfoLoadingException) {
setStatus('waiting');
return;
}
if (
err instanceof OakUnloggedInException ||
err instanceof OakTokenExpiredException
) {
setStatus('success');
return;
}
setStatus('fail');
}
};
useEffect(() => {
init();
const unsubscribe = features.token.subscribe(() => {
init();
});
return () => {
unsubscribe();
};
}, []);
if (status === 'waiting') {
return (
<div className={Styles.loadingBox}>
<Spin tip="Loading" />
</div>
);
}
if (status === 'fail') {
return (
<>
<ErrorPage code={ECode.error} title='页面加载错误' desc={'抱歉,页面加载出现错误!请稍后再试。'}>
</ErrorPage>
</>
);
}
return (
<Outlet />
)
}
export default Frontend;
在pages下新增oauth需要的页面
- pages/oauth (这里的oauth对应上面json中的path)
-> authorize (认证页面组件,作为认证服务时使用,下方有示例)
-> callback (回调页面组件,作为认证客户端时,这个页面作为回调页面,下方有示例)
-> common (普通页面组件,例如notFound,notAuthorized,可自定义编写,上方json内使用到这里的页面)
组件示例:
oauth/authorize:
- locales/zh_CN.json
{
"oauth":{
"authorize": {
"loginRequired": "请先登录您的账号"
}
}
}
- index.ts
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties: {},
data: {
userId: '',
dispose: null as (() => void) | null,
},
methods: {
async toLogin() {
this.features.navigator.navigateTo({
url: '/login'
}, undefined, true)
}
},
lifetimes: {
async ready() {
// 这里检查用户是否已经登录,因为作为oauth服务时,必须是通过一个已经登录的用户进行授权
this.setState({
userId: this.features.token.getUserId(true) || '',
dispose: this.features.token.subscribe(() => {
this.setState({
userId: this.features.token.getUserId(true) || '',
});
})
})
},
async detached() {
this.state.dispose && this.state.dispose();
},
}
});
- styles.module.less
.root {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
min-height: 100vh; /* 使页面高度占满视口 */
background: #f5f7fa; /* 可根据主题调整 */
}
.title {
margin: 12px 0 18px;
font-size: 20px;
font-weight: 600;
text-align: center;
color: #222;
}
.container {
width: 100%;
max-width: 480px;
min-height: 220px;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
}
- web.pc.tsx
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
import Auth from "oak-general-business/es/components/login/oauth/authorize"
import Login from '@project/components/login';
const Authorize = (
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
// virtual
userId: string;
},
{
toLogin: () => Promise<void>;
}
>
) => {
const { userId } = props.data;
const { toLogin, t } = props.methods;
if (!userId) { // 如果未登录则显示登录页,通过监听token,在登录之后会显示认证页
return (
<div className={Styles.root}>
<div className={Styles.title}>
{t('oauth.authorize.loginRequired')}
</div>
<div className={Styles.container}>
<Login // 这里可以自定义登录页的样式,或者直接使用下面公共登录页
oakPath={`#login-oauth-authorize`}
/>
</div>
</div>
)
}
return <Auth
oakPath={`#Authorize`}
onUnLogin={toLogin}
/>;
};
export default Authorize;
oauth/callback:
- locales/zh_CN.json
{
}
- index.ts
export default OakComponent({
// Virtual Component
isList: false,
filters: [],
properties: {},
methods: {
// 认证失败时,用户点击重试,会调用该回调
async handleRetry() {
this.features.navigator.redirectTo({
url: '/login'
}, undefined, true)
},
// 认证成功后,用户点击完成按钮,会调用该回调
async handleSuccess() {
this.features.navigator.redirectTo({
url: '/'
}, undefined, true)
}
}
});
- styles.module.less
.id {
font-size: 18px;
}
.item {
font-size: 18px;
}
- web.pc.tsx
import React from 'react';
import { EntityDict } from '@project/oak-app-domain';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import Styles from './styles.module.less';
import OAuth from "oak-general-business/es/components/oauth"
const Oauth = (
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
// virtual
},
{
handleRetry: () => void;
handleSuccess: () => void;
}
>
) => {
const { oakFullpath } = props.data;
const { handleRetry, handleSuccess } = props.methods;
return <OAuth
oakPath={`#OAuth`}
onRetry={ handleRetry }
onSuccess={ handleSuccess }
></OAuth>
};
export default Oauth;
配置OAuth
首先在作为认证服务的供应商,创建一个oauth客户端(以oak为例)
- 打开systemPanel(系统设置页面)
- 左侧边栏选择OAuth管理
- tab页选择OAuth应用
- 点击创建
- 名称描述随意
- logo随意
- 重定向URL列表,也就是客户端的callback页面的位置,例如: http://localhost:3000/oauth/callback
- 权限范围: 可为空,现未做判断
- 客户端ID 需要记下
- 机密客户端(启用)
- 强制PKCE(不启用)
- 创建完成
- 点击更新,以查看客户端密钥,并记录
- 点击创建
然后配置需要使用第三方登录的应用的配置(以oak为例)
- 在systempanel-oauth管理-oauth供应商中
- 添加新供应商
- 名称随意
- 类型可自定义,也可以选择已有(自定义需要注册用户信息处理器,否则无法同步用户信息)
- logo随意
- 授权端点:作为认证服务的应用的authorize页面路径,如 http://localhost:3000/oauth/authorize
- 令牌端点:认证服务的获取用户令牌的api地址,如:https://test.com/oak-api/endpoint/oauth/access_token
- 刷新端点:认证服务的刷新访问token的api地址,如:https://test.com/oak-api/endpoint/oauth/token
- 用户信息端点:认证服务上获取用户信息的api地址,如:https://test.com/oak-api/endpoint/oauth/userinfo
- 撤销端点:认证服务的撤销用户授权的api地址,如:https://test.com/oak-api/endpoint/oauth/revoke (可以为空)
- 客户端ID:从认证服务申请的客户端ID
- 客户端密钥:从认证服务申请的客户端密钥
- 权限范围:当前应用允许使用的权限范围(第三方服务可能需要)
- 重定向URL:当前应用的oauth回调页面地址,如http://localhost:3000/oauth/callback,必须和供应商处的重定向URL配置一致,否则校验不通过
- 自动注册:若开启,则会自动创建新账户,若关闭则在用户不存在的情况下会抛出请先注册的异常