使用Vue,ASP.NET Core和Okta构建安全的待办应用

I love lists. I keep everything I need to do (too many things, usually) in a big to-do list, and the list helps keep me sane throughout the day. It’s like having a second brain!

我喜欢清单。 我将需要做的所有事情(通常有太多事情)保存在一个很大的待办事项列表中,该列表有助于整天保持理智。 就像有第二个大脑!

There are hundreds of to-do apps out there, but today I’ll show you how to build your own from scratch. Why? It’s the perfect exercise for learning a new language or framework! A to-do app is more complex than “Hello World”, but simple enough to build in an afternoon or on the weekend. Building a simple app is a great way to stretch your legs and try a language or framework you haven’t used before.

那里有数百个待办事项应用程序,但是今天我将向您展示如何从头开始构建自己的应用程序。 为什么? 这是学习新语言或框架的完美练习! 待办事项应用程序比“ Hello World”复杂,但足够简单,可以在下午或周末构建。 开发一个简单的应用程序是伸腿并尝试以前从未使用过的语言或框架的好方法。

为什么选择Vue.js和ASP.NET Core? ( Why Vue.js and ASP.NET Core? )

In this article, I’ll show you how to build a lightweight, secure to-do app with a Vue.js frontend and an ASP.NET Core backend. Not familiar with these frameworks? That’s fine! You’ll learn everything as you go. Here’s a short introduction to both:

在本文中,我将向您展示如何使用Vue.js前端和ASP.NET Core后端构建轻便,安全的待办应用程序。 不熟悉这些框架? 没关系! 您将学到一切。 以下是这两者的简短介绍:

Vue.js is a JavaScript framework for building applications that run in the browser. It borrows some good ideas from both Angular and React, and has been gaining popularity recently. I like Vue because it’s easy to pick up and get started. Compared to both Angular and React, the learning curve doesn’t feel as steep. Plus, it has great documentation! For this tutorial, I’ve borrowed from Matt Raible’s excellent article The Lazy Developer’s Guide to Authentication with Vue.js.

Vue.js是一个JavaScript框架,用于构建在浏览器中运行的应用程序。 它借鉴了Angular和React的一些好主意,并且最近已经越来越流行。 我喜欢Vue,因为它很容易上手并开始使用。 与Angular和React相比,学习曲线并不那么陡峭。 另外,它具有出色的文档资料! 在本教程中,我借鉴了Matt Raible的出色文章《懒惰的Vue.js身份验证开发人员指南》

ASP.NET Core is Microsoft’s new open-source framework for building web apps and APIs. I like ASP.NET Core on the backend because it’s type-safe, super fast, and has a large ecosystem of packages available. If you want to learn the basics, I wrote a free ebook about version 2.0, which was released earlier this year.

ASP.NET Core是Microsoft的新开源框架,用于构建Web应用程序和API。 我喜欢后端的ASP.NET Core,因为它类型安全,超快并且具有可用的大型软件包生态系统。 如果您想学习基础知识,我写了免费的有关版本2.0的电子书 ,该书于今年早些时候发布。

Ready to build an app? Let’s get started!

准备构建应用程序了吗? 让我们开始吧!

安装工具 ( Install the tools )

You’ll need Node and npm installed for this tutorial, which you can install from the Node.js official site.

本教程需要安装Node和npm,可以从Node.js官方网站进行安装。

You’ll also need dotnet installed, which you can install from the Microsoft .NET site.

您还需要安装dotnet ,可以从Microsoft .NET站点进行安装。

To double check that everything is installed correctly, run these commands in your terminal or shell:

要仔细检查所有内容是否正确安装,请在您的终端或外壳程序中运行以下命令:

npm -v

dotnet --version

Check tool versions in terminal

I’m using Visual Studio Code for this project, but you can use whatever code editor you feel comfortable in. If you’re on Windows, you can also use Visual Studio 2017 or later.

我正在为该项目使用Visual Studio Code ,但您可以使用自己喜欢的任何代码编辑器。如果您使用Windows,还可以使用Visual Studio 2017或更高版本。

设置项目 ( Set up the project )

Instead of starting from absolute zero, you can use a template to help you scaffold a basic, working application. Mark Pieszak has an excellent ASP.NET Core Vue SPA starter kit, which we’ll use as a starting point.

您可以使用模板而不是从绝对零开始,以帮助您搭建一个基本的,可运行的应用程序。 Mark Pieszak具有出色的ASP.NET Core Vue SPA入门工具包 ,我们将以此为起点。

Download or clone the project from GitHub, and then open the folder in your code editor.

从GitHub下载或克隆项目,然后在代码编辑器中打开文件夹。

Initial project structure

Run npm install to restore and install all of the JavaScript packages (including Vue.js).

运行npm install还原并安装所有JavaScript软件包(包括Vue.js)。

配置环境并运行应用 (Configure the environment and run the app)

You’ll need to make sure the ASPNETCORE_ENVIRONMENT variable is set on your machine. ASP.NET Core looks at this environment variable to determine whether it’s running in a development or production environment.

您需要确保在ASPNETCORE_ENVIRONMENT上设置了ASPNETCORE_ENVIRONMENT变量。 ASP.NET Core会查看此环境变量,以确定它是在开发环境还是生产环境中运行。

  • If you’re on Windows, use PowerShell to execute $Env:ASPNETCORE_ENVIRONMENT = "Development"

    如果您使用的是Windows,请使用PowerShell执行$Env:ASPNETCORE_ENVIRONMENT = "Development"
  • If you’re on Mac or Linux, execute export ASPNETCORE_ENVIRONMENT=Development

    如果您使用的是Mac或Linux,请执行export ASPNETCORE_ENVIRONMENT=Development

In Visual Studio Code, you can open the Integrated Terminal from the View menu to run the above commands.

在Visual Studio Code中,可以从“查看”菜单中打开“集成终端”以运行上述命令。

Now you’re ready to run the app for the first time! Execute dotnet run in the terminal. After the app compiles, it should report that it’s running on http://localhost:5000:

现在您已经准备好首次运行该应用程序! 在终端执行dotnet run 。 应用编译后,它应报告其正在http:// localhost:5000上运行:

Execute dotnet run in terminal

Open up a browser and navigate to http://localhost:5000:

打开浏览器并导航到http:// localhost:5000

Vue starter template app

可选:安装Vue Devtools (Optional: Install Vue Devtools)

If Chrome is your preferred browser, I’d highly recommend the Vue Devtools extension. It adds some great debugging and inspection features to Chrome that are super useful when you’re building Vue.js applications.

如果Chrome是您的首选浏览器,则强烈建议您使用Vue Devtools扩展程序。 它为Chrome添加了一些很棒的调试和检查功能,这些功能在构建Vue.js应用程序时非常有用。

生成Vue.js应用 ( Build the Vue.js app )

It’s time to start writing some real code. If dotnet run is still running in your terminal, press Ctrl-C to stop it.

现在该开始编写一些真实的代码了。 如果您的终端仍在运行dotnet run ,请按Ctrl-C停止它。

Delete all the files and folders under the ClientApp folder, and create a new file called boot-app.js:

删除ClientApp文件夹下的所有文件和文件夹,并创建一个名为boot-app.js的新文件:

import Vue from 'vue'
import App from './components/App'
import router from './router'
import store from './store'
import { sync } from 'vuex-router-sync'

// Sync Vue router and the Vuex store
sync(store, router)

new Vue({
  el: '#app',
  store,
  router,
  template: '<App/>',
  components: { App }
})

store.dispatch('checkLoggedIn')

This file sets up Vue, and serves as the main entry point (or starting point) for the whole JavaScript application.

该文件设置了Vue,并用作整个JavaScript应用程序的主要入口点(或起点)。

Next, create router.js:

接下来,创建router.js

