gin react_如何使用Go,Gin和React构建Web应用

芮瑾瑜
2023-12-01

gin react

by Francis Sunday

弗朗西斯(星期日)

如何使用Go,Gin和React构建Web应用 (How to build a web app with Go, Gin, and React)

This article was originally posted on My Blog

本文最初发布在我的博客上

TL;DR: In this tutorial, I’ll show you how easy it is to build a web application with Go and the Gin framework and add authentication to it. Check out the Github repo for the code we’re going to write.

TL; DR:在本教程中,我将向您展示使用Go和Gin框架构建Web应用程序并向其添加身份验证是多么容易。 查看Github存储 ,了解我们将要编写的代码。

Gin is a high-performance micro-framework. It delivers a very minimalistic framework that carries with it only the most essential features, libraries, and functionalities needed to build web applications and microservices. It makes it simple to build a request handling pipeline from modular, reusable pieces. It does this by allowing you to write middleware that can be plugged into one or more request handlers or groups of request handlers.

杜松子酒是一种高性能的微框架。 它提供了一个非常简约的框架,其中仅包含构建Web应用程序和微服务所需的最基本的功能,库和功能。 它使从模块化,可重用的块构建请求处理管道变得很简单。 通过允许您编写可插入一个或多个请求处理程序或一组请求处理程序的中间件,可以做到这一点。

杜松子酒功能 (Gin features)

Gin is a fast, simple yet fully featured and very efficient web framework for Go. Check out some of the features below that make it a worthy framework to consider for your next Golang project.

Gin是用于Go的快速,简单但功能齐全且非常有效的Web框架。 查看下面的一些功能,这些功能使其成为值得考虑的下一个Golang项目的框架。

  • Speed: Gin is built for speed. The framework offers a Radix tree based routing and small memory footprint. No reflection. Predictable API performance.

    速度:杜松子酒是为了提高速度而设计的。 该框架提供了基于Radix树的路由和较小的内存占用。 没有反思。 可预测的API性能。

  • Crash-Free: Gin has the capability of catching crashes or panics during runtime, and can recover from them. This way your application will always be available.

    无崩溃 :Gin具有在运行时捕获崩溃或紧急事件的功能,并且可以从中恢复。 这样,您的应用程序将始终可用。

  • Routing: Gin provides a routing interface to allow you to express how your web application or API routes should look.

    路由: Gin提供了路由接口,可让您表达Web应用程序或API路由的外观。

  • JSON Validation: Gin can parse and validate JSON requests easily, checking for the existence of required values.

    JSON验证: Gin可以轻松解析和验证JSON请求,检查是否存在必需的值。

  • Error Management: Gin provides a convenient way to collect all the errors occurred during a HTTP request. Eventually, a middleware can write them to a log file or to a database and send them through the network.

    错误管理: Gin提供了一种方便的方法来收集HTTP请求期间发生的所有错误。 最终,中间件可以将它们写入日志文件或数据库中,并通过网络发送它们。

  • Built-In Rendering: Gin provides an easy to use API for JSON, XML, and HTML rendering.

    内置渲染: Gin为JSON,XML和HTML渲染提供了易于使用的API。

先决条件 (Prerequisites)

To follow along with this tutorial, you’ll need to have Go installed on your machine, a web browser to view the app, and a command line to execute build commands.

要跟随本教程的进行,您需要在计算机上安装Go,使用Web浏览器查看应用程序,并使用命令行执行构建命令。

Go, or as its normally called Golang, is a programming language developed by Google for building modern software. Go is a language designed to get stuff done efficiently and quickly. The key benefits of Go include:

Go或通常称为Golang ,是Google为开发现代软件而开发的一种编程语言。 Go是一种旨在高效快速地完成工作的语言。 Go的主要优势包括:

  • Strongly typed and garbage collected

    强类型和垃圾收集
  • Blazing fast compile times

    快速的编译时间
  • Concurrency built in

    内置并发
  • Extensive standard library

    广泛的标准库

Head over to the downloads section of the Go website to get Go running on your machine.

转至Go网站的下载部分 ,以使Go在您的计算机上运行。

使用Gin构建应用 (Building an app with Gin)

We’ll be building a simple joke listing app with Gin. Our app will list some silly dad jokes. We are going to add authentication to it, so that all logged-in users will have the privilege to like and view jokes.

我们将使用Gin构建一个简单的笑话清单应用程序。 我们的应用程序将列出一些愚蠢的爸爸笑话。 我们将向其中添加身份验证,以便所有登录的用户都有权喜欢和查看笑话。

This will allow us illustrate how Gin can be used to develop web applications and/or APIs.

这将使我们能够说明如何使用Gin开发Web应用程序和/或API。

We’ll be making use of the following functionalities offered by Gin:

我们将利用Gin提供的以下功能:

  • Middleware

    中间件
  • Routing

    路由
  • Routes Grouping

    路线分组

