react 验证身份证_使用React,GraphQL和用户身份验证构建运行状况跟踪应用

react 验证身份证

本文最初发布在Okta开发人员博客上 感谢您支持使SitePoint成为可能的合作伙伴。

我想您会喜欢我要告诉您的故事。 我将向您展示如何使用Vesper框架,TypeORM和MySQL构建GraphQL API。 这些是Node框架,我将使用TypeScript作为语言。 对于客户端,我将使用React,reactstrap和Apollo Client与API进行通信。 一旦有了这种环境,并添加了安全的用户身份验证,我相信您会喜欢这种体验的!

为什么要关注安全认证? 好吧,除了我为Okta工作以外,我认为我们都可以同意,几乎每个应用程序都依赖于安全的身份管理系统。 对于大多数正在构建React应用程序的开发人员而言,需要在滚动自己的身份验证/授权或插入Okta之类的服务之间做出决定。 在开始构建React应用之前,我想向您介绍Okta,以及为什么我认为它是所有JavaScript开发人员的绝佳解决方案。

什么是Okta?

简而言之,与以往相比,我们使身份管理更加轻松,安全和可扩展。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 我们的API使您能够:

你卖了吗 注册一个永久免费的开发者帐户 ,当您完成后,请回来查看,以便我们进一步了解如何在React中构建安全的应用程序!

为什么要使用健康跟踪应用程序?

在9月下旬至2014年10月中旬,我做了21天的糖排毒,在此期间,我停止进食糖,开始定期运动,并停止饮酒。 我已经高血压十多年了,当时正在服用降压药。 在排毒的第一周,我没用过降压药。 由于新处方需要看医生,因此我决定等到排毒后才能得到。 三个星期后,我不仅体重减轻了15磅,而且血压也恢复了正常水平!

在开始排毒之前,我想出了一个21点系统,以查看我每周的健康状况。 它的规则很简单:由于以下原因,您每天最多可以赚取三分:

  1. 如果您饮食健康,那么您会有所收获。 否则为零。
  2. 如果您运动,就会有所收获。
  3. 如果你不喝酒,你会得到一点。

我很惊讶地发现,使用该系统的第一周我获得了8分。 在排毒期间,第一周我得到16分,第二周得到20分,第三周得到21分。 在排毒之前,我认为健康饮食意味着除了快餐以外什么都不要吃。 排毒后,我意识到吃健康对我来说意味着不吃糖。 我也是精酿啤酒的忠实爱好者,因此我修改了酒精规则,每天允许喝两种更健康的酒精饮料(例如灵缇或红酒)。

我的目标是每周赚15点。 我发现如果我多吃些东西,我很可能会减轻体重,并且血压很好。 如果我少于15岁,则有生病的危险。 自2014年9月以来,我一直在跟踪自己的健康状况。我一直在减肥,血压已恢复并保持正常水平。 从20多岁开始我就一直没有好血压,所以这对我来说是一生的改变。

我建立了21点健康状况以跟踪我的健康状况。 我认为重新创建该应用程序的一小部分,只是跟踪每日积分会很有趣。

使用TypeORM,GraphQL和Vesper构建API

TypeORM是一个漂亮的ORM(对象关系映射器)框架,可以在大多数JavaScript平台上运行,包括Node,浏览器,Cordova,React Native和Electron。 它在很大程度上受Hibernate,Doctrine和Entity Framework的影响。 全局安装TypeORM以开始创建API。

npm i -g typeorm@0.2.7

创建一个目录来保存React客户端和GraphQL API。

mkdir health-tracker
cd health-tracker

使用以下命令使用MySQL创建一个新项目:

typeorm init --name graphql-api --database mysql

编辑graphql-api/ormconfig.json以自定义用户名,密码和数据库。

{
    ...
    "username": "health",
    "password": "pointstest",
    "database": "healthpoints",
    ...
}

提示:要查看针对MySQL执行的查询,请将此文件中的“ logging”值更改为“ all”。 许多其他日志记录选项也可用。

