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

一、概述

利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。

任务目标
  • 选择合适的 API 风格,实现从接口或资源(领域)建模,到 API 设计的过程
  • 使用 API 工具,编制 API 描述文件,编译生成服务器、客户端原型
  • 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
  • 使用 API 设计工具提供 Mock 服务,两个团队独立测试 API
  • 使用 travis 测试相关模块

二、xx 开发项目

1.这个是一个团队项目,团队规模建议 6 人以内

  • 必须使用github 组织管理你的项目仓库
  • 一个仓库是客户端项目,必须 使用富客户端js框架。建议框架包括(VUE.js,Angular,React)
  • 一个仓库是服务端项目,你可以选择 RPC 风格、REST 风格、 GraphQL 构建服务
  • 一个仓库是项目文档,用户可以获得项目简介和 API 说明

2.你可以自由选择项目,以下是一些建议:

  • 自己选择一个项目,如“XX博客”
    • 资源类型不能少于 4 个。 如 “极简博客” 包括, users,acticles, reviews, tags …
    • 数据来源必须真实(请选择自己喜欢的网站抓取),每类资源不能少于 4 个数据
    • 页面参考主流博客结构,但仅需包含主页(https://xxx/:user),博客内容页面,按tag列表
  • 复制 https://swapi.co/ 网站
    • 你需要想办法获取该网站所有资源与数据
    • 给出 UI 帮助客户根据明星查看相关内容

3.项目的要求

  • 开发周期
    • 2 周
  • 每个项目仓库必须要有的文档
    • README.md
    • LICENSE
  • 客户界面与美术
    • 没要求,能用就好
  • API 设计
    • API 必须规范,请在项目文档部分给出一个简洁的说明,参考 github v3 或 v4 overview
    • 选择 1-2 个 API 作为实例写在项目文档,文档格式标准,参考 github v3 或 v4
  • 资源来源
    • 必须是真实数据,可以从公共网站获取
    • 在项目文档中,务必注明资源来源
  • 服务器端数据库支持
    • 数据库 只能使用 boltDB,请 不要使用 mysql 或 postgre 或 其他
  • 页面数与 API 数限制
    • 界面不能少于 3 个界面
    • 服务 API 不能少于 6 个
  • API 要求
    • API root 能获取简单 API 服务列表
    • 支持分页
  • 加分项
    • 部分 API 支持 Token 认证
  • 提交物要求
    • 每个团队需要提供项目文档首页的 URL。在文档中包含前后端安装指南。
    • 前端一般使用 npm 安装
    • 后端使用 go get 安装
    • 每个队员必须提交一个相关的博客,或项目小结(请用markdown编写,存放在文档仓库中)
  • 认证技术提示
    • 为了方便实现用户认证,建议采用 JWT 产生 token 实现用户认证。
    • 什么是 jwt? 官网:https://jwt.io/ 中文支持:http://jwtio.com/
    • 如何使用 jwt 签发用户 token ,用户验证 http://jwtio.com/introduction.html
    • 各种语言工具 http://jwtio.com/index.html#debugger-io
    • 案例研究:基于 Token 的身份验证:JSON Web Token

三、具体实现

我们实现了搭建一个博客网站的任务,是一个简单的个人博客系统,能够给登录用户提供评论功能。
1.设计API
API的设计采用了REST v3风格,设计了四个资源,分别为User,Article,Comment,和abstract,一共有了六个API服务。设计原则参考Github API v3 overview
swagger是一个开源的API工具,功能强大,我们主要使用了swagger-editor来编写API文档并根据API文档生成框架。
swagger编辑写的API文档如下:

---
swagger: "2.0"
info:
  description: "A simple API to learn how to write OpenAPI Specification"
  version: "1.0.0"
  title: "Simple API"
host: "simple.api"
basePath: "/openapi101"
schemes:
- "https"
paths:
  /auth/signin:
    post:
      summary: "sign in"
      parameters:
      - name: "username"
        in: "path"
        required: true
        type: "string"
        x-exportParamName: "Username"
      responses:
        "200":
          description: "A list of Person"
          schema:
            required:
            - "username"
            properties:
              username:
                type: "string"
        "404":
          description: "The user does not exists"
  /auth/signup:
    post:
      summary: "sign up a user"
      description: "sign up a user"
      parameters:
      - in: "body"
        name: "user"
        required: false
        schema:
          $ref: "#/definitions/user"
        x-exportParamName: "User"
      responses:
        "204":
          description: "OK"
        "400":
          description: "Wrong"
  /article/{article_id}:
    get:
      summary: "get post"
      parameters:
      - name: "article_id"
        in: "path"
        description: "article id"
        required: true
        type: "string"
        x-exportParamName: "ArticleId"
      responses:
        "200":
          description: "A post"
          schema:
            properties:
              name:
                type: "string"
              data:
                type: "string"
  /article:
    post:
      summary: "Create article"
      parameters:
      - in: "body"
        name: "content"
        required: true
        schema:
          $ref: "#/definitions/content"
        x-exportParamName: "Content"
      responses:
        "204":
          description: "A post"
definitions:
  user:
    required:
    - "password"
    - "username"
    properties:
      username:
        type: "string"
      password:
        type: "string"
  content:
    required:
    - "article_content"
    properties:
      article_content:
        type: "string"
      author:
        type: "string"

2.后端实现
使用swagger-editor上方的Generate Server选项,点击go-server,会生成一个可运行的代码包,为我们编写了可自动化的路由匹配,匹配函数调用,以及资源的结构体定义,具体文件结构如下:
在这里插入图片描述
在这里插入图片描述
routers.go文件为我们实现了路由匹配和匹配函数调用,model_comment.go,model_content.go为我们提供了资源的结构体。对于我们来说只需要在api_default.go中实现代码逻辑即可。api_default.go具体如下:

/*
 * Simple API
 *
 * A simple API to learn how to write OpenAPI Specification
 *
 * API version: 1.0.0
 * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
 */

package swagger

import (
	"fmt"
	"log"
	"errors"
	"strings"
	"time"
	"strconv"
	"net/http"
	"encoding/json"
	"encoding/binary"
	"github.com/boltdb/bolt"
	//"github.com/codegangsta/negroni"
	"github.com/dgrijalva/jwt-go"
	"github.com/dgrijalva/jwt-go/request"
)

const (
    SecretKey = "gostbops"
)

type Token struct {
	Token string `json:"token"`
}

type ErrorResponse struct {
    Error string `json:"error"`
}

func JsonResponse(response interface{}, w http.ResponseWriter, code int) {
    json, err := json.Marshal(response)
    if err != nil {
        log.Fatal(err)
        return
    }
	w.WriteHeader(code)
    w.Write(json)
}

func ByteSliceEqual(a, b []byte) bool {
    if len(a) != len(b) {
        return false
    }
    if (a == nil) != (b == nil) {
        return false
    }
    for i, v := range a {
        if v != b[i] {
            return false
        }
    }
    return true
}

func itob(v int) []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(v))
    return b
}

func Authorization(w http.ResponseWriter, r *http.Request) bool {
	username := strings.Split(r.URL.Path, "/")[3]
	token, _ := request.ParseFromRequest(r, 
		request.AuthorizationHeaderExtractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte(SecretKey), nil
        })
	claim, _ := token.Claims.(jwt.MapClaims)
	//fmt.Println(username)
    if username != claim["sub"] {
    	return false
    }
	return true
}

func ArticleArticleIdGet(w http.ResponseWriter, r *http.Request) {
	// 验证
	ok := Authorization(w, r)
	if !ok {
		response := ErrorResponse{"Authorization failed!"}
		JsonResponse(response, w, http.StatusUnauthorized)
		return
	}

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

	articleId := strings.Split(r.URL.Path, "/")[5]
	Id, err:= strconv.Atoi(articleId)
	if err != nil {
		reponse := ErrorResponse{"Wrong ArticleId"}
		JsonResponse(reponse, w, http.StatusBadRequest)
		return
	}
	var article Article
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Article"))
		if b != nil {
			v := b.Get(itob(Id))
			if v == nil {
				return errors.New("Article Not Exists")
			} else {
				_ = json.Unmarshal(v, &article)
				return nil
			}
		} else {
			return errors.New("Article Not Exists")
		}
	})

	if err != nil {
		reponse := ErrorResponse{err.Error()}
		JsonResponse(reponse, w, http.StatusNotFound)
		return
	}
	JsonResponse(article, w, http.StatusOK)
}

func ArticlePost(w http.ResponseWriter, r *http.Request) {
	// 验证
	ok := Authorization(w, r)
	if !ok {
		response := ErrorResponse{"Authorization failed!"}
		JsonResponse(response, w, http.StatusUnauthorized)
		return
	}

	// 开启数据库
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 解析body得到article
	var article Article
	err = json.NewDecoder(r.Body).Decode(&article)
	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

	if article.Title == "" || article.ArticleContent == "" {
		response := ErrorResponse{"Wrong title or content"}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists([]byte("Article"))
		if err != nil {
			return err
		}
		id, _ := b.NextSequence()
		article.Id = int(id)
		fmt.Println("%d",int(id))
		encoded, err := json.Marshal(article)
		byte_id := itob(article.Id)
		return b.Put(byte_id, encoded)
	})

	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

func AuthSigninPost(w http.ResponseWriter, r *http.Request) {
	db, err := bolt.Open("my.db", 0600, nil)
    if err != nil {
        log.Fatal(err)
    }
	defer db.Close()

	var user User
	err = json.NewDecoder(r.Body).Decode(&user)
	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	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")
		}
	})

	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusNotFound)
		return
	}

	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()
	claims["sub"] = user.Username
	token.Claims = claims

	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, "Error extracting the key")
		log.Fatal(err)
	}

	tokenString, err := token.SignedString([]byte(user.Username))
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, "Error while signing the token")
		log.Fatal(err)
	}

	response := Token{tokenString}
	JsonResponse(response, w, http.StatusOK)
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
}

func AuthSignupPost(w http.ResponseWriter, r *http.Request) {
	db, err := bolt.Open("my.db", 0600, nil)
    if err != nil {
        log.Fatal(err)
    }
	defer db.Close()

	var user User
	err = json.NewDecoder(r.Body).Decode(&user)
	
	if err != nil || user.Password == "" || user.Username == "" {
		response := ErrorResponse{"Wrong Username or Password"}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

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

	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

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

	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

func CreateComment(w http.ResponseWriter, r *http.Request) {
	if Authorization(w,r)==false {
		response := ErrorResponse{"Token Invalid"}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	articleId  := strings.Split(r.URL.Path, "/")[5]
	Id, err:= strconv.Atoi(articleId)
	if err != nil {
		response := ErrorResponse{"Wrong ArticleId"}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Article"))
		if b != nil {
			v := b.Get(itob(Id))
			if v == nil {
				return errors.New("Article Not Exists")
			} else {
				return nil
			}
		}
		return errors.New("Article Not Exists")
	})
	
	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

	comment := &Comment{
		Content: "",
		Author: "",
		ArticleId: Id,
	}
	err = json.NewDecoder(r.Body).Decode(&comment)

	if err != nil  || comment.Content == "" {
		w.WriteHeader(http.StatusBadRequest)
		if err != nil {
			response := ErrorResponse{err.Error()}
			JsonResponse(response, w, http.StatusBadRequest)
		} else {
			response := ErrorResponse{"Empty Content"}
			JsonResponse(response, w, http.StatusBadRequest)
		} 
		return
	}
	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists([]byte("Comment"))
		if err != nil {
			return err
		}
		id, _ := b.NextSequence()
		encoded, err := json.Marshal(comment)
		return b.Put(itob(int(id)), encoded)
	})
		
	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	JsonResponse(comment, w, http.StatusOK)
}

func GetCommentsOfArticle(w http.ResponseWriter, r *http.Request) {
	if Authorization(w,r)==false {
		response := ErrorResponse{"Token Invalid"}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}
	db, err := bolt.Open("my.db", 0600, nil)
    if err != nil {
        log.Fatal(err)
    }
	defer db.Close()

	articleId := strings.Split(r.URL.Path, "/")[5]
	Id, err:= strconv.Atoi(articleId)
	if err != nil {
		reponse := ErrorResponse{"Wrong ArticleId"}
		JsonResponse(reponse, w, http.StatusBadRequest)
		return
	}
	var article []byte
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Article"))
		if b != nil {
			v := b.Get(itob(Id))
			if v == nil {
				return errors.New("Article Not Exists")
			} else {
				article = v
				return nil
			}
		} else {
			return errors.New("Article Not Exists")
		}
	})

	if err != nil {
		reponse := ErrorResponse{err.Error()}
		JsonResponse(reponse, w, http.StatusNotFound)
		return
	}
	var comments Comments
	var comment Comment
	err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Comment"))
		if b != nil {
			c := b.Cursor()

			for k, v := c.First(); k != nil; k, v = c.Next() {
				err = json.Unmarshal(v, &comment)
				if err != nil {
					return err
				}
				if comment.ArticleId == Id {
					comments.Content = append(comments.Content, comment)
				}
			}

			return nil
		} else {
			return errors.New("Comment Not Exists")
		}
	})

	if err != nil {
		reponse := ErrorResponse{err.Error()}
		JsonResponse(reponse, w, http.StatusNotFound)
		return
	}

	JsonResponse(comments, w, http.StatusOK)
}

主要的内容是boltdb数据库的使用以及jwt认证的使用。
boltdb数据库的使用:Bolt是一个纯粹Key/Value模型的程序。该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供一个简单,快速,可靠的数据库。BoltDB只需要将其链接到你的应用程序代码中即可使用BoltDB提供的API来高效的存取数据。而且BoltDB支持完全可序列化的ACID事务,让应用程序可以更简单的处理复杂操作。
BoltDB设计源于LMDB,具有以下特点:

  • 直接使用API存取数据,没有查询语句;
  • 支持完全可序列化的ACID事务,这个特性比LevelDB强;
  • 数据保存在内存映射的文件里。没有wal、线程压缩和垃圾回收;
  • 通过COW技术,可实现无锁的读写并发,但是无法实现无锁的写写并发,这就注定了读性能超高,但写性能一般,适合与读多写少的场景。
    具体应用如下:
    数据库开启:
db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

数据库更新:

err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists([]byte("Comment"))
		if err != nil {
			return err
		}
		id, _ := b.NextSequence()
		encoded, err := json.Marshal(comment)
		return b.Put(itob(int(id)), encoded)
	})
		
	if err != nil {
		response := ErrorResponse{err.Error()}
		JsonResponse(response, w, http.StatusBadRequest)
		return
	}

数据库查询:

err = db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Comment"))
		if b != nil {
			c := b.Cursor()

			for k, v := c.First(); k != nil; k, v = c.Next() {
				err = json.Unmarshal(v, &comment)
				if err != nil {
					return err
				}
				if comment.ArticleId == Id {
					comments.Content = append(comments.Content, comment)
				}
			}

			return nil
		} else {
			return errors.New("Comment Not Exists")
		}
	})

	if err != nil {
		reponse := ErrorResponse{err.Error()}
		JsonResponse(reponse, w, http.StatusNotFound)
		return
	}

jwt认证的使用:
JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。它是基于RFC 7519标准定义的一种可以安全传输的小巧和自包含的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对其进行签名。
JWT的组成: 一个JWT实际上就是一个字符串,它由三部分组成,头部(Header)、载荷(Payload)与签名(Signature)。拼接形式为: Header.Payload.Signature。
JWT的工作流程:
在这里插入图片描述

  • 用户导航到登录页,输入用户名和密码,进行登录
  • 服务器对登录用户进行认证,如果认证通过,根据用户的信息和JWT的生成规则生成JWT Token
  • 服务器将该Token字符串返回
  • 客户端得到Token信息,将Token存储在localStorage、sessionStorage或cookie等存储形式中。
  • 当用户请求服务器API时,在请求的Header中加入 Authorization:Token。
  • 服务端对此Token进行校验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出响应结果,如果不通过,返回HTTP 401。
  • 用户进入系统,获得请求资源
    在go中我们实现了token的分发和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()
	claims["sub"] = user.Username
	token.Claims = claims

jwt验证:

func Authorization(w http.ResponseWriter, r *http.Request) bool {
	username := strings.Split(r.URL.Path, "/")[3]
	token, _ := request.ParseFromRequest(r, 
		request.AuthorizationHeaderExtractor,
        func(token *jwt.Token) (interface{}, error) {
            return []byte(SecretKey), nil
        })
	claim, _ := token.Claims.(jwt.MapClaims)
	//fmt.Println(username)
    if username != claim["sub"] {
    	return false
    }
	return true
}

我们通过postman软件对后端进行测试,postman是能够网页调试与发送网页HTTP请求的程序,在终端执行go run main.go 启动服务后我们可以在postman中输入请求包就可以看到我们的API是否正确返回。
以创建评论和查看评论为例:
在这里插入图片描述
在这里插入图片描述
输入URL和json数据就能发出请求。
3.前端实现
前端部分使用vue框架,首先就是vue的安装。
从node.js官网下载并安装node,接着在命令行中输入

npm install -g cnpm --registry=https://registry.npm.taobao.org

安装淘宝镜像。接着在命令行运行命令

npm install -g vue-cli

然后等待安装完成。
安装完成后在项目目录下输入

vue init webpack vue

就可以创建vue的架构
在这里插入图片描述
接着在命令行执行

npm install
npm run dev

就可以在http://localhost:8080/中看到vue初始网址
在这里插入图片描述
我们也在这个框架的基础上编写我们的前端。
4.前后端调用
在GitHub上有人实现了swagger实现自动生成vue-client的接口

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

最终得到了vue-api-client.js,根据API服务实现了发送request请求,获取response的回调函数。

四、总结

这次作业中我们完成了一个简单的 web 服务与客户端开发,内容确实比较多,需要学习的新知识也比较多,我们的时间也是很紧,完成的比较快,简单的实现了功能,没有做进一步的优化,以后有时间会对项目进行一步的改善。
github地址:Simple-Blog

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值