预备,准备,开始 (Ready, set, Go)

We will write our entire Go application in a main.go file. Since it’s a small application, it’s going to be easy to build the application with just go run from the terminal.

我们将整个Go应用程序写入main.go文件中。 由于它是一个小型应用程序,因此只需从终端go run即可轻松构建该应用程序。

We’ll create a new directory golang-gin in our Go workspace, and then a main.go file in it:

我们将在Go工作区中创建一个新目录golang-gin ,然后在其中创建一个main.go文件:

$ mkdir -p $GOPATH/src/github.com/user/golang-gin
$ cd $GOPATH/src/github.com/user/golang-gin
$ touch main.go

The content of the main.go file:

main.go文件的内容:

package main

import (
  "net/http"
  
  "github.com/gin-gonic/contrib/static"
  "github.com/gin-gonic/gin"
)

func main() {
  // Set the router as the default one shipped with Gin
  router := gin.Default()
  
  // Serve frontend static files
  router.Use(static.Serve("/", static.LocalFile("./views", true)))
  
  // Setup route group for the API
  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H {
        "message": "pong",
      })
    })
  }
  
  // Start and run the server
  router.Run(":3000")
}

We’ll need to create some more directories for our static files. In the same directory as the main.go file, let's create a views folder. In the views folder, create a js folder and an index.html file in it.

我们需要为静态文件创建更多目录。 在与main.go文件相同的目录中,让我们创建一个views文件夹。 在views文件夹中,创建一个js文件夹和一个index.html文件。

The index.html file will be very simple for now:

现在, index.html文件将非常简单:

<!DOCTYPE html>
<html>
<head>
  <title>Jokeish App</title>
</head>

<body>
  <h1>Welcome to the Jokeish App</h1>
</body>
</html>

Before we test what we have so far, let’s install the added dependencies:

在测试到目前为止我们拥有的东西之前,让我们安装添加的依赖项:

$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/gin-gonic/contrib/static

To see what’s working, we’ll need to start our server by running go run main.go.

要查看运行情况,我们需要通过运行go run main.go来启动服务器。

Once the application is running, navigate to http://localhost:3000 in your browser. If all went well, you should see level 1 header text Welcome to the Jokeish App displayed.

应用程序运行后,在浏览器中导航到http://localhost:3000 。 如果一切顺利,您应该看到显示的1级标题文本“ 欢迎使用Jokeish App”

定义API (Defining the API)

Let’s add some more code in our main.go file for our API definitions. We'll update our main function with two routes /jokes/ and /jokes/like/:jokeID to the route group /api/.

让我们在main.go文件中为API定义添加更多代码。 我们将使用两个路由/jokes//jokes/like/:jokeID到路由组/api/main函数。

func main() {
  // ... leave the code above untouched...
  
  // Our API will consit of just two routes
  // /jokes - which will retrieve a list of jokes a user can see
  // /jokes/like/:jokeID - which will capture likes sent to a particular joke
  api.GET("/jokes", JokeHandler)
  api.POST("/jokes/like/:jokeID", LikeJoke)
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"Jokes handler not implemented yet",
  })
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"LikeJoke handler not implemented yet",
  })
}

The content of the main.go file should look like this:

main.go文件的内容应如下所示:

package main

import (
  "net/http"
  
  "github.com/gin-gonic/contrib/static"
  "github.com/gin-gonic/gin"
)

func main() {
  // Set the router as the default one shipped with Gin
  router := gin.Default()
  
  // Serve frontend static files
  router.Use(static.Serve("/", static.LocalFile("./views", true)))
  
  // Setup route group for the API
  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H {
        "message": "pong",
      })
    })
  }
  // Our API will consit of just two routes
  // /jokes - which will retrieve a list of jokes a user can see
  // /jokes/like/:jokeID - which will capture likes sent to a particular joke
  api.GET("/jokes", JokeHandler)
  api.POST("/jokes/like/:jokeID", LikeJoke)
  
  // Start and run the server
  router.Run(":3000")
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"Jokes handler not implemented yet",
  })
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, gin.H {
    "message":"LikeJoke handler not implemented yet",
  })
}

Let’s run our app again go run main.go, and access our routes. http://localhost:3000/api/jokes will return a 200 OK header response, with the message jokes handler not implemented yet. A POST request to http://localhost:3000/api/jokes/like/1 returns a 200 OK header, and the message Likejoke handler not implemented yet.

让我们再次运行我们的应用程序, go run main.go并访问我们的路线。 http://localhost:3000/api/jokes将返回200 OK标头响应,并且jokes handler not implemented yet消息jokes handler not implemented yet 。 对http://localhost:3000/api/jokes/like/1的POST请求返回200 OK标头,并且消息Likejoke handler not implemented yet

笑话数据 (Jokes data)

Since we already have our routes definition set, which does only one thing (return a JSON response), we’ll spice up our codebase a bit by adding some more code to it.