安装MySQL

如果尚未安装MySQL,请安装它。 在Ubuntu上,您可以使用sudo apt-get install mysql-server 。 在macOS上,您可以使用Homebrew和brew install mysql 。 对于Windows,您可以使用MySQL Installer

安装并配置了root密码MySQL后,登录并创建healthpoints数据库。

mysql -u root -p
create database healthpoints;
use healthpoints;
grant all privileges on *.* to 'health'@'localhost' identified by 'points';

在终端窗口中导航到graphql-api项目,安装项目的依赖项,然后启动它以确保可以连接到MySQL。

cd graphql-api
npm i
npm start

您应该看到以下输出:

Inserting a new user into the database...
Saved a new user with id: 1
Loading users from the database...
Loaded users:  [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ]
Here you can setup and run express/koa/any other framework.

安装Vesper以集成TypeORM和GraphQL

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

Vesper是一个集成TypeORM和GraphQL的Node框架。 要安装它,请使用ol'npm。

npm i vesper@0.1.9

现在是时候创建一些GraphQL模型(定义数据的外观)和一些控制器(解释如何与数据交互)了。

创建graphql-api/src/schema/model/Points.graphql

type Points {
  id: Int
  date: Date
  exercise: Int
  diet: Int
  alcohol: Int
  notes: String
  user: User
}

创建graphql-api/src/schema/model/User.graphql

type User {
  id: String
  firstName: String
  lastName: String
  points: [Points]
}

接下来,创建带有查询和突变的graphql-api/src/schema/controller/PointsController.graphql

type Query {
  points: [Points]
  pointsGet(id: Int): Points
  users: [User]
}

type Mutation {
  pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points
  pointsDelete(id: Int): Boolean
}

现在,您的数据具有GraphQL元数据,将创建将由TypeORM管理的实体。 将src/entity/User.ts更改为以下代码,该代码允许将点与用户关联。

import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm';
import { Points } from './Points';

@Entity()
export class User {

  @PrimaryColumn()
  id: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @OneToMany(() => Points, points => points.user)
  points: Points[];
}

在同一src/entity目录中,使用以下代码创建Points.ts类。

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './User';

@Entity()
export class Points {

  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
  date: Date;

  @Column()
  exercise: number;

  @Column()
  diet: number;

  @Column()
  alcohol: number;

  @Column()
  notes: string;

  @ManyToOne(() => User, user => user.points, { cascade: ["insert"] })
  user: User|null;
}

请注意上面@ManyToOne批注上的cascade: ["insert"]选项。 如果实体上存在用户,此选项将自动插入用户。 创建src/controller/PointsController.ts来处理来自GraphQL查询和变异的数据转换。

import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';

@Controller()
export class PointsController {

  constructor(private entityManager: EntityManager) {
  }

  // serves "points: [Points]" requests
  @Query()
  points() {
    return this.entityManager.find(Points);
  }

  // serves "pointsGet(id: Int): Points" requests
  @Query()
  pointsGet({id}) {
    return this.entityManager.findOne(Points, id);
  }

  // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
  @Mutation()
  pointsSave(args) {
    const points = this.entityManager.create(Points, args);
    return this.entityManager.save(Points, points);
  }

  // serves "pointsDelete(id: Int): Boolean" requests
  @Mutation()
  async pointsDelete({id}) {
    await this.entityManager.remove(Points, {id: id});
    return true;
  }
}

更改src/index.ts以使用Vesper的bootstrap()来配置所有内容。

import { bootstrap } from 'vesper';
import { PointsController } from './controller/PointsController';
import { Points } from './entity/Points';
import { User } from './entity/User';

bootstrap({
  port: 4000,
  controllers: [
    PointsController
  ],
  entities: [
    Points,
    User
  ],
  schemas: [
    __dirname + '/schema/**/*.graphql'
  ],
  cors: true
}).then(() => {
  console.log('Your app is up and running on http://localhost:4000. ' +
    'You can use playground in development mode on http://localhost:4000/playground');
}).catch(error => {
  console.error(error.stack ? error.stack : error);
});