import Vue from 'vue'
import Router from 'vue-router'
import store from './store'
import Dashboard from './components/Dashboard.vue'
import Login from './components/Login.vue'

Vue.use(Router)

function requireAuth (to, from, next) {
  if (!store.state.loggedIn) {
    next({
      path: '/login',
      query: { redirect: to.path }
    })
  } else {
    next()
  }
}

export default new Router({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Dashboard, beforeEnter: requireAuth },
    { path: '/login', component: Login },
    { path: '/logout',
      async beforeEnter (to, from, next) {
        await store.dispatch('logout')
      }
    }
  ]
})

The Vue router keeps track of what page the user is currently viewing, and handles navigating between pages or sections of your app. This file configures the router with three paths (/, /login, and /logout) and associates each path with a Vue component.

Vue路由器跟踪用户当前正在查看的页面,并处理在应用程序的页面或部分之间的导航。 该文件使用三个路径( //login/logout )配置路由器,并将每个路径与Vue组件相关联。

You might be wondering what store, Dashboard, and Login are. Don’t worry, you’ll add them next!

您可能想知道storeDashboardLogin是什么。 不用担心,接下来您将添加它们!

添加组件 (Add components)

Components are how Vue.js organizes pieces of your application. A component wraps up some functionality, from a simple button or UI element to entire pages and sections. Components can contain HTML, JavaScript, and CSS styles.

Vue.js通过组件来组织应用程序的各个部分。 组件包含一些功能,从简单的按钮或UI元素到整个页面和部分。 组件可以包含HTML,JavaScript和CSS样式。

In the ClientApp folder, create a new folder called components. Inside the new folder, create a file called Dashboard.vue:

在ClientApp文件夹中,创建一个名为components的新文件夹。 在新文件夹中,创建一个名为Dashboard.vue的文件:

<template>
  <div class="dashboard">
    <h2>{{name}}, here's your to-do list</h2>

    <input class="new-todo"
        autofocus
        autocomplete="off"
        placeholder="What needs to be done?"
        @keyup.enter="addTodo">

    <ul class="todo-list">
      <todo-item v-for="(todo, index) in todos" :key="index" :item="todo"></todo-item>
    </ul>

    <p>{{ remaining }} remaining</p>
    <router-link to="/logout">Log out</router-link>
  </div>
</template>

<script>
import TodoItem from './TodoItem'

export default {
  components: { TodoItem },
  mounted() {
      this.$store.dispatch('getAllTodos')
  },
  computed: {
    name () {
      return this.$store.state.userName
    },
    todos () {
      return this.$store.state.todos
    },
    complete () {
      return this.todos.filter(todo => todo.completed).length
    },
    remaining () {
      return this.todos.filter(todo => !todo.completed).length
    }
  },
  methods: {
    addTodo (e) {
      var text = e.target.value || ''
      text = text.trim()

      if (text.length) {
        this.$store.dispatch('addTodo', { text })
      }

      e.target.value = ''
    },
  }
}
</script>

<style>
.new-todo {
  width: 100%;
  font-size: 18px;
  margin-bottom: 15px;
  border-top-width: 0;
  border-left-width: 0;
  border-right-width: 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
</style>

The Dashboard component is responsible for displaying all the user’s to-do items, and rendering an input field that lets the user add a new item. In router.js, you told the Vue router to render this component on the / path, or the root route of the application.

仪表板组件负责显示所有用户的待办事项,并渲染一个输入字段,使用户可以添加新项目。 在router.js ,您告诉Vue路由器在/路径或应用程序的根路由上呈现此组件。

This component has HTML in the <template> section, JavaScript in the <script> section, and CSS in the <style> section, all stored in one .vue file. If your Vue components become too large or unwieldy, you can choose to split them into separate HTML, JS, and CSS files as needed.

该组件的<template>部分具有HTML, <script>部分具有JavaScript,而<style>部分具有CSS,它们均存储在一个.vue文件中。 如果您的Vue组件太大或笨拙,则可以根据需要选择将它们拆分为单独HTML,JS和CSS文件。

When you use {{moustaches}} or attributes like v-for in the component’s HTML, Vue.js automatically inserts (or binds) data that’s available to the component. In this case, you’ve defined a handful of JavaScript methods in the computed section that retrieve things like the user’s name and the user’s to-do list from the data store. That data is then automatically rendered by Vue. (You’ll build the data store in a minute!)

当您在组件HTML中使用{{moustaches}}或诸如v-for类的属性时,Vue.js会自动插入(或绑定 )组件可用的数据。 在这种情况下,您已经在computed部分中定义了一些JavaScript方法,这些方法从数据存储区检索用户名和用户的待办事项列表。 然后该数据由Vue自动呈现。 (您将在一分钟内建立数据存储!)

Notice the components: { TodoItem } line? The Dashboard component relies on another component called TodoItem. Create a file called TodoItem.vue:

注意components: { TodoItem }行吗? 仪表板组件依赖于另一个名为TodoItem的组件。 创建一个名为TodoItem.vue的文件:

<template>
  <li class="todo" :class="{ completed: item.completed }">
    <input class="toggle"
      type="checkbox"
      :checked="item.completed"
      @change="toggleTodo({ id: item.id, completed: !item.completed })">

    <label v-text="item.text"></label>

    <button class="delete" @click="deleteTodo(item.id)">
      <span class="glyphicon glyphicon-trash"></span>
    </button>
  </li>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  props: ['item'],
  methods: {
    ...mapActions([
      'toggleTodo',
      'deleteTodo'
    ])
  }
}
</script>

<style>
  .todo {
    list-style-type: none;
  }

  .todo.completed {
    opacity: 0.5;
  }

  .todo.completed label {
    text-decoration: line-through;
  }

  button.delete {
    color: red;
    opacity: 0.5;
    -webkit-appearance: none;
    -moz-appearance: none;
    outline: none;
    border: 0;
    background: transparent;
  }
</style>

The TodoItem component is only responsible for rendering a single to-do item. The props: ['item'] line declares that this component receives a prop or parameter called item, which contains the data about a to-do item (the text, whether it’s completed, and so on).

TodoItem组件仅负责呈现单个待办事项。 props: ['item']行声明该组件接收到称为item道具或参数,其中包含有关待办事项的数据(文本,是否完成等)。

In the Dashboard component, this line creates a TodoItem component for each to-do item:

在仪表板组件中,此行为每个待办事项创建一个TodoItem组件:

<todo-item v-for="(todo, index) in todos" :key="index" :item="todo"></todo-item>

There’s a lot going on in this syntax, but the important bits are:

这种语法有很多事情要做,但是重要的一点是:

  • The <todo-item> tag, which refers to the new TodoItem component.

    <todo-item>标记,它引用新的TodoItem组件。

  • The v-for directive, which tells Vue to loop through all the items in the todos array and render a <todo-item> for each one.

    v-for指令,它告诉Vue遍历todos数组中的所有项目并为每个<todo-item>渲染一个<todo-item>

  • The :item="todo" attribute, which binds the value of todo (a single item from the todos array) to an attribute called item. This data is passed into the TodoItem component as the item prop.

    :item="todo"属性,它将todo的值( todos数组中的单个项目)绑定到名为item的属性。 该数据被传递到的TodoItem组件作为item支撑。

Using components to split your app into small pieces makes it easier to organize and maintain your code. If you need to change how to-do items are rendered in the future, you just need to make changes to the TodoItem component.

使用组件将您的应用程序分成小块,可以更轻松地组织和维护代码。 如果将来需要更改待办事项的呈现方式,则只需更改TodoItem组件。

Add another component called Login.vue:

添加另一个名为Login.vue组件:

<template>
  <div>
    <h2>Login</h2>
    <p v-if="$route.query.redirect">
      You need to login first.
    </p>

    <form @submit.prevent="login" autocomplete="off">
      <label for="email">Email</label>
      <input id="email" v-model="email" placeholder="you@example.com">
      <label for="password">Password</label>
      <input id="password" v-model="password" placeholder="password" type="password">
      <button type="submit">login</button>
      <p v-if="loginError" class="error">{{loginError}}</p>
    </form>
  </div>
</template>

<script>
export default {
  data () {
    return {
      email: '',
      password: '',
      error: false
    }
  },
  computed: {
    loginError () {
      return this.$store.state.loginError
    }
  },
  methods: {
    login () {
      this.$store.dispatch('login', {
        email: this.email,
        password: this.password
       })
    }
  }
}
</script>

<style scoped>
.error {
  color: red;
}

label {
  display: block;
}

input {
  display: block;
  margin-bottom: 10px;
}
</style>

The Login component renders a simple login form, and shows an error message if the login is unsuccessful.

登录组件将呈现一个简单的登录表单,如果登录失败,则会显示一条错误消息。

Notice the scoped attribute on the <style> tag? That’s a cool feature called scoped CSS. Marking a block of CSS as scoped means the CSS rules only apply to this component (otherwise, they apply globally). It’s useful here to set display: block on the input and label elements in this component, without affecting how those elements are rendered elsewhere in the app.

注意<style>标签上的scoped属性吗? 这是一个很酷的功能,称为scoped CSS 。 将一块CSS标记为scoped意味着CSS规则仅适用于此组件(否则,它们将全局应用)。 在这里设置display: block在此组件的inputlabel元素上很有用,而不会影响这些元素在应用程序中其他位置的呈现方式。

The Dashboard and Login components (and the router configuration) refer to something called store. I’ll explain what the store is, and how to build it, in the next section.

仪表板和登录组件(以及路由器配置)引用了称为store东西。 在下一部分中,我将解释什么是商店以及如何建立商店。

Before you get there, you need to build one more component. Create a file called App.vue in the components folder:

在到达那里之前,您需要再构建一个组件。 在components文件夹中创建一个名为App.vue的文件:

<template>
  <div class="app-container">
    <div class="app-view">
      <template v-if="$route.matched.length">
        <router-view></router-view>
      </template>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    loggedIn () {
      return this.$store.state.loggedIn
    }
  }
}
</script>