由于我们已经设置了路由定义集,该路由集仅做一件事(返回JSON响应),因此我们将通过向其添加更多代码来为代码库增添一些趣味。

// ... leave the code above untouched...

// Let's create our Jokes struct. This will contain information about a Joke

// Joke contains information about a single Joke
type Joke struct {
  ID     int     `json:"id" binding:"required"`
  Likes  int     `json:"likes"`
  Joke   string  `json:"joke" binding:"required"`
}

// We'll create a list of jokes
var jokes = []Joke{
  Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."},
  Joke{2, 0, "What do you call a fake noodle? An Impasta."},
  Joke{3, 0, "How many apples grow on a tree? All of them."},
  Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."},
  Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."},
  Joke{6, 0, "Why did the coffee file a police report? It got mugged."},
  Joke{7, 0, "How does a penguin build it's house? Igloos it together."},
}

func main() {
  // ... leave this block untouched...
}

// JokeHandler retrieves a list of available jokes
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  c.JSON(http.StatusOK, jokes)
}

// LikeJoke increments the likes of a particular joke Item
func LikeJoke(c *gin.Context) {
  // confirm Joke ID sent is valid
  // remember to import the `strconv` package
  if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil {
    // find joke, and increment likes
    for i := 0; i < len(jokes); i++ {
      if jokes[i].ID == jokeid {
        jokes[i].Likes += 1
      }
    }
    // return a pointer to the updated jokes list
    c.JSON(http.StatusOK, &jokes)
  } else {
    // Joke ID is invalid
    c.AbortWithStatus(http.StatusNotFound)
  }
}

// NB: Replace the JokeHandler and LikeJoke functions in the previous version to the ones above

With our code looking good, let’s go ahead and test our API. We can test with cURL or postman , and then send a GET request to http://localhost:3000/jokes to get the full list of jokes, and a POST request to http://localhost:3000/jokes/like/{jokeid} to increment the likes of a joke.

在我们的代码看起来不错的情况下,让我们继续测试我们的API。 我们可以使用cURLpostman进行测试,然后将GET请求发送到http://localhost:3000/jokes以获取完整的笑话列表,并将POST请求发送到http://localhost:3000/jokes/like/{jokeid}来增加喜欢的笑话。

$ curl http://localhost:3000/api/jokes

$ curl -X POST http://localhost:3000/api/jokes/like/4

构建UI(React) (Building the UI (React))

We have our API in place, so let’s build a frontend to present the data from our API. For this, we’ll be using React. We won’t go too deep into React, as it will be out of the scope of this tutorial. If you need to learn more about React, checkout the official tutorial. You can implement the UI with any frontend framework you’re comfortable with.

我们已经有了我们的API,因此让我们构建一个前端来展示来自我们API的数据。 为此,我们将使用React。 我们不会深入研究React,因为它不在本教程的讨论范围之内。 如果您需要了解更多有关React的信息,请查看官方教程 。 您可以使用自己喜欢的任何前端框架来实现UI。

建立 (Setup)

We’ll edit the index.html file to add the external libraries needed to run React. Then we'll need to create an app.jsx file in the views/js directory, which will contain our React code.

我们将编辑index.html文件,以添加运行React所需的外部库。 然后,我们需要在views/js目录中创建一个app.jsx文件,其中将包含我们的React代码。

Our index.html file should look like this:

我们的index.html文件应如下所示:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>Jokeish App</title>
  <script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
  <script src="https://cdn.auth0.com/js/auth0/9.0/auth0.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/react@16.0.0/umd/react.production.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js"></script>
  <script type="application/javascript" src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
  <script type="text/babel" src="js/app.jsx"></script>
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
</body>

</html>

构建我们的组件 (Building our components)

In React, views are broken down into components. We’ll need to build some components:

在React中,视图被分解为组件。 我们需要构建一些组件:

  • an App component as the main entry that launches the application

    一个App组件作为启动该应用程序的主要条目

  • a Home component which will face non logged-in users

    面向未登录用户的Home组件

  • a LoggedIn component with content only visible by authenticated users

    一个LoggedIn组件,其内容仅由经过身份验证的用户可见

  • and a Joke component to display a list of jokes.

    还有一个Joke组件,用于显示笑话列表。

We'll write all these components in the app.jsx file.

我们将所有这些组件写入app.jsx文件。

应用程序组件 (The app component)

This component bootstraps our entire React app. It decides which component to show based on whether a user is authenticated or not. We’ll start off with just its base, and later update it with more functionality.

该组件引导我们整个React应用程序。 它根据用户是否通过身份验证来决定显示哪个组件。 我们将从其基础开始,然后再通过更多功能对其进行更新。

class App extends React.Component {
  render() {
    if (this.loggedIn) {
      return (<LoggedIn />);
    } else {
      return (<Home />);
    }
  }
}