此代码告诉Vesper注册控制器,实体,GraphQL模式,以在端口4000上运行,并启用CORS(跨域资源共享)。

使用npm start启动您的API,并导航到http:// localhost:4000 / playground。 在左窗格中,输入以下变体,然后按播放按钮。 您可以尝试在下面键入代码,以便体验GraphQL为您提供的代码完成。

mutation {
  pointsSave(exercise:1, diet:1, alcohol:1, notes:"Hello World") {
    id
    date
    exercise
    diet
    alcohol
    notes
  }
}

您的结果应该类似于我的结果。

GraphQL游乐场

您可以单击右侧的“ SCHEMA”选项卡以查看可用的查询和变异。 很漂亮吧?

您可以使用以下points查询来验证数据是否在数据库中。

query {
  points {id date exercise diet notes}
}

修复日期

您可能会注意到, pointsSavepoints查询返回的日期采用JavaScript客户端可能难以理解的格式。 您可以修复此问题,安装graphql-iso-date

npm i graphql-iso-date@3.5.0

然后,在src/index.ts添加导入,并为各种日期类型配置自定义解析器。 此示例仅使用Date ,但是了解其他选项会有所帮助。

import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';

bootstrap({
  ...
  // https://github.com/vesper-framework/vesper/issues/4
  customResolvers: {
    Date: GraphQLDate,
    Time: GraphQLTime,
    DateTime: GraphQLDateTime
  },
  ...
});

现在,运行points查询将返回更适合客户的结果。

{
  "data": {
    "points": [
      {
        "id": 1,
        "date": "2018-06-04",
        "exercise": 1,
        "diet": 1,
        "notes": "Hello World"
      }
    ]
  }
}

您已经在大约20分钟的时间内使用GraphQL和TypeScript编写了API。 多么酷啊?! 尽管如此,仍有工作要做。 在下一部分中,您将为此API创建一个React客户端,并使用OIDC添加身份验证。 添加身份验证将使您能够获取用户的信息并将用户与他们的积分相关联。

React入门

使用React最快的方法之一是使用Create React App 。 使用以下命令安装最新版本。

npm i -g create-react-app@1.1.4

导航到创建GraphQL API的目录,然后创建一个React客户端。

cd health-tracker
create-react-app react-client

安装将Apollo Client与React以及Bootstrap和reactstrap集成所需的依赖

npm i apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2

为您的API配置Apollo客户端

打开react-client/src/App.js并从apollo-boost导入ApolloClient并将端点添加到您的GraphQL API。

import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

而已! 仅需三行代码,您的应用就可以开始获取数据了。 您可以通过从graphql-tag导入gql函数来证明这一点。 这将解析您的查询字符串,并将其转换为查询文档。

import gql from 'graphql-tag';

class App extends Component {

  componentDidMount() {
    client.query({
      query: gql`
        {
          points {
            id date exercise diet alcohol notes
          }
        }
      `
    })
    .then(result => console.log(result));
  }
...
}

确保打开浏览器的开发人员工具,以便在进行此更改后可以查看数据。 您可以修改console.log()以使用this.setState({points: results.data.points}) ,但是随后您必须在构造函数中初始化默认状态。 但是有一种更简单的方法:您可以使用react-apollo ApolloProviderQuery组件!

以下是使用这些组件的react-client/src/App.js的修改版本。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ApolloClient from 'apollo-boost';
import gql from 'graphql-tag';
import { ApolloProvider, Query } from 'react-apollo';
const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

class App extends Component {

  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
          <Query query={gql`
            {
              points {id date exercise diet alcohol notes}
            }
          `}>
            {({loading, error, data}) => {
              if (loading) return <p>Loading...</p>;
              if (error) return <p>Error: {error}</p>;
              return data.points.map(p => {
                return <div key={p.id}>
                  <p>Date: {p.date}</p>
                  <p>Points: {p.exercise + p.diet + p.alcohol}</p>
                  <p>Notes: {p.notes}</p>
                </div>
              })
            }}
          </Query>
        </div>
      </ApolloProvider>
    );
  }
}

