Ory Kratos 用户认证

GoCoding
• 阅读 1505

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

Ory Kratos 用户认证

运行服务

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

Ory Kratos 用户认证

查看表:

Ory Kratos 用户认证

查看配置

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

设置环境变量可以覆盖。以 _ 表示层级,如 SELFSERVICE_FLOWS_SETTINGS_UI_URL=<value> 覆盖 selfservice.flows.settings.ui_url

Self-Service 流程

浏览器流程

Ory Kratos 用户认证

客户端流程

Ory Kratos 用户认证

动手配置: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 蓝色按钮组件。

Ory Kratos 用户认证

引入 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 页展示了授权信息:

Ory Kratos 用户认证

验证

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

Ory Kratos 用户认证

登出

  • 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 个人实践的经验分享,可关注公众号!

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
4个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
Jacquelyn38 Jacquelyn38
1年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
Karen110 Karen110
1年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_34035044 helloworld_34035044
7个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为