一、概述
利用 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