export default App;

您已经构建了一个GraphQL API和与其交谈的React UI –出色的工作! 但是,还有更多工作要做。 在接下来的部分中,我将向您展示如何向React添加身份验证,如何使用Vesper验证JWT以及如何向UI添加CRUD功能。 由于您先前编写的变异,API中已经存在CRUD功能。

使用OpenID Connect为React添加身份验证

您需要将React配置为使用Okta进行身份验证。 为此,您需要在Okta中创建OIDC应用。

登录到您的1563开发者帐户(或者注册 ,如果你没有一个帐户)并导航到应用程序 > 添加应用程序 。 单击“ 单页应用程序” ,再单击“ 下一步” ,然后为应用程序命名。 将localhost:8080所有实例更改为localhost:3000 ,然后单击完成 。 您的设置应与以下屏幕截图相似。

OIDC应用设置

Okta的React SDK允许您将OIDC集成到React应用程序中。 要安装,请运行以下命令:

npm i @okta/okta-react@1.0.2 react-router-dom@4.2.2

Okta的React SDK依赖于react-router ,因此需要安装react-router-dom的原因。 在client/src/App.tsx配置路由是一种常见的做法,因此请使用下面JavaScript替换其代码,该JavaScript设置了Okta的身份验证。

import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { ImplicitCallback, SecureRoute, Security } from '@okta/okta-react';
import Home from './Home';
import Login from './Login';
import Points from './Points';

function onAuthRequired({history}) {
  history.push('/login');
}

class App extends Component {
  render() {
    return (
      <Router>
        <Security issuer='https://{yourOktaDomain}/oauth2/default'
                  client_id='{clientId}'
                  redirect_uri={window.location.origin + '/implicit/callback'}
                  onAuthRequired={onAuthRequired}>
          <Route path='/' exact={true} component={Home}/>
          <SecureRoute path='/points' component={Points}/>
          <Route path='/login' render={() => <Login baseUrl='https://{yourOktaDomain}'/>}/>
          <Route path='/implicit/callback' component={ImplicitCallback}/>
        </Security>
      </Router>
    );
  }
}

export default App;

确保替换上面代码中的{yourOktaDomain}{clientId} 。 您可以在Okta开发人员控制台中找到这两个值。

App.js的代码引用了两个尚不存在的组件: HomeLoginPoints 。 使用以下代码创建src/Home.js 该组件将呈现默认路线,提供“登录”按钮,并在您登录后链接到您的点和注销。

import React, { Component } from 'react';
import { withAuth } from '@okta/okta-react';
import { Button, Container } from 'reactstrap';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';

export default withAuth(class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {authenticated: null, userinfo: null, isOpen: false};
    this.checkAuthentication = this.checkAuthentication.bind(this);
    this.checkAuthentication();
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      if (authenticated && !this.state.userinfo) {
        const userinfo = await this.props.auth.getUser();
        this.setState({authenticated, userinfo});
      } else {
        this.setState({authenticated});
      }
    }
  }

  async componentDidMount() {
    this.checkAuthentication();
  }

  async componentDidUpdate() {
    this.checkAuthentication();
  }

  async login() {
    this.props.auth.login('/');
  }

  async logout() {
    this.props.auth.logout('/');
    this.setState({authenticated: null, userinfo: null});
  }

  render() {
    if (this.state.authenticated === null) return null;
    const button = this.state.authenticated ?
        <div>
          <Button color="link"><Link to="/points">Manage Points</Link></Button><br/>
          <Button color="link" onClick={this.logout}>Logout</Button>
        </div>:
      <Button color="primary" onClick={this.login}>Login</Button>;

    const message = this.state.userinfo ?
      <p>Hello, {this.state.userinfo.given_name}!</p> :
      <p>Please log in to manage your points.</p>;

    return (
      <div>
        <AppNavbar/>
        <Container fluid>
          {message}
          {button}
        </Container>
      </div>
    );
  }
});

