Ory Kratos 為使用者認證與管理系統。本文將動手實現瀏覽器(React+AntD)的完整流程,實際瞭解下它的 API 。

瞭解 Kratos

獲取程式碼

git clone -b v0.7.0-alpha.1 --depth 1 https://github.com/ory/kratos.git

檢視 API

go-swagger 檢視:

cd kratos
swagger serve -F=swagger ./spec/swagger.json

執行服務

docker-compose 執行:

cd kratos
docker-compose -f quickstart.yml -f quickstart-postgres.yml -f quickstart-standalone.yml up --build --force-recreate
# If you have SELinux, run: -f quickstart-selinux.yml

運行了官方 Quickstart 例子,可以訪問 http://127.0.0.1:4455/dashboard 體驗。

檢視 DB

pgAdmin 開啟(DB 資訊見 quickstart-postgres.yml):

查看錶:

檢視配置

cd kratos
cat contrib/quickstart/kratos/email-password/kratos.yml

設定環境變數可以覆蓋。以 _ 表示層級,如 SELFSERVICE_FLOWS_SETTINGS_UI_URL=<value> 覆蓋 selfservice.flows.settings.ui_url

Self-Service 流程

瀏覽器流程

客戶端流程

動手配置:Kratos 服務

  • Ory Kratos

    • Public API (port 4433)
    • Admin API (port 4434)
    • Postgres DB (port 5432)
    • Browser Return URL (port 3000)
  • MailSlurper: a development SMTP server
    • Server UI (port 4436)

配置檔案

啟動檔案

執行服務

cd ory-kratos
docker-compose -f start.yml up --build --force-recreate

如果想執行官方 Self-Service UI 例子,那麼:

docker-compose -f start.yml -f start-ui-node.yml up --build --force-recreate

之後,訪問 http://127.0.0.1:3000/ 體驗。在 Register new account / Reset password 時,可訪問虛擬 SMTP 服務 http://127.0.0.1:4436 接收郵件。

動手實現:瀏覽器流程

React + Ant Design

新建 React 應用

yarn create react-app my-web --template typescript
cd my-web
yarn start

訪問 http://localhost:3000/ ,可見 React 歡迎頁。

引入 AntD

yarn add antd

修改 src/App.tsx,引入 antd 元件:

import React, { Component } from 'react'
import { Button } from 'antd';
import logo from './logo.svg';
import './App.css'; class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Button type="primary">Button</Button>
</header>
</div>
);
}
} export default App;

修改 src/App.css,引入 antd 樣式:

@import '~antd/dist/antd.css';

可見 antd 藍色按鈕元件。

引入 Sass

yarn add node-sass

字尾 css 改為 scsstsx 裡的 import 也改下。

引入 Router

yarn add react-router-dom @types/react-router-dom

pages 目錄下實現如下頁面 UI:

src/pages               功能        路由
├── dashboard.tsx 主頁 /, /dashboard
├── error.tsx 錯誤 /error
├── login.tsx 登入 /auth/login
├── recovery.tsx 恢復 /recovery
├── registration.tsx 註冊 /auth/registration
├── settings.tsx 設定 /settings
└── verification.tsx 驗證 /verify

引入 SDK

yarn add @ory/[email protected]

註冊

APIs:

  • GET /self-service/registration/browser: 初始化註冊流程
  • GET /self-service/registration/flows: 獲取註冊流程
  • POST /self-service/registration: 提交註冊流程

頁面載入後的處理流程:

componentDidMount() {
// 獲取 flow id 引數
const flowId = utils.parseUrlQuery("flow", this.props.location) as string; // 沒有 flow id,初始化註冊流程
if (!flowId || !utils.isString(flowId)) {
console.log("No flow ID found in URL, initializing registration flow.");
utils.redirectToSelfService("/self-service/registration/browser");
return;
} // 根據 flow id,獲取註冊流程資訊
authPublicApi
.getSelfServiceRegistrationFlow(flowId, undefined, {
withCredentials: true,
})
.then((res: AxiosResponse<SelfServiceRegistrationFlow>) => {
if (utils.assertResponse(res)) {
utils.redirectToSelfService("/self-service/registration/browser");
return;
}
this.setState({ flowId: flowId, flow: res.data });
})
.catch(utils.redirectOnError);
}

流程資訊 this.state.flow,如下:

{
"id": "74c643a1-f302-45c9-a760-1ad7b1157e1c",
"type": "browser",
"expires_at": "2021-07-20T05:22:30.958717Z",
"issued_at": "2021-07-20T05:12:30.958717Z",
"request_url": "http://127.0.0.1:4433/self-service/registration/browser",
"ui": {
"action": "http://127.0.0.1:4433/self-service/registration?flow=74c643a1-f302-45c9-a760-1ad7b1157e1c",
"method": "POST",
"nodes": [{
"type": "input",
"group": "default",
"attributes": {
"name": "csrf_token",
"type": "hidden",
"value": "QQyUDHa4KJ3M6mowHHN4pboN4iaUOZL+4gYVtKYRWzSdWjSNcW5dG/SNzocyqqqAtV48KzQVMIC6X+Pv3tNPNw==",
"required": true,
"disabled": false
},
"messages": [],
"meta": {}
}, {
"type": "input",
"group": "password",
"attributes": {
"name": "traits.email",
"type": "email",
"disabled": false
},
"messages": [],
"meta": {
"label": {
"id": 1070002,
"text": "E-Mail",
"type": "info"
}
}
}, {
...
}]
}
}