家庭组件 (The Home component)

This component is shown to non logged-in users, along with a button which opens a Hosted lock screen where they can signup or login. We’ll add this functionality later.

向未登录的用户显示此组件,以及一个按钮,该按钮可打开托管锁定屏幕,用户可以在其中注册或登录。 稍后我们将添加此功能。

class Home extends React.Component {
  render() {
    return (
      <div className="container">
        <div className="col-xs-8 col-xs-offset-2 jumbotron text-center">
          <h1>Jokeish</h1>
          <p>A load of Dad jokes XD</p>
          <p>Sign in to get access </p>
          <a onClick={this.authenticate} className="btn btn-primary btn-lg btn-login btn-block">Sign In</a>
        </div>
      </div>
    )
  }
}

登录组件 (LoggedIn component)

This component is displayed when a user is authenticated. It stores in its state an array of jokes which is populated when the component mounts.

验证用户身份后,将显示此组件。 它以其state存储一个笑话数组,该笑话数组在组件安装时填充。

class LoggedIn extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      jokes: []
    }
  }
    
  render() {
    return (
      <div className="container">
        <div className="col-lg-12">
          <br />
          <span className="pull-right"><a onClick={this.logout}>Log out</a></span>
          <h2>Jokeish</h2>
          <p>Let's feed you with some funny Jokes!!!</p>
          <div className="row">
            {this.state.jokes.map(function(joke, i){
              return (<Joke key={i} joke={joke} />);
            })}
          </div>
        </div>
      </div>
    )
  }
}

笑话组件 (The Joke component)

The Joke component will contain information about each item from the jokes response to be displayed.

Joke组件将包含有关要显示的笑话响应中每个项目的信息。

class Joke extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      liked: ""
    }
    this.like = this.like.bind(this);
  }
    
  like() {
    // ... we'll add this block later
  }
    
  render() {
    return (
      <div className="col-xs-4">
        <div className="panel panel-default">
          <div className="panel-heading">#{this.props.joke.id} <span className="pull-right">{this.state.liked}</span></div>
          <div className="panel-body">
            {this.props.joke.joke}
          </div>
          <div className="panel-footer">
            {this.props.joke.likes} Likes &nbsp;
            <a onClick={this.like} className="btn btn-default">
              <span className="glyphicon glyphicon-thumbs-up"></span>
            </a>
          </div>
        </div>
      </div>
    )
  }
}

We’ve written our components, so now let’s tell React where to render the app. We’ll add the block of code below to the bottom of our app.jsx file.

我们已经编写了组件,现在让我们告诉React在哪里渲染应用程序。 我们将下面的代码块添加到app.jsx文件的底部。

ReactDOM.render(<App />, document.getElementById('app'));

Let’s restart our Go server go run main.go, and head over to our app's URL http://localhost:3000/. You'll see that the Home component is being rendered.

让我们重新启动Go服务器, go run main.go ,然后转到应用程序的URL http://localhost:3000/ 。 您将看到Home组件正在呈现。

使用Auth0保护我们的笑话应用程序 (Securing our jokes app with Auth0)

Auth0 issues JSON Web Tokens on every login for your users. This means that you can have a solid identity infrastructure, including single sign-on, user management, support for social identity providers (Facebook, Github, Twitter, etc.), enterprise identity providers (Active Directory, LDAP, SAML, etc.) and your own database of users, with just a few lines of code.

Auth0在每次登录时为您的用户颁发 JSON Web令牌 。 这意味着您可以拥有一个可靠的身份基础结构 ,包括单点登录 ,用户管理,对社交身份提供商(Facebook,Github,Twitter等),企业身份提供商(Active Directory,LDAP,SAML等)的支持。和您自己的用户数据库,只需几行代码。

We can easily set up authentication in our GIN app by using Auth0. You’ll need an account to follow along with this part. If you don’t already have an Auth0 account, sign up for one now.

我们可以使用Auth0在GIN应用中轻松设置身份验证。 您需要一个帐户来跟随此部分。 如果您还没有Auth0帐户,请立即注册一个。

Disclaimer: This isn’t sponsored content.

免责声明:这不是赞助内容。

创建API客户端 (Creating the API client)

Our tokens will be generated with Auth0, so we need to create an API and a Client from our Auth0 dashboard. Again, if you haven’t already, sign up for an Auth0 account.

我们的令牌将使用Auth0生成,因此我们需要从Auth0信息中心创建一个API和一个客户端。 同样,如果您还没有的话,请注册一个Auth0帐户。

To create a new API, navigate to the APIs section in your dashboard, and click the Create API button.

要创建新的API,请导航至仪表板中的“ API”部分 ,然后单击“ 创建API”按钮。

Choose an API name and an identifier. The identifier will be the audience for the middleware. The Signing Algorithm should be RS256.

选择一个API 名称和一个标识符 。 标识符将是中间件的受众签名算法应为RS256