该组件使用reactstrap中的<Container/><Button/> 。 安装reactstrap,以便所有内容都能编译。 它取决于Bootstrap,因此也包含它。

npm i reactstrap@6.1.0 bootstrap@4.1.1

将BootstrapCSS文件作为导入添加到src/index.js

import 'bootstrap/dist/css/bootstrap.min.css';

您可能会注意到Home组件的render()方法中有一个<AppNavbar/> 。 创建src/AppNavbar.js以便可以在组件之间使用通用标头。

import React, { Component } from 'react';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';

export default class AppNavbar extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false};
    this.toggle = this.toggle.bind(this);
  }

  toggle() {
    this.setState({
      isOpen: !this.state.isOpen
    });
  }

  render() {
    return <Navbar color="success" dark expand="md">
      <NavbarBrand tag={Link} to="/">Home</NavbarBrand>
      <NavbarToggler onClick={this.toggle}/>
      <Collapse isOpen={this.state.isOpen} navbar>
        <Nav className="ml-auto" navbar>
          <NavItem>
            <NavLink
              href="https://twitter.com/oktadev">@oktadev</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="https://github.com/oktadeveloper/okta-react-graphql-example/">GitHub</NavLink>
          </NavItem>
        </Nav>
      </Collapse>
    </Navbar>;
  }
}

在此示例中,我将嵌入Okta的Sign-In Widget 。 另一个选项是重定向到Okta并使用托管的登录页面。 使用npm安装登录小部件。

npm i @okta/okta-signin-widget@2.9.0

创建src/Login.js并添加以下代码。

import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import OktaSignInWidget from './OktaSignInWidget';
import { withAuth } from '@okta/okta-react';

export default withAuth(class Login extends Component {
  constructor(props) {
    super(props);
    this.onSuccess = this.onSuccess.bind(this);
    this.onError = this.onError.bind(this);
    this.state = {
      authenticated: null
    };
    this.checkAuthentication();
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      this.setState({authenticated});
    }
  }

  componentDidUpdate() {
    this.checkAuthentication();
  }

  onSuccess(res) {
    return this.props.auth.redirect({
      sessionToken: res.session.token
    });
  }

  onError(err) {
    console.log('error logging in', err);
  }

  render() {
    if (this.state.authenticated === null) return null;
    return this.state.authenticated ?
      <Redirect to={{pathname: '/'}}/> :
      <OktaSignInWidget
        baseUrl={this.props.baseUrl}
        onSuccess={this.onSuccess}
        onError={this.onError}/>;
  }
});

Login组件具有对OktaSignInWidget的引用。 创建src/OktaSignInWidget.js

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import OktaSignIn from '@okta/okta-signin-widget';
import '@okta/okta-signin-widget/dist/css/okta-sign-in.min.css';
import '@okta/okta-signin-widget/dist/css/okta-theme.css';
import './App.css';

export default class OktaSignInWidget extends Component {
  componentDidMount() {
    const el = ReactDOM.findDOMNode(this);
    this.widget = new OktaSignIn({
      baseUrl: this.props.baseUrl
    });
    this.widget.renderEl({el}, this.props.onSuccess, this.props.onError);
  }

  componentWillUnmount() {
    this.widget.remove();
  }

  render() {
    return <div/>;
  }
};

创建src/Points.js来呈现API中的点列表。

import React, { Component } from 'react';
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
import { withAuth } from '@okta/okta-react';
import AppNavbar from './AppNavbar';
import { Alert, Button, Container, Table } from 'reactstrap';
import PointsModal from './PointsModal';

export const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql'
});