<style>
html, body {
    margin: 0;
    padding: 0;
}

body {
    font: 14px -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #F3F5F6;
    color: #4d4d4d;
}

ul {
  padding: 0;
}

h1, h2 {
  text-align: center;
}

.app-container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.app-view {
  background: #fff;
  min-width: 400px;
  padding: 20px 25px 15px 25px;
  margin: 30px;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
                0 5px 10px 0 rgba(0, 0, 0, 0.1);
}
</style>

The App component doesn’t do much -- it just provides the base HTML and CSS styles that the other components will be rendered inside. The <router-view> element loads a component provided by the Vue router, which will render either the Dashboard or Login component depending on the path in the address bar.

App组件并没有做太多事情,它只是提供了其他组件将在其中呈现的基本HTML和CSS样式。 <router-view>元素将加载Vue路由器提供的组件,该组件将根据地址栏中的路径来呈现Dashboard或Login组件。

If you look back at boot-app.js, you’ll see this line:

如果回头看boot-app.js ,您将看到以下行:

import App from './components/App'

This statement loads the App component, which is then passed to Vue in new Vue(...).

该语句加载App组件,然后将其传递到new Vue(...)

You’re all done building components! It’s time to add some state management. First, I’ll explain what state management is and why it’s useful.

搭建组件已经完成! 现在该添加一些状态管理了。 首先,我将解释什么是状态管理以及它为什么有用。

添加Vuex进行状态管理 (Add Vuex for state management)

Components are a great way to break up your app into manageable pieces, but when you start passing data between many components, it can be hard to keep all of that data in sync.

组件是将您的应用分解成可管理的部分的好方法,但是当您开始在许多组件之间传递数据时,很难使所有这些数据保持同步。

The Vuex library helps solve this problem by creating a store that holds data in a central place. Then, any of your components can get the data they need from the store. Bonus: if the data in the store changes, your components get the updated data immediately!

Vuex库通过创建将数据存储在中央位置的存储来帮助解决此问题。 然后,您的任何组件都可以从存储中获取所需的数据。 奖励:如果商店中的数据发生更改,您的组件将立即获得更新的数据!

If you’ve used Flux or Redux, you’ll find Vuex familiar: it’s a state management library with strict rules around how state can be mutated (modified) inside your app.

如果您使用过Flux或Redux,您会发现Vuex很熟悉:它是一个状态管理库,其中包含关于如何在应用程序内部更改(修改)状态的严格规则。

The template you started with already has Vuex installed. To keep things tidy, create a new folder under ClientApp called store. Then, create a file inside called index.js:

您开始使用的模板已经安装了Vuex。 为了使内容整洁,请在ClientApp下创建一个名为store的新文件夹。 然后,在内部创建一个名为index.js的文件:

import Vue from 'vue'
import Vuex from 'vuex'
import { state, mutations } from './mutations'
import { actions } from './actions'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions
})

This file initializes Vuex and makes it available to your Vue components, and that’s it. The real meat of Vuex is in mutations and actions, but you’ll write those in separate files to keep everything organized.

该文件初始化Vuex并将其提供给您的Vue组件,仅此而已。 Vuex的真正本领在于突变动作 ,但是您将把它们写在单独的文件中,以保持一切井井有条。

Create a file called mutations.js:

创建一个名为mutations.js的文件:

import router from '../router'

export const state = {
  todos: [],
  loggedIn: false,
  loginError: null,
  userName: null
}

export const mutations = {
  loggedIn(state, data) {
    state.loggedIn = true
    state.userName = (data.name || '').split(' ')[0] || 'Hello'

    let redirectTo = state.route.query.redirect || '/'
    router.push(redirectTo)
  },

  loggedOut(state) {
    state.loggedIn = false
    router.push('/login')
  },

  loginError(state, message) {
    state.loginError = message
  },

  loadTodos(state, todos) {
    state.todos = todos || [];
  }
}

This file defines two things: the state or data that’s shared across the app, and mutations that change that state. Vuex follows a few simple rules:

该文件定义了两件事:在应用程序之间共享的state或数据,以及更改该状态的突变。 Vuex遵循一些简单的规则:

  • State is immutable -- it can’t be changed except by a mutation.

    状态是不可变的- 除非更改, 否则无法更改。
  • Mutations can change state, but they must be synchronous. Async code (like API calls) must run in an action instead.

    变异可以改变状态,但必须保持同步。 异步代码(例如API调用)必须在操作中运行。
  • Actions run asynchronous code, then commit mutations, which change state.

    动作运行异步代码,然后提交更改状态的突变。

Enforcing this hierarchy of rules makes it easier to understand how data and changes flow through your app. If some piece of state changes, you know that a mutation caused the change.

强制执行此规则层次结构可以更轻松地了解数据和更改如何在您的应用程序中流动。 如果某些状态发生了变化,您就会知道是由突变引起的。

As you can see, this app uses the Vuex store to keep track of both the to-do list (the state.todos array), and authentication state (whether the user is logged in, what their name is). The Dashboard and Login components access this data with computed properties like:

如您所见,此应用程序使用Vuex存储来跟踪待办事项列表( state.todos数组)和身份验证状态(无论用户是否登录,用户名)。 仪表板和登录组件使用计算的属性访问此数据,例如:

todos () {
  return this.$store.state.todos
},

The mutations defined here are only half the story, because they only handle updating the state after an action has run. Create another file called actions.js:

这里定义的突变只是故事的一半,因为它们仅操作执行后才处理状态更新。 创建另一个名为actions.js文件:

import axios from 'axios'