To create a new Client, navigate to the clients section in your dashboard, and click the Create Client button. Select the type Regular Web Applications.

要创建新客户端,请导航至仪表板中的“ 客户端”部分 ,然后单击“ 创建客户端”按钮。 选择Regular Web Applications类型。

Once the client is created, take note of the client_id and client_secret, as we'll need them later.

创建客户端后,请注意client_idclient_secret ,因为稍后将需要它们。

We have to add the credentials needed for our API to an environment variable. In the root directory, create a new file .env and add the following to it, with the details from the Auth0 dashboard:

我们必须将API所需的凭据添加到环境变量中。 在根目录中,创建一个新文件.env并向其中添加以下内容,其中包含来自Auth0仪表板的详细信息:

export AUTH0_API_CLIENT_SECRET=""
export AUTH0_CLIENT_ID=""
export AUTH0_DOMAIN="yourdomain.auth0.com"
export AUTH0_API_AUDIENCE=""

保护我们的API端点 (Securing our API endpoints)

Currently, our API is open to the world. We need to secure our endpoints, so only authorized users can access them.

目前,我们的API已向世界开放。 我们需要保护我们的端点,因此只有授权用户才能访问它们。

We are going to make use of a JWT Middleware to check for a valid JSON Web Token from each request hitting our endpoints.

我们将利用JWT中间件从命中端点的每个请求中检查有效的JSON Web令牌

Let’s create our middleware:

让我们创建我们的中间件:

// ...

var jwtMiddleWare *jwtmiddleware.JWTMiddleware

func main() {
  jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      aud := os.Getenv("AUTH0_API_AUDIENCE")
      checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
      if !checkAudience {
        return token, errors.New("Invalid audience.")
      }
      // verify iss claim
      iss := os.Getenv("AUTH0_DOMAIN")
      checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
      if !checkIss {
        return token, errors.New("Invalid issuer.")
      }
      
      cert, err := getPemCert(token)
      if err != nil {
        log.Fatalf("could not get cert: %+v", err)
      }
      
      result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
      return result, nil
    },
    SigningMethod: jwt.SigningMethodRS256,
  })
  
  // register our actual jwtMiddleware
  jwtMiddleWare = jwtMiddleware

  // ... the rest of the code below this function doesn't change yet
}

// authMiddleware intercepts the requests, and check for a valid jwt token
func authMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    // Get the client secret key
    err := jwtMiddleWare.CheckJWT(c.Writer, c.Request)
    if err != nil {
      // Token not found
      fmt.Println(err)
      c.Abort()
      c.Writer.WriteHeader(http.StatusUnauthorized)
      c.Writer.Write([]byte("Unauthorized"))
      return
    }
  }
}

In the above code, we have a new jwtMiddleWare variable which is initialized in the main function. It is used in the authMiddleware middle function.

在上面的代码中,我们有一个新的jwtMiddleWare变量,该变量在main函数中初始化。 它在authMiddleware中间函数中使用。

If you notice, we are pulling our server-side credentials from an environment variable (one of the tenets of a 12-factor app). Our middleware checks and receives a token from a request and calls the jwtMiddleWare.CheckJWT method to validate the token sent.

如果您注意到,我们将从环境变量(包含12个因子的应用程序的原则之一)中提取服务器端凭据。 我们的中间件检查并接收来自请求的令牌,然后调用jwtMiddleWare.CheckJWT方法来验证发送的令牌。

Let’s also write the function to return the JSON Web Keys:

让我们还编写返回JSON Web密钥的函数:

// ... the code above is untouched...

// Jwks stores a slice of JSON Web Keys
type Jwks struct {
  Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
  Kty string   `json:"kty"`
  Kid string   `json:"kid"`
  Use string   `json:"use"`
  N   string   `json:"n"`
  E   string   `json:"e"`
  X5c []string `json:"x5c"`
}

func main() {
  // ... the code in this method is untouched...
}

func getPemCert(token *jwt.Token) (string, error) {
  cert := ""
  resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json")
  if err != nil {
    return cert, err
  }
  defer resp.Body.Close()
    
  var jwks = Jwks{}
  err = json.NewDecoder(resp.Body).Decode(&jwks)
    
  if err != nil {
    return cert, err
  }
    
  x5c := jwks.Keys[0].X5c
  for k, v := range x5c {
    if token.Header["kid"] == jwks.Keys[k].Kid {
      cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----"
    }
  }
    
  if cert == "" {
    return cert, errors.New("unable to find appropriate key.")
  }
    
  return cert, nil
}

使用JWT中间件 (Using the JWT middleware)

Using the middleware is very straightforward. We just pass it as a parameter to our routes definition.

使用中间件非常简单。 我们只是将其作为参数传递给路线定义。

...

api.GET("/jokes", authMiddleware(), JokeHandler)
api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke)

...

Our main.go file should look like this:

我们的main.go文件应如下所示:

package main

import (
  "encoding/json"
  "errors"
  "fmt"
  "log"
  "net/http"
  "os"
  "strconv"
  
  jwtmiddleware "github.com/auth0/go-jwt-middleware"
  jwt "github.com/dgrijalva/jwt-go"
  "github.com/gin-gonic/contrib/static"
  "github.com/gin-gonic/gin"
)

type Response struct {
  Message string `json:"message"`
}

type Jwks struct {
  Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
  Kty string   `json:"kty"`
  Kid string   `json:"kid"`
  Use string   `json:"use"`
  N   string   `json:"n"`
  E   string   `json:"e"`
  X5c []string `json:"x5c"`
}

type Joke struct {
  ID    int    `json:"id" binding:"required"`
  Likes int    `json:"likes"`
  Joke  string `json:"joke" binding:"required"`
}

/** we'll create a list of jokes */
var jokes = []Joke{
  Joke{1, 0, "Did you hear about the restaurant on the moon? Great food, no atmosphere."},
  Joke{2, 0, "What do you call a fake noodle? An Impasta."},
  Joke{3, 0, "How many apples grow on a tree? All of them."},
  Joke{4, 0, "Want to hear a joke about paper? Nevermind it's tearable."},
  Joke{5, 0, "I just watched a program about beavers. It was the best dam program I've ever seen."},
  Joke{6, 0, "Why did the coffee file a police report? It got mugged."},
  Joke{7, 0, "How does a penguin build it's house? Igloos it together."},
}

var jwtMiddleWare *jwtmiddleware.JWTMiddleware

func main() {
  jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      aud := os.Getenv("AUTH0_API_AUDIENCE")
      checkAudience := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
      if !checkAudience {
        return token, errors.New("Invalid audience.")
      }
      // verify iss claim
      iss := os.Getenv("AUTH0_DOMAIN")
      checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
      if !checkIss {
        return token, errors.New("Invalid issuer.")
      }
      
      cert, err := getPemCert(token)
      if err != nil {
        log.Fatalf("could not get cert: %+v", err)
      }
      
      result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
      return result, nil
    },  
    SigningMethod: jwt.SigningMethodRS256,
  })
  
  jwtMiddleWare = jwtMiddleware
  // Set the router as the default one shipped with Gin
  router := gin.Default()
  
  // Serve the frontend
  router.Use(static.Serve("/", static.LocalFile("./views", true)))
  
  api := router.Group("/api")
  {
    api.GET("/", func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H{
        "message": "pong",
      })
    })
    api.GET("/jokes", authMiddleware(), JokeHandler)
    api.POST("/jokes/like/:jokeID", authMiddleware(), LikeJoke)
  }
  // Start the app
  router.Run(":3000")
}

func getPemCert(token *jwt.Token) (string, error) {
  cert := ""
  resp, err := http.Get(os.Getenv("AUTH0_DOMAIN") + ".well-known/jwks.json")
  if err != nil {
    return cert, err
  }
  defer resp.Body.Close()
  
  var jwks = Jwks{}
  err = json.NewDecoder(resp.Body).Decode(&jwks)
  
  if err != nil {
    return cert, err
  }
  
  x5c := jwks.Keys[0].X5c
  for k, v := range x5c {
    if token.Header["kid"] == jwks.Keys[k].Kid {
      cert = "-----BEGIN CERTIFICATE-----\n" + v + "\n-----END CERTIFICATE-----"
    }
  }
  
  if cert == "" {
    return cert, errors.New("unable to find appropriate key")
  }
  
  return cert, nil
}

// authMiddleware intercepts the requests, and check for a valid jwt token
func authMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    // Get the client secret key
    err := jwtMiddleWare.CheckJWT(c.Writer, c.Request)
    if err != nil {
      // Token not found
      fmt.Println(err)
      c.Abort()
      c.Writer.WriteHeader(http.StatusUnauthorized)
      c.Writer.Write([]byte("Unauthorized"))
      return
    }
  }
}

// JokeHandler returns a list of jokes available (in memory)
func JokeHandler(c *gin.Context) {
  c.Header("Content-Type", "application/json")
  
  c.JSON(http.StatusOK, jokes)
}

func LikeJoke(c *gin.Context) {
  // Check joke ID is valid
  if jokeid, err := strconv.Atoi(c.Param("jokeID")); err == nil {
    // find joke and increment likes
    for i := 0; i < len(jokes); i++ {
      if jokes[i].ID == jokeid {
        jokes[i].Likes = jokes[i].Likes + 1
      }
    }
    c.JSON(http.StatusOK, &jokes)
  } else {
    // the jokes ID is invalid
    c.AbortWithStatus(http.StatusNotFound)
  }
}

Let’s install the jwtmiddleware libraries:

让我们安装jwtmiddleware库:

$ go get -u github.com/auth0/go-jwt-middleware
$ go get -u github.com/dgrijalva/jwt-go

Let’s source our environment file, and restart our app server:

让我们获取环境文件,然后重新启动我们的应用服务器:

$ source .env
$ go run main.go

Now if we try accessing any of the endpoints, we’ll be faced with a 401 Unauthorized error. That's because we need to send along a token with the request.

现在,如果我们尝试访问任何端点,我们将面临401 Unauthorized错误。 那是因为我们需要随请求一起发送令牌。

使用Auth0登录并进行React (Login with Auth0 and React)

Let’s implement a login system so that users can login or create accounts and get access to our jokes. We’ll add to our app.jsx file the following Auth0 credentials:

让我们实现一个登录系统,以便用户可以登录或创建帐户并访问我们的笑话。 我们将以下Auth0凭据添加到我们的app.jsx文件中:

  • AUTH0_CLIENT_ID

    AUTH0_CLIENT_ID

  • AUTH0_DOMAIN

    AUTH0_DOMAIN

  • AUTH0_CALLBACK_URL - The URL of your app

    AUTH0_CALLBACK_URL您的应用程序的URL

  • AUTH0_API_AUDIENCE

    AUTH0_API_AUDIENCE

You can find the AUTH0_CLIENT_ID, AUTH0_DOMAIN, and AUTH0_API_AUDIENCE data from your Auth0 management dashboard.

您可以从Auth0 管理信息中心找到AUTH0_CLIENT_IDAUTH0_DOMAINAUTH0_API_AUDIENCE数据。

We need to set a callback which Auth0 redirects to. Navigate to the Clients section in your dashboard. In the settings, let's set the callback to http://localhost:3000:

我们需要设置Auth0重定向到的callback 。 导航到仪表板中的“客户”部分。 在设置中,让我们将回调设置为http://localhost:3000

With the credentials in place, let’s update our React components.

有了凭据,让我们更新我们的React组件。

APP组件 (APP component)

const AUTH0_CLIENT_ID = "aIAOt9fkMZKrNsSsFqbKj5KTI0ObTDPP";
const AUTH0_DOMAIN = "hakaselabs.auth0.com";
const AUTH0_CALLBACK_URL = location.href;
const AUTH0_API_AUDIENCE = "golang-gin";

class App extends React.Component {
  parseHash() {
    this.auth0 = new auth0.WebAuth({
      domain: AUTH0_DOMAIN,
      clientID: AUTH0_CLIENT_ID
    });
    this.auth0.parseHash(window.location.hash, (err, authResult) => {
      if (err) {
        return console.log(err);
      }
      if (
        authResult !== null &&
        authResult.accessToken !== null &&
        authResult.idToken !== null
      ) {
        localStorage.setItem("access_token", authResult.accessToken);
        localStorage.setItem("id_token", authResult.idToken);
        localStorage.setItem(
          "profile",
          JSON.stringify(authResult.idTokenPayload)
        );
        window.location = window.location.href.substr(
          0,
          window.location.href.indexOf("#")
        );
      }
    });
  }
    
  setup() {
    $.ajaxSetup({
      beforeSend: (r) => {
        if (localStorage.getItem("access_token")) {
          r.setRequestHeader(
            "Authorization",
            "Bearer " + localStorage.getItem("access_token")
          );
        }
      }
    });
  }
  setState() {
    let idToken = localStorage.getItem("id_token");
    if (idToken) {
      this.loggedIn = true;
    } else {
      this.loggedIn = false;
    }
  }
    
  componentWillMount() {
    this.setup();
    this.parseHash();
    this.setState();
  }
    
  render() {
    if (this.loggedIn) {
      return <LoggedIn />;
    }
    return <Home />;
  }
}

We updated the App component with three component methods (setup, parseHash, and setState), and a lifecycle method componentWillMount. The parseHash method initializes the auth0 webAuth client and parses the hash to a more readable format, saving them to localSt. To show the lock screen, capture and store the user token and add the correct authorization header to any requests to our API

我们使用三个组件方法( setupparseHashsetState )以及生命周期方法componentWillMount更新了App componentWillMountparseHash方法将初始化auth0 webAuth客户端,并将哈希解析为更易读的格式,然后将其保存到localSt。 要显示锁定屏幕,请捕获并存储用户令牌,并将正确的授权标头添加到对我们的API的任何请求中

家庭组件 (Home component)

Our Home component will be updated. We’ll add the functionality for the authenticate method, which will trigger the hosted lock screen to display and allow our users to login or signup.