export default withAuth(class Points extends Component {
  client;

  constructor(props) {
    super(props);
    this.state = {points: [], error: null};

    this.refresh = this.refresh.bind(this);
    this.remove = this.remove.bind(this);
  }

  refresh(item) {
    let existing = this.state.points.filter(p => p.id === item.id);
    let points = [...this.state.points];
    if (existing.length === 0) {
      points.push(item);
      this.setState({points});
    } else {
      this.state.points.forEach((p, idx) => {
        if (p.id === item.id) {
          points[idx] = item;
          this.setState({points});
        }
      })
    }
  }

  remove(item, index) {
    const deletePoints = gql`mutation pointsDelete($id: Int) { pointsDelete(id: $id) }`;

    this.client.mutate({
      mutation: deletePoints,
      variables: {id: item.id}
    }).then(result => {
      if (result.data.pointsDelete) {
        let updatedPoints = [...this.state.points].filter(i => i.id !== item.id);
        this.setState({points: updatedPoints});
      }
    });
  }

  componentDidMount() {
    const authLink = setContext(async (_, {headers}) => {
      const token = await this.props.auth.getAccessToken();
      const user = await this.props.auth.getUser();

      // return the headers to the context so httpLink can read them
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : '',
          'x-forwarded-user': user ? JSON.stringify(user) : ''
        }
      }
    });

    this.client = new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache(),
      connectToDevTools: true
    });

    this.client.query({
      query: gql`
        {
          points {
              id,
              user {
                  id,
                  lastName
              }
              date,
              alcohol,
              exercise,
              diet,
              notes
          }
        }`
    }).then(result => {
      this.setState({points: result.data.points});
    }).catch(error => {
      this.setState({error: <Alert color="danger">Failure to communicate with API.</Alert>});
    });
  }

  render() {
    const {points, error} = this.state;
    const pointsList = points.map(p => {
      const total = p.exercise + p.diet + p.alcohol;
      return <tr key={p.id}>
        <td style={{whiteSpace: 'nowrap'}}><PointsModal item={p} callback={this.refresh}/></td>
        <td className={total <= 1 ? 'text-danger' : 'text-success'}>{total}</td>
        <td>{p.notes}</td>
        <td><Button size="sm" color="danger" onClick={() => this.remove(p)}>Delete</Button></td>
      </tr>
    });

    return (
      <div>
        <AppNavbar/>
        <Container fluid>
          {error}
          <h3>Your Points</h3>
          <Table>
            <thead>
            <tr>
              <th width="10%">Date</th>
              <th width="10%">Points</th>
              <th>Notes</th>
              <th width="10%">Actions</th>
            </tr>
            </thead>
            <tbody>
            {pointsList}
            </tbody>
          </Table>
          <PointsModal callback={this.refresh}/>
        </Container>
      </div>
    );
  }
})

这段代码从refresh()remove()方法开始,稍后我将介绍它们。 重要的部分发生在componentDidMount() ,其中访问令牌添加在Authorization标头中,而用户信息填充在x-forwarded-user标头中。 使用此信息创建一个ApolloClient ,添加一个缓存,并打开connectToDevTools标志。 这对于使用Apollo Client Developer Tools进行调试很有用。

componentDidMount() {
  const authLink = setContext(async (_, {headers}) => {
    const token = await this.props.auth.getAccessToken();

    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : '',
        'x-forwarded-user': user ? JSON.stringify(user) : ''
      }
    }
  });

  this.client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
    connectToDevTools: true
  });

  // this.client.query(...);
}

使用Apollo Client进行身份验证需要一些新的依赖项。 立即安装。

npm apollo-link-context@1.0.8 apollo-link-http@1.5.4

在页面的JSX中,有一个删除按钮,该按钮在Points中调用remove()方法。 还有一个<PointsModal/>组件。 每个项目以及其底部均引用此标记。 您会注意到这两个都引用了refresh()方法,该方法将更新列表。

<PointsModal item={p} callback={this.refresh}/>
<PointsModal callback={this.refresh}/>

如果未设置任何item则此组件将呈现一个链接以编辑该组件,或提供一个“添加”按钮。

创建src/PointsModal.js并添加以下代码。

import React, { Component } from 'react';
import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { withAuth } from '@okta/okta-react';
import { httpLink } from './Points';
import { ApolloClient } from 'apollo-client';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
import { Link } from 'react-router-dom';

