Skip to main content

Ory Kratos 用户认证

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 kratosswagger serve -F=swagger ./spec/swagger.json

运行服务#

docker-compose 运行:

cd kratosdocker-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 kratoscat 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-kratosdocker-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 typescriptcd my-webyarn 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/kratos-client@0.7.0-alpha.1

注册#

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

参考#