const sleep  = ms => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

export const actions = {
  checkLoggedIn({ commit }) {
    // Todo: commit('loggedIn') if the user is already logged in
  },

  async login({ dispatch, commit }, data) {
    // Todo: log the user in
    commit('loggedIn', { userName: data.email })
  },

  async logout({ commit }) {
      // Todo: log the user out
      commit('loggedOut')
  },

  async loginFailed({ commit }, message) {
    commit('loginError', message)
    await sleep(3000)
    commit('loginError', null)
  },

  async getAllTodos({ commit }) {
    // Todo: get the user's to-do items
    commit('loadTodos', [{ text: 'Fake to-do item' }])
  },

  async addTodo({ dispatch }, data) {
    // Todo: save a new to-do item
    await dispatch('getAllTodos')
  },

  async toggleTodo({ dispatch }, data) {
    // Todo: toggle to-do item completed/not completed
    await dispatch('getAllTodos')
  },

  async deleteTodo({ dispatch }, id) {
    // Todo: delete to-do item
    await dispatch('getAllTodos')
  }
}

Most of these actions are marked with // Todo (no pun intended), because you’ll need to revisit them after you have the backend API in place. For now, the getAllTodos action commits the loadTodos mutation with a fake to-do item. Later, this action will call your API to retrieve the user’s to-do items and then commit the same mutation with the items returned from the API.

这些操作大多数都用// Todo标记(不需要双关语),因为在安装了后端API之后,您需要重新访问它们。 目前, getAllTodos操作使用伪造的getAllTodos提交loadTodos突变。 稍后,此操作将调用您的API来检索用户的待办事项,然后对从API返回的项目进行相同的更改。

Note: Because the starter template includes Babel and the transform-async-to-generator plugin, the new async/await syntax in ES2017 is available. I love async/await, because it makes dealing with async things like API calls much easier (no more big chains of Promises, or callback hell). As you’ll see in the next section, C# uses the same syntax!

注意:由于入门模板包括Babel和transform-async-to-generator插件,因此ES2017中新的async / await语法可用。 我喜欢异步/等待,因为它使处理诸如API调用之类的异步事情变得更加轻松(不再需要更多的Promises链或回调地狱)。 正如您将在下一节中看到的那样,C#使用相同的语法!

运行应用 (Run the app)

You still need to add the backend API and authentication bits, but let’s take a quick break and see what you’ve built so far. Start up the app with dotnet run and browse to http://localhost:5000. Log in with a fake username and password:

您仍然需要添加后端API和身份验证位,但是让我们快速休息一下,看看到目前为止您已经完成了什么。 使用dotnet run启动应用程序,然后浏览至http:// localhost:5000 。 使用假的用户名和密码登录:

Logged in with a fake user

Tip: If you need to fix bugs or make changes, you don’t need to stop and restart the server with dotnet run again. As soon as you modify any of your Vue or JavaScript files, the frontend app will be recompiled automatically. Try making a change to the Dashboard component and see it appear instantly in your browser (like magic).

提示:如果需要修复错误或进行更改,则无需停止并重新启动dotnet run重新启动服务器。 修改任何Vue或JavaScript文件后,前端应用程序将自动重新编译。 尝试对Dashboard组件进行更改,然后立即将其显示在浏览器中(就像魔术一样)。

The to-do item is fake, but your app is very real! You’ve set up Vue.js, built components and routing, and added state management with Vuex. The next step is adding the backend API with ASP.NET Core. Grab a refill of coffee and let’s dive in!

待办事项是假的,但您的应用程序是真实的! 您已经设置了Vue.js,构建了组件和路由,并使用Vuex添加了状态管理。 下一步是使用ASP.NET Core添加后端API。 拿杯咖啡,让我们潜入水中吧!

使用ASP.NET Core添加API ( Add APIs with ASP.NET Core )

The user’s to-do items will be stored in an online database so they can be accessed anywhere. Your frontend app won’t access this data directly. Instead, the Vuex actions will call your backend API, which will retrieve the data and return it to the frontend.

用户的待办事项将存储在在线数据库中,以便可以在任何地方访问它们。 您的前端应用程序将无法直接访问此数据。 相反,Vuex操作将调用您的后端API,该API将检索数据并将其返回到前端。

This pattern (JavaScript code calling a backend API) is a common way to architect modern apps. The API can be written in any language you prefer. In this tutorial, you’ll write the API in C# using the ASP.NET Core framework.

这种模式(JavaScript代码调用后端API)是构建现代应用程序的常用方法。 可以使用您喜欢的任何语言编写API。 在本教程中,您将使用ASP.NET Core框架以C#编写API。

Tip: If you want an introduction to ASP.NET Core from the ground up, check out my free Little ASP.NET Core Book!

提示:如果您想从头开始介绍ASP.NET Core,请查阅我免费的Little ASP.NET Core书

The template you started from already includes the scaffolding you need for a basic ASP.NET Core project:

您开始使用的模板已经包含了基本ASP.NET Core项目所需的支架:

  • The Startup.cs file, which configures the project and defines the middleware pipeline. You’ll modify this file later.

    Startup.cs文件,该文件配置项目并定义中间件管道。 您稍后将修改此文件。
  • A pair of controllers in the aptly-named Controllers folder.

    恰当命名的Controllers文件夹中的一对控制器。

In ASP.NET Core, Controllers handle requests to specific routes in your app or API. The HomeController contains boilerplate code that handles the root route / and renders your frontend app. You won’t need to modify it. The sample WeatherController, on the other hand, can be deleted. Time to write a new controller!

在ASP.NET Core中,控制器处理对您的应用程序或API中特定路由的请求。 在HomeController包含样板代码,处理根路径/和呈现您的前端应用程序。 您无需修改​​它。 另一方面,可以删除示例WeatherController 。 是时候编写一个新的控制器了!

Create a new file in the Controllers folder called TodoController.cs:

在Controllers文件夹中创建一个名为TodoController.cs的新文件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Vue2Spa.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : Controller
    {
        // Handles GET /api/todo
        [HttpGet]
        public async Task<IActionResult> GetAllTodos()
        {
            // TODO: Get to-do items and return to frontend
        }
    }
}

The Route attribute at the top of the controller with a value of "api/[controller]" tells ASP.NET Core that this controller will be handling the route http://yourdomain/api/todo. Inside the controller, the GetAllTodos method is decorated with the HttpGet attribute to indicate that it should handle an HTTP GET. When your frontend code makes a GET to /api/todo, the GetAllTodos method will run.

控制器顶部的Route属性,其值为"api/[controller]"告诉ASP.NET Core该控制器将处理路由http://yourdomain/api/todo 。 在控制器内部, GetAllTodos方法装饰有HttpGet属性,以指示它应处理HTTP GET。 当您的前端代码对/api/todo进行GET时,将运行GetAllTodos方法。

定义模型 (Define a model)

Before you return to-do items from the method, you need to define the structure of the object, or model, you’ll return. Since C# is a statically-typed language (as opposed to a dynamic language like JavaScript), it’s typical to define these types in advance.

在从方法返回待办事项之前,需要定义对象或模型的结构,然后再返回。 由于C#是一种静态类型的语言(与动态语言(如JavaScript)相反),因此通常需要预先定义这些类型。

Create a new folder at the root of the project (next to to the Controllers folder) called Models. Inside, create a file called TodoItemModel.cs:

在项目的根目录(在Controllers文件夹旁边)创建一个名为Models的新文件夹。 在内部,创建一个名为TodoItemModel.cs的文件:

using System;

namespace Vue2Spa.Models
{
    public class TodoItemModel
    {
        public Guid Id { get; set; }

        public string Text { get; set; }

        public bool Completed { get; set; }
    }
}