export default withAuth(class PointsModal extends Component {
  client;
  emptyItem = {
    date: (new Date()).toISOString().split('T')[0],
    exercise: 1,
    diet: 1,
    alcohol: 1,
    notes: ''
  };

  constructor(props) {
    super(props);
    this.state = {
      modal: false,
      item: this.emptyItem
    };

    this.toggle = this.toggle.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  componentDidMount() {
    if (this.props.item) {
      this.setState({item: this.props.item})
    }

    const authLink = setContext(async (_, {headers}) => {
      const token = await this.props.auth.getAccessToken();
      const user = await this.props.auth.getUser();

      // return the headers to the context so httpLink can read them
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : '',
          'x-forwarded-user': JSON.stringify(user)
        }
      }
    });

    this.client = new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache()
    });
  }

  toggle() {
    if (this.state.modal && !this.state.item.id) {
      this.setState({item: this.emptyItem});
    }
    this.setState({modal: !this.state.modal});
  }

  render() {
    const {item} = this.state;
    const opener = item.id ? <Link onClick={this.toggle} to="#">{this.props.item.date}</Link> :
      <Button color="primary" onClick={this.toggle}>Add Points</Button>;

    return (
      <div>
        {opener}
        <Modal isOpen={this.state.modal} toggle={this.toggle}>
          <ModalHeader toggle={this.toggle}>{(item.id ? 'Edit' : 'Add')} Points</ModalHeader>
          <ModalBody>
            <Form onSubmit={this.handleSubmit}>
              <FormGroup>
                <Label for="date">Date</Label>
                <Input type="date" name="date" id="date" value={item.date}
                       onChange={this.handleChange}/>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="exercise" id="exercise" checked={item.exercise}
                         onChange={this.handleChange}/>{' '}
                  Did you exercise?
                </Label>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="diet" id="diet" checked={item.diet}
                         onChange={this.handleChange}/>{' '}
                  Did you eat well?
                </Label>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="alcohol" id="alcohol" checked={item.alcohol}
                         onChange={this.handleChange}/>{' '}
                  Did you drink responsibly?
                </Label>
              </FormGroup>
              <FormGroup>
                <Label for="notes">Notes</Label>
                <Input type="textarea" name="notes" id="notes" value={item.notes}
                       onChange={this.handleChange}/>
              </FormGroup>
            </Form>
          </ModalBody>
          <ModalFooter>
            <Button color="primary" onClick={this.handleSubmit}>Save</Button>{' '}
            <Button color="secondary" onClick={this.toggle}>Cancel</Button>
          </ModalFooter>
        </Modal>
      </div>
    )
  };

  handleChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? (target.checked ? 1 : 0) : target.value;
    const name = target.name;
    let item = {...this.state.item};
    item[name] = value;
    this.setState({item});
  }

  handleSubmit(event) {
    event.preventDefault();
    const {item} = this.state;

    const updatePoints = gql`
      mutation pointsSave($id: Int, $date: Date, $exercise: Int, $diet: Int, $alcohol: Int, $notes: String) {
        pointsSave(id: $id, date: $date, exercise: $exercise, diet: $diet, alcohol: $alcohol, notes: $notes) {
          id date
        }
      }`;

    this.client.mutate({
      mutation: updatePoints,
      variables: {
        id: item.id,
        date: item.date,
        exercise: item.exercise,
        diet: item.diet,
        alcohol: item.alcohol,
        notes: item.notes
      }
    }).then(result => {
      let newItem = {...item};
      newItem.id = result.data.pointsSave.id;
      this.props.callback(newItem);
      this.toggle();
    });
  }
});

确保您的GraphQL后端已启动,然后使用npm start React前端。 文本紧靠顶部导航栏,因此通过在src/index.css添加规则来添加一些填充。

.container-fluid {
  padding-top: 10px;
}

您应该看到Home组件和一个登录按钮。

主屏幕

单击“ 登录” ,系统将提示您输入Okta凭据。

登入画面

然后您将登录!