之後,依據流程資訊建立表單:

<Card title="Register new account" bordered={false}>
{/* 流程訊息展示 */}
{this.state.flow.ui.messages &&
this.state.flow.ui.messages.map((m: UiText, index) => (
<Alert
key={index}
message={m.text}
type={m.type as AlertProps["type"]}
style={{ marginBottom: 16 }}
showIcon
/>
))}
{/* 流程表單建立 */}
<Form
name="register"
ref={this.formRef}
encType="application/x-www-form-urlencoded"
action={this.state.flow.ui.action}
method={this.state.flow.ui.method}
onFinish={onFinish}
>
{this.state.flow.ui.nodes.map((node, index) => {
return React.cloneElement(ui.toUiNodeAntd(node)!, {
key: index,
});
})}
</Form>
</Card>

其中表單 onFinish 裡處理提交:

const onFinish = (values: any) => {
// 因 AntD Form 不提交原 HTML form,所以自己建立 from 提交
// - 不能直接 find form 提交,此時值已清空
// - 建立 from 提交,與 AntD From 相互無影響
ui.submitViaForm(this.state.flow!.ui, values); // 或者,用 `/self-service/registration/api` 提交
// this.submitViaApi(values);
};

登入

  • GET /self-service/login/browser: 初始化登入流程
  • GET /self-service/login/flows: 獲取登入流程
  • POST /self-service/login: 提交登入流程

與註冊流程一樣。

登入後,可通過 whoami 獲取授權資訊:

  • GET /sessions/whoami: 獲取授權資訊
authPublicApi
.toSession(undefined, undefined, {
withCredentials: true,
})
.then((res: AxiosResponse<Session>) => {
if (utils.assertResponse(res)) {
utils.redirectToSelfService("/self-service/login/browser");
return;
}
this.setState({ session: res.data });
})
.catch((err: AxiosError) => utils.redirectOnError(err, "/auth/login"));

Dashboard 頁展示了授權資訊:

驗證

  • GET /self-service/verification/browser: 初始化驗證流程
  • GET /self-service/verification/flows: 獲取驗證流程
  • POST /self-service/verification: 提交驗證流程

與註冊流程一樣。

恢復

  • GET /self-service/recovery/browser: 初始化恢復流程
  • GET /self-service/recovery/flows: 獲取恢復流程
  • POST /self-service/recovery: 提交恢復流程

與註冊流程一樣。

設定

  • GET /self-service/settings/browser: 初始化設定流程
  • GET /self-service/settings/flows: 獲取設定流程
  • POST /self-service/settings: 完成設定流程

與註冊流程一樣。

但要注意的是,依據流程資訊建立表單時,請區分 group 構建多個表單:

const nodesGroup: Record<
string,
{
title?: string;
nodes?: Array<UiNode>;
}
> = {
default: {},
profile: { title: "Profile" },
password: { title: "Password" },
oidc: { title: "Social Sign In" },
};
for (const [k, v] of Object.entries(nodesGroup)) {
nodesGroup[k] = {
title: v.title,
nodes: ui.onlyNodes(this.state.flow!.ui.nodes, k),
};
}
<Card title="Settings" bordered={false}>
{this.state.flow.ui.messages &&
this.state.flow.ui.messages.map((m: UiText, index) => (
<Alert
key={index}
message={m.text}
type={m.type as AlertProps["type"]}
style={{ marginBottom: 16 }}
showIcon
/>
))}
{/* Split Form by group here. Otherwise, one AntD Form method conflicts. */}
{Object.entries(nodesGroup)
.filter(([k, v]) => k !== "default" && v && v.nodes!.length > 0)
.map(([k, v], index) => (
<Form
key={index}
name={k}
encType="application/x-www-form-urlencoded"
action={this.state.flow!.ui.action}
method={this.state.flow!.ui.method}
onFinish={onFinish}
>
<Form.Item>
<div>{v.title}</div>
</Form.Item>
{v
.nodes!.concat(nodesGroup["default"].nodes!)
.map((node, index) => {
return React.cloneElement(ui.toUiNodeAntd(node)!, {
key: index,
});
})}
</Form>
))}
</Card>

登出

  • GET /self-service/logout/browser: 建立登出 URL
  • POST /self-service/logout: 完成登出流程

頁面載入後建立登出 URL ,

authPublicApi
.createSelfServiceLogoutFlowUrlForBrowsers(undefined, {
withCredentials: true,
})
.then((res: AxiosResponse<SelfServiceLogoutUrl>) => {
this.setState({ logoutUrl: res.data.logout_url });
})
.catch((err) => {
// console.log(err);
});

之後,頁面加上登出按鈕:

{this.state.logoutUrl && (
<Button
type="link"
shape="circle"
href={this.state.logoutUrl}
icon={<LogoutOutlined />}
/>
)}

參考

GoCoding 個人實踐的經驗分享,可關注公眾號!