我们的主页组件将被更新。 我们将为authenticate方法添加功能,这将触发托管锁屏显示并允许我们的用户登录或注册。

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.authenticate = this.authenticate.bind(this);
  }
  authenticate() {
    this.WebAuth = new auth0.WebAuth({
      domain: AUTH0_DOMAIN,
      clientID: AUTH0_CLIENT_ID,
      scope: "openid profile",
      audience: AUTH0_API_AUDIENCE,
      responseType: "token id_token",
      redirectUri: AUTH0_CALLBACK_URL
    });
    this.WebAuth.authorize();
  }
    
  render() {
    return (
      <div className="container">
        <div className="row">
          <div className="col-xs-8 col-xs-offset-2 jumbotron text-center">
            <h1>Jokeish</h1>
            <p>A load of Dad jokes XD</p>
            <p>Sign in to get access </p>
            <a
              onClick={this.authenticate}
              className="btn btn-primary btn-lg btn-login btn-block"
            >
              Sign In
            </a>
          </div>
        </div>
      </div>
    );
  }
}

登录组件 (LoggedIn component)

We will update the LoggedIn component to communicate with our API and pull all jokes. It will pass each joke as a prop to the Joke component, which renders a bootstrap panel. Let's write those:

我们将更新LoggedIn组件以与我们的API进行通信并拉开所有笑话。 它将把每个笑话作为prop传递给“ Joke组件,该组件将呈现一个引导面板。 让我们写那些:

class LoggedIn extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      jokes: []
    };
    
    this.serverRequest = this.serverRequest.bind(this);
    this.logout = this.logout.bind(this);
  }
  
  logout() {
    localStorage.removeItem("id_token");
    localStorage.removeItem("access_token");
    localStorage.removeItem("profile");
    location.reload();
  }
  
  serverRequest() {
    $.get("http://localhost:3000/api/jokes", res => {
      this.setState({
        jokes: res
      });
    });
  }
  
  componentDidMount() {
    this.serverRequest();
  }
  
  render() {
    return (
      <div className="container">
        <br />
        <span className="pull-right">
          <a onClick={this.logout}>Log out</a>
        </span>
        <h2>Jokeish</h2>
        <p>Let's feed you with some funny Jokes!!!</p>
        <div className="row">
          <div className="container">
            {this.state.jokes.map(function(joke, i) {
              return <Joke key={i} joke={joke} />;
            })}
          </div>
        </div>
      </div>
    );
  }
}

笑话成分 (Joke component)

We’ll also update the Joke component to format each Joke item passed to it from the Parent compoent (LoggedIn). We’ll also add a like method, which will increment the likes of a Joke.

我们还将更新Joke组件,以格式化从上级组件( LoggedIn )传递给它的每个Joke项目。 我们还将添加一个like方法,它将增加一个笑话的like。

class Joke extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      liked: "",
      jokes: []
    };
    this.like = this.like.bind(this);
    this.serverRequest = this.serverRequest.bind(this);
  }
    
  like() {
    let joke = this.props.joke;
    this.serverRequest(joke);
  }
  serverRequest(joke) {
    $.post(
      "http://localhost:3000/api/jokes/like/" + joke.id,
      { like: 1 },
      res => {
        console.log("res... ", res);
        this.setState({ liked: "Liked!", jokes: res });
        this.props.jokes = res;
      }
    );
  }
    
  render() {
    return (
      <div className="col-xs-4">
        <div className="panel panel-default">
          <div className="panel-heading">
            #{this.props.joke.id}{" "}
            <span className="pull-right">{this.state.liked}</span>
          </div>
          <div className="panel-body">{this.props.joke.joke}</div>
          <div className="panel-footer">
            {this.props.joke.likes} Likes &nbsp;
            <a onClick={this.like} className="btn btn-default">
              <span className="glyphicon glyphicon-thumbs-up" />
            </a>
          </div>
        </div>
      </div>
    )
  }
}

全部放在一起 (Putting it all together)

With the UI and API complete, we can test our app. We’ll start off by booting our server source .env && go run main.go, and then we’ll navigate to http://localhost:3000 from any browser. You should see the Home component with a signin button. Clicking on the signin button will redirect to a hosted Lock page (create an account or login) to continue using the application.

完成UI和API后,我们就可以测试我们的应用了。 我们将从启动服务器source .env && go run main.go ,然后从任何浏览器导航到http://localhost:3000 。 您应该看到带有登录按钮的Home组件。 单击登录按钮将重定向到托管的Lock页面(创建帐户或登录名)以继续使用该应用程序。

Home:

家:

Auth0 Hosted Lock Screen:

Auth0托管锁定屏幕:

LoggedIn App view:

登录的应用程序视图:

结论 (Conclusion)

Congrats! You have learned how to build an application and an API with Go and the Gin framework.

恭喜! 您已经了解了如何使用Go和Gin框架构建应用程序和API。

Did I miss something important? Let me know of it in the comments.

我错过了重要的事情吗? 在评论中让我知道。

You can say Hi to me on Twitter @codehakase

您可以在Twitter @codehakase上对我问好

翻译自: https://www.freecodecamp.org/news/how-to-build-a-web-app-with-go-gin-and-react-cffdc473576/

gin react

 类似资料: