【服务计算】简单 web 服务与客户端开发实战

这次作业实现了一个简单的个人博客系统,能够给登录用户提供评论功能。

作业要求:简单 web 服务与客户端开发实战
项目地址:SimpleBlog

简单地回顾一下开发流程:

  1. 设计并规定好API
  2. 使用swagger编写文档和自动化生成go-server和vue-client
  3. 前端使用mock服务,后端使用postman,各自独立开发
  4. 开发过程中修改需求细节,更改文档
  5. 前后端开发完成后,运行测试,解决耦合后出现的BUG

0、设计API

API采用REST v3风格,设计了四种资源,分别为User,Article,Tag,Comment,一共设计了六个API服务

设计原则参考Github API v3 overview

API服务列表如下:

{
    "GetArticleById":"/v3/article/{id}",
    "GetArticles":"/v3/articles",
    "GetCommentsOfArticle":"/v3/article/{id}/comments",
    "CreateComment":"/v3/article/{id}/comment",
    "SignIn":"/v3/auth/signin",
    "SignUp": "/v3/auth/signup"
}

1、使用swagger

swagger是一个开源的API工具,功能强大,这次作业只是略略一窥其貌,主要使用了swagger-editor来编写API文档

swagger-editor:https://github.com/swagger-api/swagger-editor
在线编辑器:http://editor.swagger.io/

yaml

swagger-editor使用yaml语法来编写API文档,并且可以实时响应,使用漂亮的UI来测试效果

这次实现的SimpleBlog的部分yaml文件如下:
这是实现了SignIn服务的API,可以根据官方例子学习,语法清晰,上手简单

  /auth/signin:
    post:
      tags:
      - "user"
      summary: "sign in"
      description: "Check user with username and password"
      operationId: "SignIn"
      produces:
      - "application/json"
      parameters:
      - in: "body"
        name: "body"
        required: true
        schema:          
          $ref: "#/definitions/User"
      responses:
        200:
          description: "Successful Operation"
          schema:
            type: object
            properties:
              token: 
                type: string
        404:
          description: "Not Found"
          schema:
            type: object
            properties:
              error: 
                type: string
                example: "Wrong Username or Password"

API Doc

使用swagger-editor上方的Generate Client选项,点击html2,会生成一个压缩包,里面就包含了根据编写的yaml而生成的html静态页面

扫描二维码关注公众号,回复: 5877943 查看本文章

SimpleBlog的API Doc:https://gostbops.github.io/API-doc/

go-server

使用swagger-editor上方的Generate Server选项,点击go-server,会生成一个可运行的代码包,为我们编写了可自动化的路由匹配,匹配函数调用,以及资源的结构体定义

包结构如下:

- go-server
	- .swagger-codegen
		- VERSION
	- api
		- swagger.yaml
	- go
		- routers.go
		- logger.go
		- user.go
		- user_api.go
		- ...
		- README.md
	- main.go

阅读一下 routers.go,为我们实现了路由匹配和匹配函数调用

type Route struct {
	Name        string
	Method      string
	Pattern     string
	HandlerFunc http.HandlerFunc
}

type Routes []Route

func NewRouter() *mux.Router {
	router := mux.NewRouter().StrictSlash(true)
	for _, route := range routes {
		var handler http.Handler
		handler = route.HandlerFunc
		handler = Logger(handler, route.Name)

		router.
			Methods(route.Method).
			Path(route.Pattern).
			Name(route.Name).
			Handler(handler)
	}

	return router
}

var routes = Routes{
	Route{
		"Index",
		"GET",
		"/v3/",
		Index,
	},

	Route{
		"GetArticleById",
		strings.ToUpper("Get"),
		"/v3/article/{id}",
		GetArticleById,
	},
	...
}

article.go,为我们定义好了资源article的结构体

type Article struct {

	Id int32 `json:"id"`

	Name string `json:"name"`

	Tags []Tag `json:"tags,omitempty"`

	Date string `json:"date,omitempty"`

	Content string `json:"content"`
}

article_api.go,为我们写好了匹配函数的定义

