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

GitHub:

项目文档: https://github.com/fentender/book-blog-doc

客户端: https://github.com/fentender/book-blog-client

服务端: https://github.com/fentender/book-blog-server

目录

 

前言

一、API设计

二、Swagger Editor使用

1.编写API文档

2.自动化生成项目需求文档

API Doc文档

客户端

服务端

三、BookBlog客户端

1.使用vue-cli建立项目

2. book_blog_api包的使用

3.使用mock拦截请求,模拟数据并返回

4.客户端JWT认证

5.客户端结构

四、BookBlog服务端

1.API的编写

2.Postman的使用

3.JWT的服务端实现

五、前后端耦合

六、项目展示


前言

此次项目以API-First为原则,利用Web客户端调用远端服务以开发一个前后端分离的Web博客。简单回顾此次项目开发历程,可以将其大致分为以下几步:

  1. 设计本次项目中博客所需要使用到RESTful风格的API
  2. 使用Swagger Editor编写文档,并生成利用其自动化生成客户端与服务端的API接口代码
  3. 利用mock模拟返回数据以进行测试,独立开发客户端项目
  4. 利用postman模拟请求,独立开发服务端项目
  5. 前后端开发完成后,进行测试,完善前后端项目

同时在本次的项目博客详细情况如下:

  1. 包含资源类型:Book,User,Bookshelf,Review,Token(书局来源于豆瓣读书
  2. 通过/books, /reviews/{bookID},/users/{username}/bookshelfs等可获得简单 API 服务列表,并且支持分页
  3. Bookshelf资源与User资源支持token认证,只能在登录后才能使用。

 

一、API设计

在本次项目中使用了JWT的Token身份认证,设计思路如下:

  1. 客户端通过用户名和密码向服务器发送请求登陆
  2. 服务器收到请求数据,在数据库进行查询验证
  3. 如果验证成功,服务器签发一个Token给客户端
  4. 客户端可以将Token存放到SessionStroage 或者Cookie里
  5. 客服端设置监听,每次跳转路由,就判断 SessionStroage 中有无 Token ,没有就跳转到登录页面,有则跳转到对应路由页面
  6. 在每次客户端发送的请求中,在请求头中加上Token
  7. 在后端设置拦截器,用户登录后的每次请求都会经过这个拦截器校验Token是否有效
  8. 如果验证成功,则继续执行请求,返回请求到的数据

因此除了普通资源的API设计外,还需要增加用于登录、注册和注销的几个API。在本次项目BookBlog中有如下4种资源:User、Book、Bookshelf、Review、Token。其API服务如下:

{
    //Book
    "getBook" : GET "/books/{bookId}",
    "getBooks" : GET "/books",
    
    //Review
    "getReview" : GET "/review/{reviewId}",
    "getReviews" : GET "/reviews",

    //User
    "getUser" : GET "/users/user",

    //Bookshelf
    "createBookshelf" : POST "/user/{username}/bookshelfs",
    "getBookshelfs" : GET "/users/{username}/bookshelfs",
    "getBookshelf" : GET "/users/{username}/bookshelfs/{bookshelfName}",
    "deleteBookshelf" : DELETE "/users/{username}/bookshelfs/{bookshelfName}",

    //添加书籍到书架中
    "addBookInBookshelf" : POST "/users/{username}/bookshelfs/{bookshelfName}/{bookId}",
    "deleteBookInBookshelf" : DELETE "/users/{username}/bookshelfs/{bookshelfName}/{bookId}",

    //token相关资源,用于账户JWT验证
    "signIn" : GET "/token",
    "signOut" : DELETE "/token",
    "signUp" : POST "/token"
}

二、Swagger Editor使用

OpenAPI 规范(OAS)是一种通用的、和编程语言无关的 API 描述规范,使人类和计算机都可以发现和理解服务的功能,而无需访问源代码、文档或针对接口进行嗅探。而Swagger 规范即是OpenAPI的原身。Swagger则可以说是一个 OpenAPI 的工具集。Swagger Editor是Swagger中的一个编写OpenAPI的编辑器。其使用yaml语法来编写API文档,并且可以实时响应,使用漂亮的UI来测试效果

 Swagger官网: https://swagger.io/

Swagger Editor在线编辑:https://editor.swagger.io/

1.编写API文档

Swagger Editor使用yaml语法进行编写API文档,采用OpenAPI规范描述API,并能够根据代码实时响应生成UI效果,让我们直观地看到效果。  

可以通过查阅OpenAPI文档与查看官方例子学习其使用用法。

(需要注意的是XMLHttpRequest不支持GET请求中带body参数,而swagger在生成javascript时使用的superagent包是通过XMLHttpRequest发送请求的,我目前还没找到解决方案。因此在编写API文档时,注意尽量不要在GET请求中带body参数)

yaml教程:http://www.ruanyifeng.com/blog/2016/07/yaml.html

OpenAPI文档:https://swagger.io/docs/specification/api-host-and-base-path/

以下即为本次项目BookBlog使用Swagger Editor生成的文档展示

Swagger editor展示效果

 

 

2.自动化生成项目需求文档

Swagger Editor能够根据所编写的文档自动生成客户端、服务端接口或是API文档。

选择Swagger Editor菜单栏:

API Doc文档

"Generate Client" - "html2":

导出html格式的API Doc文档。

客户端

"Generate Client" - "javascript" :

导出JavaScript编写的客户端API接口代码。其文件结构如下:

javascript-client
|-- git_push.sh
|-- mocha.opts
|-- package.json
|-- README.md
|-- docs
|   |-- Book.md
|   |-- BookApi.md
|   ...
|   |-- UserApi.md
|   `-- UserBookshelfApi.md
|-- src
|   |-- ApiClient.js
|   |-- api
|   |   |-- BookApi.js
|   |   ...
|   |   |-- UserApi.js
|   |   `-- UserBookshelfApi.js
|   |-- index.js
|   `-- model
|       |-- Book.js
|       ...
|       |-- Token.js
|       `-- User.js
`-- test
    |-- api
    |   |-- BookApi.spec.js
    |   ...
    |   |-- UserApi.spec.js
    |   `-- UserBookshelfApi.spec.js
    |-- assert-equals.js
    `-- model
        |-- Book.spec.js
        ...
        |-- Token.spec.js
        `-- User.spec.js

可以看到由Swagger editor生成的客户端接口文档实际上就是一个npm包。其中各个文件作用如下:

  • docs: 各个API的描述
  • src: 存放使用javascript编写的API接口方法,其使用superagent(一个轻量的Ajax API)向服务端发送请求。
  • test:文件夹下为API接口方法的测试文档,可在根目录下使用npm run test命令进行mocha测试。
  • mocha.opts: mocha测试的配置文档
  • git_push.sh: 用于快速推送文档至git上的sh脚本
  • package.json: bookblogapi客户端的npm模块描述文件。
  • README.md: 客户端API接口的使用方法

为了在之后的前端项目中使用这个生成的npm包,将其公布在github: https://github.com/fentender/book_blog_api。在之后的客户端项目中就可以使用以下命令安装api接口npm包。

npm install fentender/book_blog_api --save 

服务端

"Generate Server" - "go-server" :

导出go编写的服务端代码,其结构如下:

go-server-server
    |-- api
    |   `-- swagger.yaml
    |-- go
    |   |-- README.md
    |   |-- api_book.go
    |   |-- api_review.go
    |   |-- api_user.go
    |   |-- api_user_bookshelf.go
    |   |-- logger.go
    |   |-- model_book.go
    |   |-- model_books.go
    |   |-- model_bookshelf.go
    |   |-- model_bookshelfs.go
    |   |-- model_review.go
    |   |-- model_reviews.go
    |   |-- model_user.go
    |   `-- routers.go
    `-- main.go

各个文件作用如下:

go/module_xxx.go: 定义了API中各种数据类型的结构。

//model_book.go
type Book struct {

	BookId int32 `json:"bookId"`

	BookName string `json:"bookName,omitempty"`

	Autor string `json:"autor,omitempty"`

	Info string `json:"info,omitempty"`
}

go/logger.go: 为每个路由的处理函数都注册了一个log.Printf()语句,即在服务端运行时,会打印出接收到的每一个有效请求的信息

//logger.go
func Logger(inner http.Handler, name string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        inner.ServeHTTP(w, r)

        log.Printf(
            "%s %s %s %s",
            r.Method,
            r.RequestURI,
            name,
            time.Since(start),
        )
    })
}

go/router.go: 为我们实现了路由匹配和匹配函数调用

//router.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)        //调用logger.go进行注册,使得每个有效的路由请求信息被打印出来

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

	return router
}

func Index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World!")
}

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

 

三、BookBlog客户端

本次项目使用vue.js框架,并利用vue-cli以开发单页面应用的博客。在开发过程中使用vue router、vuex进行一些功能上的辅助,并在开发过程中使用mock.js进行mock 测试。最后使用Ajax-hook包拦截客户端向服务端发送的请求添加token以完成jwt验证功能。

vue官网: https://cn.vuejs.org/

vue-cli: https://cli.vuejs.org/zh/

mock.js: http://mockjs.com/

Ajax-hook: https://github.com/wendux/Ajax-hook

1.使用vue-cli建立项目

在vue-cli安装完成后,使用以下命令建立bookblog项目文档

vue create bookblog

 之后进入如下界面

Vue CLI v4.5.9
? Please pick a preset: (Use arrow keys)
> Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

 可以选择Default默认配置,或是Manually手动配置。在此次项目中,直接使用Default默认即可。至于在Vue版本的选择上由于Vue 3的API文档还是beta版,因此我选择了Vue 2。在安装完成后就生成了bookblog项目文档。

bookblog
|-- README.md
|-- babel.config.js
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
`-- src

在项目文档生成后,即可使用vue框架正式开始BookBlog的页面的开发。为了更好地使用vue-cli,还需要了解学习vue的语法、单文件组件、npm、webpack等等相关知识(详情参看Vue官方网站)。在这些准备工作完成后,即可正式开始项目开发了。

2. book_blog_api包的使用

为了使用在上文中使用Swagger Editor生成的api接口book_blog_api包,需要在bookblog项目文档中进行npm install安装

npm install fentender/book_blog_api --save

 在安装完成后即可在项目中调用该api包,以向服务端发送数据请求。具体用法如下:

import BookBlogApi from 'book_blog_api';

var api = new BookBlogApi.BookApi()

var bookId = 56; // {Number} Book's ID


var callback = function(error, data, response) {
  if (error) {
    console.error(error);
  } else {
    console.log('API called successfully. Returned data: ' + data);
  }
};
api.getBook(bookId, callback);

 以下为本次项目中用于获得书籍信息时调用getBooks()接口的一次具体例子:

//Books.vue
import BookBlogApi from 'book_blog_api';

var api = new BookBlogApi.BookApi();
var Books = null;

export default {
  name: 'Books',
  data: function() {
    return {
      books: Books.books,
      num: Books.num,
      currentPage: 1
    }
  },
  ...
  methods: {
    setData(err, data) {
      if(err) {
        this.error = err.toString();
      } else {
        this.books = data.books;
        this.num = data.num;
      }
    },
  },
  beforeRouteEnter(to, from, next) {
    if(!Books) {
      api.getBooks( {num: 1}, (err, data, response) => {
        let obj
        if(response.body == null) {
          obj = JSON.parse(response.text);
        } else {
          obj = data;
        }
        Books = obj;
        next(vm => vm.setData(err, obj));
      })
    } else {
      next(vm => vm.setData(null, Books));
    }
  }, 
  ...
}

3.使用mock拦截请求,模拟数据并返回

同样的,为了在项目中使用mockjs服务,需要先进行npm包的安装

npm install mockjs --save-dev    //由于mock测试只需要在开发环境进行,而在生成环境就不需要了,因此使用-dev

 为了便于在完成客户端的开发后将mockjs测试关闭,独立创建一个文件用于保存mockjs测试代码。

//mock.js
import Mock from 'mockjs';
import ruler from './ruler'
// 请求模拟数据

//Books
Mock.mock(/books$/, 'get', ruler.books);
Mock.mock(/books\/[0-9]+$/, 'get', ruler.book);
Mock.mock(/books\/[0-9]+$/, 'delete', null);

...
...
...

Mock.mock(/users\/[a-zA-Z0-9%]+\/bookshelfs\/[a-zA-Z0-9%]+\/[0-9]+$/, 'post', null);
Mock.mock(/users\/[a-zA-Z0-9%]+\/bookshelfs\/[a-zA-Z0-9%]+\/[0-9]+$/, 'delete', null);

//Token
Mock.mock(/token$/, 'post', ruler.token);
Mock.mock(/token$/, 'get', ruler.token);

//User
Mock.mock(/users\/[a-zA-Z0-9%]+$/, 'get', ruler.user);

 ruler.js用于保存mockjs生成的模拟数据规范

import Mock from 'mockjs';

var Random = Mock.Random;

var exports = function() {
    this.book = {
        'bookId|+1' : 0,
        'bookName' : /[A-Z][a-z]{4,8}/,
        'autor' : /[a-z]{4,6} [a-z]{4,6}/,
        'info' : Random.paragraph(8, 15)
    }
    this.books = {
        'num|25-50' : 1,
        'books|10' : [this.book]
    };

    this.review = {
        'ID|+1' : 0,
        'Content' : Random.paragraph(8, 15),
        'autor' : /[a-z]{4,6} [a-z]{4,6}/
    };
    this.reviews = {
        'num|25-50' : 1,
        'reviews|10' : [this.review]
    };

    this.user = {
        'Username' : Random.word(),
        'Password' : Random.word(8, 10)
    };

    this.bookshelf = {
        'num|25-50' : 1,
        'bookshelf|8' : [{
            'bookName' : /[a-z]{4,6} [a-z]{4,6}/,
            'bookId|+1' : 0
        }]
    }
    this.bookshelfs = {
        'num|25-50' : 1,
        'bookshelfs|8' : [{'bookshelfName' : /[a-z]{4,6} [a-z]{4,6}/}]
    }
    this.token = {
        'Token' : Random.word(8)
    }
}

export default new exports();

 完成以上文件后,即可在main.js文件中通过添加与取消该文件的导入来控制mock测试的开启与否。

import './mock/mock.js';

4.客户端JWT认证

 按照上文所述JWT设计思路:

  1. 客户端通过用户名和密码向服务器发送请求登陆
  2. 服务器收到请求数据,在数据库进行查询验证
  3. 如果验证成功,服务器签发一个Token给客户端
  4. 客户端可以将Token存放到SessionStroage 或者Cookie里
  5. 客服端设置监听,每次跳转路由,就判断 SessionStroage 中有无 Token ,没有就跳转到登录页面,有则跳转到对应路由页面
  6. 在每次客户端发送的请求中,在请求头中加上Token
  7. 在后端设置拦截器,用户登录后的每次请求都会经过这个拦截器校验Token是否有效
  8. 如果验证成功,则继续执行请求,返回请求到的数据

因此,为了实现JWT,首先需要在使用 login API成功登录客户端接收到Token后,需要将其存储在SessionStroage中。之后在每次跳转路由时,判断SessionStroage是否存在Token。并对于客户端的每个请求都进行拦截,在其请求头上加入Token。

为了储存Token在SeesionStorage,可以在调用signIn API的回调函数中,将数据存储在SessionStroage中。

//login.vue
...
...
api.signIn(userInfo, (err, data, response) => {
                if(response.statusCode == 200) {
                    alert("登录成功!");
                    let token;
                    if(response.body) {
                        token = data;
                    } else {
                        token = JSON.parse(response.text);
                    }
                    this.$store.commit("setToken", { token: token.Token });        //调用vuex的store状态中的setToken方法
                    this.$store.commit("setUser", { username: this.username });
                    this.$router.push({ name:"User", params: { username: this.username }});
                } else {
                    alert("账号登录失败,请重新输入");
                }
            })
...
...


//store/index.js
mutations: {
        setToken(state, token) {
            state.token = token.token;
            sessionStorage.setItem("token", state.token);            //储存token至sessionStorage中
        }, 
        ...
}

 之后的实现在路由跳转时判断token存在与否则可使用vue router的导航守卫beforeEach(to ,from, next)钩子函数来实现。该钩子函数会在每次路由跳转时被调用,from为跳转路由时的起始路由,to为跳转的目标路由,next用于继续管道中的下一个钩子。

router.beforeEach((to, from, next) => {
  if ( !to.meta.requiresAuth ) {
    next();
  } else {
    let token = sessionStorage.getItem("token");  //从sessionStorage取出字段token的值
    if( token === '' || token === null) {    //判断token是否存在
      alert("请先登录账号");
      next({ name: "Login"} )        //若不存在就跳转路由至登录界面
    } else {
      next();                        //若存在则继续管道中的下一个钩子,不对该路由跳转操作
    }
  }
})

 最后,还需要在客户端的每一个发送请求的请求头上加入token,为了实现该功能,需要使用到Ajax-hook包,该包可以用于拦截浏览器XMLHttpRequest的库,并对其进行操作。

(其中需要注意的是,Ajax-hook与mockjs都会拦截XML HttpRequest请求,因此在同时使用这两者的功能时,两者会先后拦截客户端发送的请求并处理。这可能会造成一些错误,我暂时还没有解决。这时候的测试只能单独进行。)

官方案例:

import {proxy, unProxy} from "ajax-hook";
proxy({
    //请求发起前进入
    onRequest: (config, handler) => {
        console.log(config.url)
        handler.next(config);
    },
    //请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
    onError: (err, handler) => {
        console.log(err.type)
        handler.next(err)
    },
    //请求成功后进入
    onResponse: (response, handler) => {
        console.log(response.response)
        handler.next(response)
    }
})

 通过使用该包,即可以实现出我们所需要的功能。

//http/http.js
import store from "../store";
import {proxy} from "ajax-hook";

proxy({
    //请求发起前进入
    onRequest: (config, handler) => { 
        if(store.state.token != ''){
            config.headers.Authorization = store.state.token        //在请求的headers中加入Authorization
        }
        handler.next(config);
    },
    //请求发生错误时进入
    onError: (err, handler) => {
        handler.next(err)
    },
    //请求成功后进入
    onResponse: (response, handler) => {
        handler.next(response)
    }
})

 

5.客户端结构

以下即为本次项目完成后的客户端src目录结构:

src
|-- App.vue
|-- assets
|   `-- imgs
|       |-- book.png
|       |-- bookshelf.png
|       |-- logo-mini.png
|       `-- logo.png
|-- components
|   |-- BookBrief.vue
|   |-- BookshelfBookBrief.vue
|   |-- BookshelfBrief.vue
|   |-- BookshelfFooter.vue
|   |-- Nav.vue
|   |-- PageFooter.vue
|   `-- ReviewBrief.vue
|-- http
|   `-- http.js
|-- main.js
|-- mock
|   |-- MockServer.js
|   |-- mock.js
|   `-- ruler.js
|-- router
|   `-- index.js
|-- store
|   `-- index.js
`-- views
    |-- Book.vue
    |-- Books.vue
    |-- Bookshelf.vue
    |-- Bookshelfs.vue
    |-- Home.vue
    |-- Login.vue
    |-- Review.vue
    |-- Reviews.vue
    |-- Signup.vue
    `-- User.vue

四、BookBlog服务端

本次项目服务端采用由Swagger自动生成的go-server为基础编写而成,使用Boltdb作为数据库,并借助negroni中间件实现jwt的验证。在最后使用postman进行测试。

Boltdb地址: https://github.com/boltdb/bolt

postman官网: https://www.postman.com/

negroni地址: https://github.com/urfave/negroni

1.API的编写

由于Swagger自动生成的go-server已经编写好了服务端的路由匹配功能,因此在服务端的工作只需要完成各个路由对应的API函数功能即可。以GetBooks为例:

//GetBooks 根据请求读取相应Book[]并返回
func GetBooks(w http.ResponseWriter, r *http.Request) {
	var i, j, pageNumber int = 0, 0, 1
	var book [30]Book = [30]Book{}
	if r.URL.Query()["pageNumber"] != nil {
		pageNumber, _ = strconv.Atoi(r.URL.Query()["pageNumber"][0])
	}

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

	db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("Book"))
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			if ((pageNumber-1)*10 > i || i >= pageNumber*10) && pageNumber != -1 {
				i++
				continue
			}
			json.Unmarshal(v, &book[j])
			j++
			i++
		}
		return nil
	})

	var buf []byte
	if pageNumber != -1 {
		buf, _ = json.Marshal(Books{int32(i), book[:10]})
	} else {
		buf, _ = json.Marshal(Books{int32(i), book[:]})
	}
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	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.WriteHeader(http.StatusOK)
	w.Write(buf)
}

 在完成GetBooks函数后,即可使用GET  /books。 按照以上例子,逐步将所有API填补实现即可。

2.Postman的使用

在编写完成API函数后即可使用Postman向服务端请求资源,以测试对应的功能。Postman的使用方法在其官网上已有详细的教程,通过简单的学习就可以开始使用了。

测试:GET /books

GET /books测试

 

3.JWT的服务端实现

为了实现token验证,我们需要服务端每次接收请求后、正式处理请求前,在这两者之间进行token的验证。因此我们可以用到中间件negroni。它能够让我们添加中间件,以达到在API函数处理请求前验证token的功能。

首先,由于博客中并不是所有的功能都需要登录账号,因此只有一部分API需要验证token,因此对于每一个路由都为其添加requiredToken属性,通过设置它来控制其是否需要用到验证token的中间件。

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

其次,在routers.go中修改原先的路由匹配

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
}

 改为

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)

		//根据属性requiredToken判断是否需要使用中间件验证token
		if route.requiredToken {
			router.Methods(route.Method).
				Path(route.Pattern).
				Name(route.Name).
				Handler(negroni.New(negroni.HandlerFunc(authorizedValid), negroni.Wrap(handler)))
		} else {
			router.
				Methods(route.Method).
				Path(route.Pattern).
				Name(route.Name).
				Handler(handler)
		}
	}

	return router
}

 最后完成authorizedValid中间件的编写,实现token验证

package swagger

import (
	"github.com/boltdb/bolt"
	"github.com/dgrijalva/jwt-go"
	"github.com/dgrijalva/jwt-go/request"
	"log"
	"net/http"
)

func authorizedValid(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	token, errors := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
		func(token *jwt.Token) (interface{}, error) {
			return []byte(SecretKey), nil
		})
	db, err := bolt.Open("./database/bookblog.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}

	if errors == nil && token.Valid {
		tokenString, _ := token.SignedString([]byte(SecretKey))
		if DbKeyofToken(db, tokenString) != nil {
			db.Close()
			log.Println("Token验证成功")
			next(rw, r)
		} else {
			db.Close()
			log.Println("Token验证失败")
			rw.Header().Set("Content-Type", "application/json; charset=UTF-8")
			rw.Header().Set("Access-Control-Allow-Origin", "*")
			rw.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
			rw.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
			rw.WriteHeader(http.StatusUnauthorized)
			rw.Write([]byte("Token is not valid"))
		}
	} else {
		db.Close()
		log.Println("Token验证失败")
		rw.Header().Set("Content-Type", "application/json; charset=UTF-8")
		rw.Header().Set("Access-Control-Allow-Origin", "*")
		rw.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
		rw.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
		rw.WriteHeader(http.StatusUnauthorized)
		rw.Write([]byte("Unauthorized access to this resource"))
	}
}

 而JWT功能的测试也可通过Postman进行,只需要在请求中设置Authorization即可

 

五、前后端耦合

在完成前后端程序的编写后即可尝试将两者共同开启,测试其功能并进一步改善程序。然而在本次项目中前后端是独立部署,因此在测试时会遇到跨域的问题。

跨源资源共享(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

我们可以通过修改服务端返回Header来解决此问题。

    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	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.WriteHeader(http.StatusOK)

除此之外,在本次项目中的一些请求可能会触发 CORS 预检请求。它会在正式发送请求前先发送一次OPTIONS类型的方法请求,以获知服务器是否允许该实际请求,从而避免跨域请求对服务器的用户数据产生未预期的影响。只有满足以下如有条件的请求才不会触发预检请求,也被称为简单请求。

 因此我们还需要创建一个处理OPTIONS的路由

Route{
		"OPTIONS",
		strings.ToUpper("options"),
		"/{all:[a-zA-Z0-9=\\-\\/]+}",
		Options,
		false,
	},

 

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

六、项目展示

主页:

index

 登录界面:

sigin

 书架界面:(可自由创建删除且支持分页)

bookshelf

 书籍界面:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值