经过身份验证的用户的主屏幕

单击管理点以查看点列表。

您的积分屏幕

看到一切正常很酷,不是吗? :D

您的React前端是安全的,但是您的API仍然是开放的。 让我们修复它。

从JWT获取用户信息

在终端窗口中导航到您的graphql-api项目,然后安装Okta的JWT Verifier。

npm i @okta/jwt-verifier@0.0.12

创建graphql-api/src/CurrentUser.ts来保存当前用户的信息。

export class CurrentUser {
  constructor(public id: string, public firstName: string, public lastName: string) {}
}

graphql-api/src/index.ts导入OktaJwtVerifierCurrentUser并配置JWT验证程序以使用OIDC应用程序的设置。

import * as OktaJwtVerifier from '@okta/jwt-verifier';
import { CurrentUser } from './CurrentUser';

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: '{clientId}',
  issuer: 'https://{yourOktaDomain}/oauth2/default'
});

在引导程序配置中,将setupContainer定义为需要authorization标头,然后从x-forwarded-user标头设置当前用户。

bootstrap({
  ...
  cors: true,
  setupContainer: async (container, action) => {
    const request = action.request;
    // require every request to have an authorization header
    if (!request.headers.authorization) {
      throw Error('Authorization header is required!');
    }
    let parts = request.headers.authorization.trim().split(' ');
    let accessToken = parts.pop();
    await oktaJwtVerifier.verifyAccessToken(accessToken)
      .then(async jwt => {
        const user = JSON.parse(request.headers['x-forwarded-user'].toString());
        const currentUser = new CurrentUser(jwt.claims.uid, user.given_name, user.family_name);
        container.set(CurrentUser, currentUser);
      })
      .catch(error => {
        throw Error('JWT Validation failed!');
      })
  }
  ...
});

修改graphql-api/src/controller/PointsController.ts以将CurrentUser作为依赖项注入。 当您在那里时,调整points()方法以按用户ID进行过滤,并修改pointsSave()以在保存时设置用户。

import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';
import { User } from '../entity/User';
import { CurrentUser } from '../CurrentUser';

@Controller()
export class PointsController {

  constructor(private entityManager: EntityManager, private currentUser: CurrentUser) {
  }

  // serves "points: [Points]" requests
  @Query()
  points() {
    return this.entityManager.getRepository(Points).createQueryBuilder("points")
      .innerJoin("points.user", "user", "user.id = :id", { id: this.currentUser.id })
      .getMany();
  }

  // serves "pointsGet(id: Int): Points" requests
  @Query()
  pointsGet({id}) {
    return this.entityManager.findOne(Points, id);
  }

  // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
  @Mutation()
  pointsSave(args) {
    // add current user to points saved
    if (this.currentUser) {
      const user = new User();
      user.id = this.currentUser.id;
      user.firstName = this.currentUser.firstName;
      user.lastName = this.currentUser.lastName;
      args.user = user;
    }

    const points = this.entityManager.create(Points, args);
    return this.entityManager.save(Points, points);
  }

  // serves "pointsDelete(id: Int): Boolean" requests
  @Mutation()
  async pointsDelete({id}) {
    await this.entityManager.remove(Points, {id: id});
    return true;
  }
}

重新启动API,您应该参与竞赛!

加分模态

编辑点模态

源代码

您可以在此处找到本文的源代码。

了解有关React,节点和用户身份验证的更多信息

本文向您展示了如何使用GraphQL,TypeORM和Node / Vesper构建安全的React应用。 希望您喜欢这里!

在Okta,我们关心使React和Node的身份验证易于实现。 我们有一些关于该主题的博客文章和文档! 我鼓励您查看以下链接:

希望您在使用React和GraphQL构建应用程序方面有出色的经验。 如果您有任何疑问,请在Twitter或我整个团队的@oktadev 上打我 。 我们的DM是开放的! :)

翻译自: https://www.sitepoint.com/build-a-health-tracking-app-with-react-graphql-and-user-authentication/

react 验证身份证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值