func GetArticleById(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

func GetArticles(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

func GetCommentsOfArticle(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

因此我们只需要在article_api.go等包含匹配函数的文件里实现项目的业务就行了,十分方便,省去了许多重复性的东西

vue-client

swagger-editor官方没有自动生成vue-client的接口,不过我们在github
上找到了开发者实现的swagger-vue-client生成插件

swagger-vue:https://github.com/chenweiqun/swagger-vue

生成结果是vue-api-client.js,根据API服务实现了发送request请求,获取response的回调函数。

vue-api-client.js
该函数实现了获取文章列表的请求响应函数

export const GetArticlesURL = function(parameters = {}) {
  let queryParameters = {}
  const domain = parameters.$domain ? parameters.$domain : getDomain()
  let path = '/articles'
  if (parameters['page'] !== undefined) {
    queryParameters['page'] = parameters['page']
  }
  if (parameters.$queryParameters) {
    Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
      queryParameters[parameterName] = parameters.$queryParameters[parameterName]
    })
  }
  let keys = Object.keys(queryParameters)
  return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}

2、后端开发

这次项目后端开发用到的知识点不难,总结起来有三种:

  1. json序列化和反序列化
  2. boltdb数据库的使用
  3. jwt认证的使用

json序列化和反序列化

REST风格的API是使用json作为请求request和响应response的body的格式

所以当服务端收到请求后,需要使用json解析器反序列化为结构体

当服务端发送响应的时候,需要将结构体序列化为字符串

json反序列化:


comment := &Comment{
	Date:  time.Now().Format("2006-01-02 15:04:05"),
	Content: "",
	Author: "",
	ArticleId: Id,
}
err = json.NewDecoder(r.Body).Decode(&comment)

json序列化:

json, err := json.Marshal(response)

参考资料:golang基础-json序列化、反序列化、自定义error

boltdb数据库的使用

BoltDB是一个嵌入式key/value的数据库,即只需要将其链接到你的应用程序代码中即可使用BoltDB提供的API来高效的存取数据。而且BoltDB支持完全可序列化的ACID事务,让应用程序可以更简单的处理复杂操作。

BoltDB设计源于LMDB,具有以下特点:

  1. 直接使用API存取数据,没有查询语句;
  2. 支持完全可序列化的ACID事务,这个特性比LevelDB强;
  3. 数据保存在内存映射的文件里。没有wal、线程压缩和垃圾回收;
  4. 通过COW技术,可实现无锁的读写并发,但是无法实现无锁的写写并发,这就注定了读性能超高,但写性能一般,适合与读多写少的场景。

最后,BoltDB使用Golang开发,而且被应用于influxDB项目作为底层存储。

开启数据库:

	db, err := bolt.Open("my.db", 0600, nil)
    if err != nil {
        log.Fatal(err)
    }
	defer db.Close()

读操作:

	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("User"))
		if b != nil {
			v := b.Get([]byte(user.Username))
			if ByteSliceEqual(v, []byte(user.Password)) {
				return nil
			} else {
				return errors.New("Wrong Username or Password")
			}
		} else {
			return errors.New("Wrong Username or Password")
		}
	})

写操作:

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists([]byte("User"))
		if err != nil {
			return err
		}
		return b.Put([]byte(user.Username), []byte(user.Password))
	})

参考资料:boltdb 学习和实践

jwt认证的使用

JWT是Json Web Token的缩写

token的意思是“令牌”,是用户身份的验证方式,最简单的token组成:

  1. uid(用户唯一的身份标识)
  2. time(当前时间的时间戳)
  3. sign(签名,由token的前几位+用哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。
  4. 还可以把不变的参数也放进token,避免多次查库time(当前时间的时间戳)

JWT认证:
用户注册之后, 服务器生成一个 JWT token返回给浏览器, 浏览器向服务器请求数据时将 JWT token 发给服务器, 服务器用 signature 中定义的方式解码 JWT 获取用户信息.

一个JWT token包含3部分:

  1. header: 告诉我们使用的算法和 token 类型
  2. Payload:必须使用 sub key 来指定用户 ID, 还可以包括其他信息比如 email, username 等.
  3. Signature: 用来保证 JWT 的真实性. 可以使用不同算法

Token分发:

	token := jwt.New(jwt.SigningMethodHS256)
	claims := make(jwt.MapClaims)
	claims["exp"] = time.Now().Add(time.Hour * time.Duration(1)).Unix()
	claims["iat"] = time.Now().Unix()
	token.Claims = claims

	if err != nil {
			fatal(err)
	}

	tokenString, err := token.SignedString([]byte(user.Username))
	if err != nil {
			fatal(err)
	}

客户端获得Token后,可以将其储存在cookie中,当要请求需要授权的服务时,把Token赋给Header的Authorization项

Token验证:

	token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte(comment.Author), nil
        })
	if err == nil {
		if token.Valid {
			...
		}
	}

参考资料:Go实战–golang中使用JWT(JSON Web Token)

3、后端测试

在前端没有完成的时候,后端可以使用postman发送GET/POST等请求,来测试响应结果

4、前后端耦合

当前后端都实现好,并运行在localhost之后,我们会面临一个问题:跨域请求

这需要前后端在请求和响应的header设置一些项,golang中设置如下:

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.Header().Set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.WriteHeader(http.StatusOK)

值得注意的是,状态码的设置,即w.WriteHeader(code)需要写在最后,如果写在前面会导致后面的设置无效

还有一个需要注意的地方是,前端使用POST方法发送请求时,会先发送一个OPTIONS方法的请求,收到正确的响应后才会发送POST请求,所以在golang中设置如下:

routes.go

var routes = Route{
	...
	Route{
		"OPTIONS",
		strings.ToUpper("options"),
		"/v3/auth/signup",
		Options,
	},
	...
}

func Options(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Authorization")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(204)
}

猜你喜欢

转载自blog.csdn.net/hcm_0079/article/details/85072030