This model defines a few simple properties for all to-do items: an ID, some text, and a boolean indicating whether the to-do is complete. When you fill this model with data and return it from your controller, ASP.NET Core will automatically serialize these properties to JSON that your frontend code can easily consume.

该模型为所有待办事项定义了一些简单的属性:一个ID,一些文本和一个指示待办事项是否完成的布尔值。 当用数据填充此模型并将其从控制器返回时,ASP.NET Core会将这些属性自动序列化为JSON,您的前端代码可以轻松使用这些属性。

定义服务 (Define a service)

You could return a model directly from the GetAllTodos method, but it’s common to add another layer. In the next section, you’ll add code to look up the user’s profile in Okta and retrieve their to-do items. To keep everything organized, you can create a service that will wrap up the code that retrieves the user’s to-do items.

您可以直接从GetAllTodos方法返回模型,但是添加另一层很常见。 在下一部分中,您将添加代码以在Okta中查找用户的个人资料并检索其待办事项。 为了使所有内容井井有条,您可以创建一个服务 ,该服务将包装用于检索用户待办事项的代码。

Create one more folder in the project root called Services, and add a file called ITodoItemService.cs:

在项目根目录中再创建一个名为Services的文件夹,并添加一个名为ITodoItemService.cs的文件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Vue2Spa.Models;

namespace Vue2Spa.Services
{
    public interface ITodoItemService
    {
        Task<IEnumerable<TodoItemModel>> GetItems(string userId);

        Task AddItem(string userId, string text);

        Task UpdateItem(string userId, Guid id, TodoItemModel updatedData);

        Task DeleteItem(string userId, Guid id);
    }
}

This file describes an interface -- a feature in C# (and many other languages) that defines the methods available in a particular class without providing the “concrete” implementation. This makes it easy to test and swap out implementations during development.

该文件描述了一个接口 -一种C#(以及许多其他语言)的功能,该功能定义了特定类中可用的方法,而没有提供“具体”的实现。 这使得在开发过程中测试和交换实现变得容易。

Create a new file called FakeTodoItemService.cs that will be a temporary implementation until you add the connection to Okta in the next section:

创建一个名为FakeTodoItemService.cs的新文件,该文件将是一个临时实现,直到在下一节中将连接添加到Okta为止:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Vue2Spa.Models;

namespace Vue2Spa.Services
{
    public class FakeTodoItemService : ITodoItemService
    {
        public Task<IEnumerable<TodoItemModel>> GetItems(string userId)
        {
            var todos = new[]
            {
                new TodoItemModel { Text = "Learn Vue.js", Completed = true },
                new TodoItemModel { Text = "Learn ASP.NET Core" }
            };

            return Task.FromResult(todos.AsEnumerable());
        }

        public Task AddItem(string userId, string text)
        {
            throw new NotImplementedException();
        }

        public Task DeleteItem(string userId, Guid id)
        {
            throw new NotImplementedException();
        }

        public Task UpdateItem(string userId, Guid id, TodoItemModel updatedData)
        {
            throw new NotImplementedException();
        }
    }
}

This dummy service will always return the same to-do items. You’ll replace it later, but it will let you test the app and make sure everything is working.

该虚拟服务将始终返回相同的待办事项。 您稍后将替换它,但是它将使您能够测试应用程序并确保一切正常。

使用服务 (Use the service)

With the controller, model, and service all in place, all you need to do is connect them together. First, open up the Startup.cs file and add this line anywhere in the ConfigureServices method:

有了控制器,模型和服务后,您需要做的就是将它们连接在一起。 首先,打开Startup.cs文件,并将此行添加到ConfigureServices方法中的任何位置:

services.AddSingleton<ITodoItemService, FakeTodoItemService>();

You’ll also need to add this using statement to the top of the file:

您还需要将此using语句添加到文件顶部:

using Vue2Spa.Services;

Adding the new service to the ConfigureServices method makes it available throughout the ASP.NET Core project. In your TodoController, add this code at the top of the class:

通过将新服务添加到ConfigureServices方法中,可以在整个ASP.NET Core项目中使用它。 在您的TodoController ,将此代码添加到类的顶部:

public class TodoController : Controller
{
    private readonly ITodoItemService _todoItemService;

    public TodoController(ITodoItemService todoItemService)
    {
        _todoItemService = todoItemService;
    }

    // Existing code...

Adding this code causes ASP.NET Core to inject an ITodoItemService object into the controller. Because you’re using the interface here, your controller doesn’t know (or care) which implementation of the ITodoItemService it receives. It’s currently the FakeTodoItemService, but later it’ll be a more interesting (and real) implementation.

添加此代码将导致ASP.NET Core将ITodoItemService对象注入到控制器中。 因为您在这里使用接口,所以您的控制器不知道(或不在乎)它收到的ITodoItemService实现。 当前是FakeTodoItemService ,但稍后它将是一个更有趣(更真实)的实现。

Add this using statement at the top of the file:

在文件顶部添加此using语句:

using Vue2Spa.Services;

Finally, add this code to the GetAllTodos method:

最后,将此代码添加到GetAllTodos方法:

var userId = "123"; // TODO: Get actual user ID
var todos = await _todoItemService.GetItems(userId);

return Ok(todos);

When a request comes into the GetAllTodos method, the controller calls the ITodoItemService to get the to-do items for the current user, and then returns HTTP OK (200) with the to-do items.

当请求进入GetAllTodos方法时,控制器将调用ITodoItemService来获取当前用户的待办事项,然后返回HTTP OK(200)和待办事项。

That takes care of the backend API (for now). The frontend now needs to be updated to call the /api/todo route to get the user’s to-do items. In actions.js, update the getAllTodos function:

这样就可以处理后端API(目前)。 现在需要更新前端,以调用/api/todo路由来获取用户的/api/todo 。 在actions.js ,更新getAllTodos函数:

async getAllTodos({ commit }) {
  let response = await axios.get('/api/todo')

  if (response && response.data) {
    let updatedTodos = response.data
    commit('loadTodos', updatedTodos)
  }
},

The new action code uses the axios HTTP library to make a request to the backend on the /api/todo route, which will be handled by the GetAllTodos method on the TodoController. If data is returned, the loadTodos mutation is committed and the Vuex store is updated with the user’s to-do items. The Dashboard view will automatically see the updated data in the store and render the items in the browser.

新的操作代码使用axios HTTP库/api/todo路由上的后端发出请求,该请求将由GetAllTodos上的TodoController方法处理。 如果返回了数据,则会提交loadTodos突变,并使用用户的待办事项更新Vuex存储。 仪表板视图将自动查看商店中的更新数据,并在浏览器中呈现项目。

Ready to test it out? Run the project with dotnet run and browse to http://localhost:5000:

准备测试了吗? 使用dotnet run运行项目并浏览到http:// localhost:5000

Logged in with an Okta user

The data is still fake, but less fake than before! You’ve successfully connected the backend and frontend and have data flowing between them.

数据仍然是伪造的,但是伪造的次数比以前少! 您已经成功连接了后端和前端,并在它们之间流动了数据。

The final step is to add authentication and real data storage to the app. You’re almost there!

最后一步是向应用程序添加身份验证和真实数据存储。 您快到了!

使用Okta添加身份和安全性 ( Add identity and security with Okta )

Okta is a cloud-hosted identity API that makes it easy to add authentication, authorization, and user management to your web and mobile apps. You’ll use it in this project to:

Okta是一种云托管的身份API,可轻松向Web和移动应用添加身份验证,授权和用户管理。 您将在此项目中使用它来:

  • Add functionality to the Login component

    向登录组件添加功能
  • Require authentication on the backend API

    要求在后端API上进行身份验证
  • Store each user’s to-do items securely

    安全地存储每个用户的待办事项

To get started, sign up for a free Okta Developer account. After you activate your new account (called an Okta Organization, or Org for short), click Applications at the top of the screen. Choose Single-Page App and change both the base URI and login redirect URI to http://localhost:5000:

首先,注册一个免费的Okta Developer帐户 。 激活新帐户(称为Okta Organization,简称Org)后,请点击屏幕顶部的应用程序。 选择单页应用程序,然后将基本URI和登录重定向URI都更改为http://localhost:5000

Okta application settings

After you click Done, you’ll be redirected to the new application’s details. Scroll down and copy the Client ID -- you’ll need it in a minute.

单击“完成”后,您将被重定向到新应用程序的详细信息。 向下滚动并复制客户端ID-一分钟内将需要它。

添加自定义用户个人资料字段 (Add a custom user profile field)

By default, Okta stores basic information about your users: first name, last name, email, and so on. If you want to store more, Okta supports custom profile fields that can store any type of user data you need. You can use this to store the to-do items for each user right on the user profile -- no extra database needed!

默认情况下,Okta存储有关您的用户的基本信息:名字,姓氏,电子邮件等。 如果要存储更多内容,Okta支持可存储您需要的任何类型的用户数据的自定义配置文件字段。 您可以使用它在用户个人资料上存储每个用户的待办事项-无需额外的数据库!

To add a custom field, open the Users menu at the top of the screen and click on Profile Editor. On the first row (Okta User), click Profile to edit the default user profile. Add a string attribute called todos:

要添加自定义字段,请打开屏幕顶部的“用户”菜单,然后单击“配置文件编辑器”。 在第一行(Okta用户)上,单击“配置文件”以编辑默认用户配置文件。 添加一个名为todos的字符串属性:

Add custom field in Okta profile

Next, you need to connect your frontend code to Okta.

接下来,您需要将前端代码连接到Okta。

添加Okta Auth SDK (Add the Okta Auth SDK)

The Okta Auth SDK provides methods that make it easy to authenticate users from JavaScript code. Install it with npm:

Okta Auth SDK提供的方法可轻松地从JavaScript代码对用户进行身份验证。 用npm安装它:

npm install @okta/okta-auth-js@1.11.0

Create a file in the ClientApp folder called oktaAuth.js that holds the Auth SDK configuration and makes the client available to the rest of your Vue app:

在ClientApp文件夹中创建一个名为oktaAuth.js的文件,该文件包含Auth SDK配置,并使客户端可用于其余的Vue应用程序:

import OktaAuth from '@okta/okta-auth-js'

const org = '{{yourOktaOrgUrl}}',
      clientId = '{{appClientId}}',
      redirectUri = 'http://localhost:5000',
      authorizationServer = 'default'

const oktaAuthClient = new OktaAuth({
  url: org,
  issuer: authorizationServer,
  clientId,
  redirectUri
})

export default {
  client: oktaAuthClient
}

Replace {{yourOktaOrgUrl}} with your Okta Org URL, which usually looks like this: https://dev-12345.oktapreview.com. You can find it in the top right corner of the Dashboard page.

{{yourOktaOrgUrl}}替换为您的Okta Org URL,通常如下所示: https://dev-12345.oktapreview.com : {{yourOktaOrgUrl}} 。 您可以在“信息中心”页面的右上角找到它。

Next, paste the Client ID you copied from the application you created a minute ago into the clientId property.

接下来,将您从一分钟前创建的应用程序复制的客户端ID粘贴到clientId属性中。

The checkLoggedIn, login, and logout actions can now be replaced with real implementations in actions.js:

现在,可以使用actions.js实际实现来替换checkLoggedInloginlogout操作:

checkLoggedIn({ commit }) {
  if (oktaAuth.client.tokenManager.get('access_token')) {
    let idToken = oktaAuth.client.tokenManager.get('id_token')
    commit('loggedIn', idToken.claims)
  }
},

async login({ dispatch, commit }, data) {
  let authResponse
  try {
    authResponse = await oktaAuth.client.signIn({
      username: data.email,
      password: data.password
    });
  }
  catch (err) {
    let message = err.message || 'Login error'
    dispatch('loginFailed', message)
    return
  }

  if (authResponse.status !== 'SUCCESS') {
    console.error("Login unsuccessful, or more info required", response.status)
    dispatch('loginFailed', 'Login error')
    return
  }

  let tokens
  try {
    tokens = await oktaAuth.client.token.getWithoutPrompt({
      responseType: ['id_token', 'token'],
      scopes: ['openid', 'email', 'profile'],
      sessionToken: authResponse.sessionToken,
    })
  }
  catch (err) {
    let message = err.message || 'Login error'
    dispatch('loginFailed', message)
    return
  }

  // Verify ID token validity
  try {
    await oktaAuth.client.token.verify(tokens[0])
  } catch (err) {
    dispatch('loginFailed', 'An error occurred')
    console.error('id_token failed validation')
    return
  }

  oktaAuth.client.tokenManager.add('id_token', tokens[0]);
  oktaAuth.client.tokenManager.add('access_token', tokens[1]);

  commit('loggedIn', tokens[0].claims)
},

async logout({ commit }) {
  oktaAuth.client.tokenManager.clear()
  await oktaAuth.client.signOut()
  commit('loggedOut')
},

These actions delegate to the Okta Auth SDK, which calls the Okta authentication API to log the user in and get access and ID tokens for the user via OpenID Connect. The Auth SDK also stores and manages the tokens for your app.

这些操作委派给Okta Auth SDK,后者调用Okta身份验证API来登录用户并通过OpenID Connect获取用户的访问和ID令牌。 Auth SDK还可以存储和管理应用程序的令牌。

You’ll also need to add an import statement at the top of actions.js:

您还需要在actions.js顶部添加一个import语句:

import oktaAuth from '../oktaAuth'

Try it out: run the server with dotnet run and try logging in with the email and password you used to sign up for Okta:

试用:用dotnet run运行服务器,然后尝试使用您用来注册Okta的电子邮件和密码登录:

Logged in and sending real API requests

Try logging in, refreshing the page (you should still be logged in!), and logging out.

尝试登录,刷新页面(您仍然应该登录!),然后注销。

That takes care of authenticating the user on the frontend. The Vuex store will keep track of the authentication state, and the Okta Auth SDK will handle login, logout, and keeping the user’s tokens fresh.

这需要在前端对用户进行身份验证。 Vuex存储将跟踪身份验证状态,而Okta Auth SDK将处理登录,注销并保持用户令牌的最新状态。

To secure the backend API, you need to configure ASP.NET Core to use token authentication and require a token when the frontend code makes a request.

为了保护后端API,您需要将ASP.NET Core配置为使用令牌身份验证,并且在前端代码发出请求时需要令牌。

添加API令牌认证 (Add API token authentication)

Under the hood, the Okta Auth SDK uses OpenID Connect to get access and ID tokens when the user logs in. The ID token is used to display the user’s name in the Vue app, and the access token can be used to secure the backend API.

在后台,Okta Auth SDK在用户登录时使用OpenID Connect获取访问和ID令牌。ID令牌用于在Vue应用程序中显示用户名,访问令牌可用于保护后端API。 。

Open up the Startup.cs file and add this code to the ConfigureServices method:

打开Startup.cs文件,然后将此代码添加到ConfigureServices方法中:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.Authority = "{{yourOktaOrgUrl}}/oauth2/default";
    options.Audience = "api://default";
});

This code adds token authentication to the ASP.NET Core authentication system. With this in place, your frontend code will need to attach an access token to requests in order to access the API.

此代码将令牌身份验证添加到ASP.NET Core身份验证系统。 使用此功能后,您的前端代码将需要在请求中附加访问令牌才能访问API。

Make sure you replace {{yourOktaOrgUrl}} with your Okta Org URL (find it in the top-right of your Okta developer console’s Dashboard).

确保将{{yourOktaOrgUrl}}替换为Okta Org URL(在Okta开发人员控制台的仪表板的右上角找到它)。

You’ll also need to add this using statement at the top of the file:

您还需要在文件顶部添加此using语句:

using Microsoft.AspNetCore.Authentication.JwtBearer;

And this line down in the Configure method:

这行在Configure方法中:

app.UseStaticFiles();

// Add this:
app.UseAuthentication();

app.UseMvc(...

The Configure method defines the middleware pipeline for an ASP.NET Core project, or the list of handlers that modify incoming requests. Adding UseAuthentication() makes it possible for you to require authentication for your controllers.

Configure方法定义ASP.NET Core项目的中间件管道或修改传入请求的处理程序列表。 添加UseAuthentication()可以让您要求对控制器进行身份验证。

Controllers need to opt-in to an authentication check, by adding the [Authorize] attribute at the top of the controller. Add this in the TodoController:

控制器需要通过在控制器顶部添加[Authorize]属性来选择参加身份验证检查。 将此添加到TodoController

[Route("api/[controller]")]
[Authorize] // Add this
public class TodoController : Controller
{
    // ...

If you tried running the app and looking at your browser’s network console, you’d see a failed API request:

如果尝试运行该应用程序并查看浏览器的网络控制台,则会看到失败的API请求:

API request returns 401

The TodoController is responding with 401 Unauthorized because it now requires a valid token to access the /api/todo route, and your frontend code isn’t sending a token.

TodoController的响应为401 Unauthorized,因为它现在需要有效的令牌才能访问/api/todo路由,并且您的前端代码未发送令牌。

Open up actions.js once more and add a small function below sleep that attaches the user’s token to the HTTP Authorization header:

再次打开actions.js ,并在sleep添加一个小的函数,该函数将用户的令牌附加到HTTP Authorization标头:

const addAuthHeader = () => {
  return {
    headers: {
        'Authorization': 'Bearer '
            + oktaAuth.client.tokenManager.get('access_token').accessToken
    }
  }
}

Then, update the code that calls the backend in the getAllTodos function:

然后,更新在getAllTodos函数中调用后端的代码:

let response = await axios.get('/api/todo', addAuthHeader())

Refresh the page (or start the server) and the now-authenticated request will succeed once again.

刷新页面(或启动服务器),现在已认证的请求将再次成功。

添加Okta .NET SDK (Add the Okta .NET SDK)

You’re almost done! The final task is to store and retrieve the user’s to-do items in the Okta custom profile attribute you set up earlier. You’ll use the Okta .NET SDK to do this in a few lines of backend code.

你几乎完成! 最后的任务是在您之前设置的Okta自定义配置文件属性中存储和检索用户的待办事项。 您将使用Okta .NET SDK在几行后端代码中执行此操作。

Stop the ASP.NET Core server (if it’s running), and install the Okta .NET SDK in your project with the dotnet tool:

停止ASP.NET Core服务器(如果正在运行),然后使用dotnet工具在项目中安装Okta .NET SDK:

dotnet add package Okta.Sdk --version 1.0.0-alpha4

Open the Startup.cs file again and add this code anywhere in the ConfigureServices method:

再次打开Startup.cs文件,并将此代码添加到ConfigureServices方法中的任何位置:

services.AddSingleton<IOktaClient>(new OktaClient(new OktaClientConfiguration
{
    OrgUrl = "{{yourOktaOrgUrl}}",
    Token = Configuration["okta:token"]
}));

This makes the Okta .NET SDK available to the whole project as a service. You’ll also need to add these lines to the top of the file:

这使得Okta .NET SDK可以作为服务用于整个项目。 您还需要将以下行添加到文件的顶部:

using Okta.Sdk;
using Okta.Sdk.Configuration;

Remember to replace {{yourOktaOrgUrl}} with your Okta Org URL.

请记住用您的Okta组织网址替换{{yourOktaOrgUrl}}

获取Okta API令牌 (Get an Okta API token)

The Okta SDK needs an Okta API token to call the Okta API. This is used for management tasks (like storing and retrieving user profile data), and is separate from the Bearer tokens you’re using for user authentication.

Okta SDK需要Okta API令牌才能调用Okta API。 这用于管理任务(例如存储和检索用户配置文件数据),并且与用于用户身份验证的承载令牌分离。

Generate an Okta API token in the Okta developer console by hovering on API and clicking Tokens. Create a token and copy the value.

将鼠标悬停在API上并单击“令牌”,即可在Okta开发人员控制台中生成Okta API令牌。 创建一个令牌并复制该值。

The Okta API token is sensitive and should be protected, because it allows you to do any action in the Okta API, including deleting users and applications! Because of this, you shouldn’t store it in code that gets checked into source control. Instead, use the .NET Secret Manager tool.

Okta API令牌很敏感,应该受到保护,因为它允许您在Okta API中执行任何操作,包括删除用户和应用程序! 因此,您不应将其存储在已检查到源代码管理中的代码中。 而是使用.NET Secret Manager工具。

Tip: If you’re using Visual Studio 2017 on Windows, you can right-click the project in the Solution Explorer and choose Manage user secrets. Then you can skip the installation steps and jump down to adding the secret value with dotnet user-secrets set.

提示:如果您在Windows上使用Visual Studio 2017,则可以在解决方案资源管理器中右键单击该项目,然后选择管理用户机密 。 然后,您可以跳过安装步骤,然后跳至添加带有dotnet user-secrets set的秘密值。

Open up the Vue2Spa.csproj file and add this line near the existing DotNetCliToolReference line:

打开Vue2Spa.csproj文件,并将此行添加到现有DotNetCliToolReference行附近:

<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />

The .csproj file is the main project file for any ASP.NET Core application. It defines the packages that are installed in the project, and some other metadata. Adding this line installs the Secret Manager tool in this project. Run a package restore to make sure the tool gets installed, and test it out:

.csproj文件是任何ASP.NET Core应用程序的主要项目文件。 它定义了项目中安装的软件包以及其他一些元数据。 添加此行将在此项目中安装Secret Manager工具。 运行软件包还原以确保已安装该工具,然后对其进行测试:

dotnet restore
dotnet user-secrets -h

Next, add another line near the top of the Vue2Spa.csproj file, right under <TargetFramework>:

接下来,在<TargetFramework>下的Vue2Spa.csproj文件顶部附近添加另一行:

<UserSecretsId>{{some random value}}</UserSecretsId>

Generate a random GUID as the ID value, and save the project file.

生成随机GUID作为ID值,然后保存项目文件。

Grab the Okta API token value and store it using the Secret Manager:

获取Okta API令牌值并使用Secret Manager将其存储:

dotnet user-secretsset okta:token {{oktaApiToken}}

To make the values stored in the Secret Manager available to your application, you need to add it as a configuration source in Startup.cs. At the top of the file, in the Startup (constructor) method, add this code:

为了使存储在Secret Manager中的值可用于您的应用程序,您需要将其添加为Startup.cs的配置源。 在文件顶部的Startup (构造函数)方法中,添加以下代码:

// ... existing code
.AddEnvironmentVariables();

// Add this:
if (env.IsDevelopment())
{
    builder.AddUserSecrets<Startup>();
}

// Existing code... 
Configuration = builder.Build();

With that, the Okta .NET SDK will have an API token it can use to call the Okta API. You’ll use the SDK to store and retrieve the user’s to-do items.

这样,Okta .NET SDK将具有一个API令牌,可用于调用Okta API。 您将使用SDK来存储和检索用户的待办事项。

使用Okta进行用户数据存储 (Use Okta for user data storage)

Remember the FakeTodoItemService you created before? It’s time to replace it with a new service that uses Okta to store the user’s to-do items. Create OktaTodoItemService.cs in the Services foldre:

还记得您之前创建的FakeTodoItemService吗? 现在该替换为使用Okta来存储用户的待办事项的新服务了。 在“服务”文件OktaTodoItemService.cs中创建OktaTodoItemService.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Okta.Sdk;
using Vue2Spa.Models;

namespace Vue2Spa.Services
{
    public class OktaTodoItemService : ITodoItemService
    {
        private const string TodoProfileKey = "todos";

        private readonly IOktaClient _oktaClient;

        public OktaTodoItemService(IOktaClient oktaClient)
        {
            _oktaClient = oktaClient;
        }

        private IEnumerable<TodoItemModel> GetItemsFromProfile(IUser oktaUser)
        {
            if (oktaUser == null)
            {
                return Enumerable.Empty<TodoItemModel>();
            }

            var json = oktaUser.Profile.GetProperty<string>(TodoProfileKey);
            if (string.IsNullOrEmpty(json))
            {
                return Enumerable.Empty<TodoItemModel>();
            }

            return JsonConvert.DeserializeObject<TodoItemModel[]>(json);
        }

        private async Task SaveItemsToProfile(IUser user, IEnumerable<TodoItemModel> todos)
        {
            var json = JsonConvert.SerializeObject(todos.ToArray());

            user.Profile[TodoProfileKey] = json;
            await user.UpdateAsync();
        }

        public async Task AddItem(string userId, string text)
        {
            var user = await _oktaClient.Users.GetUserAsync(userId);

            var existingItems = GetItemsFromProfile(user)
                .ToList();

            existingItems.Add(new TodoItemModel
            {
                Id = Guid.NewGuid(),
                Completed = false,
                Text = text
            });

            await SaveItemsToProfile(user, existingItems);
        }

        public async Task DeleteItem(string userId, Guid id)
        {
            var user = await _oktaClient.Users.GetUserAsync(userId);

            var updatedItems = GetItemsFromProfile(user)
                .Where(item => item.Id != id);

            await SaveItemsToProfile(user, updatedItems);
        }

        public async Task<IEnumerable<TodoItemModel>> GetItems(string userId)
        {
            var user = await _oktaClient.Users.GetUserAsync(userId);
            return GetItemsFromProfile(user);
        }

        public async Task UpdateItem(string userId, Guid id, TodoItemModel updatedData)
        {
            var user = await _oktaClient.Users.GetUserAsync(userId);

            var existingItems = GetItemsFromProfile(user)
                .ToList();

            var itemToUpdate = existingItems
                .FirstOrDefault(item => item.Id == id);
            if (itemToUpdate == null)
            {
                return;
            }

            // Update the item with the new data
            itemToUpdate.Completed = updatedData.Completed;
            if (!string.IsNullOrEmpty(updatedData.Text))
            {
                itemToUpdate.Text = updatedData.Text;
            }

            await SaveItemsToProfile(user, existingItems);
        }
    }
}

Okta custom profile fields are limited to storing primitives likes strings and numbers, but you’re using the TodoModel type to represent to-do items. This service serializes the strongly-typed items to a JSON array and stores them as a string. It’s not the fastest data storage mechanism, but it works!

Okta自定义配置文件字段仅限于存储诸如字符串和数字之类的基元,但是您使用的是TodoModel类型来表示待办事项。 该服务将强类型的项目序列化为JSON数组,并将其存储为字符串。 它不是最快的数据存储机制,但可以!

Since you’ve created a new service class, update the line in the Startup.cs file to use the OktaTodoitemService instead of the FakeTodoItemService:

由于创建了新的服务类,因此更新Startup.cs文件中的行以使用OktaTodoitemService而不是FakeTodoItemService

services.AddSingleton<ITodoItemService, OktaTodoItemService>();

The TodoController will now use the new service when it interacts with the ITodoItemService interface. Update the controller with some new code and methods:

TodoController现在将使用新的服务时,它与交互ITodoItemService接口。 使用一些新的代码和方法更新控制器:

// GET /api/todo
[HttpGet]
public async Task<IActionResult> GetAllTodos()
{
    var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
    if (string.IsNullOrEmpty(userId)) return BadRequest();

    var todos = await _todoItemService.GetItems(userId);
    var todosInReverseOrder = todos.Reverse();

    return Ok(todosInReverseOrder);
}

// POST /api/todo
[HttpPost]
public async Task<IActionResult> AddTodo([FromBody]TodoItemModel newTodo)
{
    if (string.IsNullOrEmpty(newTodo?.Text)) return BadRequest();

    var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
    if (string.IsNullOrEmpty(userId)) return BadRequest();

    await _todoItemService.AddItem(userId, newTodo.Text);

    return Ok();
}

// POST /api/todo/{id}
[HttpPost("{id}")]
public async Task<IActionResult> UpdateTodo(Guid id, [FromBody]TodoItemModel updatedData)
{
    var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
    if (string.IsNullOrEmpty(userId)) return BadRequest();

    await _todoItemService.UpdateItem(userId, id, updatedData);

    return Ok();
}

// DELETE /api/todo/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodo(Guid id)
{
    var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
    if (string.IsNullOrEmpty(userId)) return BadRequest();

    try
    {
        await _todoItemService.DeleteItem(userId, id);
    }
    catch (Exception ex)
    {
        return BadRequest(ex.Message);
    }

    return Ok();
}

And add one more using statement at the top:

并在顶部再添加一个using语句:

using Vue2Spa.Models;

In each method, the first step is to extract the user’s ID from the Bearer token attached to the incoming request. The ID is then passed along to the service method, and the Okta .NET SDK uses it to find the right user’s profile.

在每种方法中,第一步是从附加到传入请求的Bearer令牌中提取用户的ID。 然后将ID传递给服务方法,Okta .NET SDK使用它来查找正确的用户的配置文件。

Finish out the frontend code by adding the last few actions to actions.js:

通过将最后的几个动作添加到actions.js来完成前端代码:

async addTodo({ dispatch }, data) {
    await axios.post(
      '/api/todo',
      { text: data.text },
      addAuthHeader())

    await dispatch('getAllTodos')
  },

  async toggleTodo({ dispatch }, data) {
    await axios.post(
      '/api/todo/' + data.id,
      { completed: data.completed },
      addAuthHeader())

    await dispatch('getAllTodos')
  },

  async deleteTodo({ dispatch }, id) {
    await axios.delete('/api/todo/' + id, addAuthHeader())
    await dispatch('getAllTodos')
  }

Start the server one more time with dotnet run and try adding a real to-do item to the list:

通过dotnet run再启动服务器一次,然后尝试将真正的待办事项添加到列表中:

Final application

学到更多! ( Learn More! )

If you made it all the way to the end, congratulations! I’d love to hear about what you built. Shoot me a tweet @nbarbettini and tell me about it!

如果您一路走到尽头,恭喜! 我很想知道你的建造。 给我发一条推文@nbarbettini并告诉我!

Feel free to download the final project’s code on GitHub.

可以在GitHub上免费下载最终项目的代码

If you want to keep building, here’s what you could do next:

如果要继续构建,可以执行以下操作:

  • Add a form in Vue (and a controller on the backend) to let a new user create an account

    在Vue中添加一个表单(在后端添加一个控制器),让新用户创建一个帐户
  • Store a timestamp when a new to-do item is added, and display it with each item

    添加新的待办事项时存储时间戳,并与每个项目一起显示
  • Speed up the frontend by storing changes in the Vuex store immediately (before the API response arrives)

    通过立即将更改存储在Vuex存储中(在API响应到达之前)来加快前端速度

For more Vue.js inspiration, check our other recent posts:

有关Vue.js的更多灵感,请查看我们最近发布的其他文章:

Happy coding!

编码愉快!

翻译自: https://scotch.io/tutorials/build-a-secure-to-do-app-with-vuejs-aspnet-core-and-okta

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值