项目文档仓库:https://github.com/chuhongwei/WeBlog-doc
服务端仓库:https://github.com/chuhongwei/BlogServer
客户端仓库:https://github.com/zengty-2018/WeBlog_Client
我们小组实现的 ”博客网站“ 包含的功能有:
笔者主要负责后端开发,后端工作包括:
后端代码结构:
├── api
│ └── swagger.yaml
├── source
│ ├── Blogs
│ │ ├── WriteBlog.go
│ │ └── xxx.html
│ ├── Blog.db
│ ├── db.go
│ └── model.go
├── go
│ ├── api_article.go
│ ├── api_comment.go
│ ├── api_user.go
│ ├── jwt.go
│ ├── logger.go
│ ├── response.go
│ └── routers.go
├── LICENSE
├── README.md
└── main.go
source 文件夹用于存取数据
Blog.db
是数据库 ,它是由 blotDB 实现的;db.go
中,对数据库的 CURD操作被封装成函数;model.go
中定义了 Article、tag、user 和 comment 四个结构体;Blogs
文件夹下是一些网上的博客,作为本项目的素材。go 文件夹下是对以下各个功能的实现
api_article.go
: Article API 的实现api_comment.go
: Comment API 的实现api_user.go
: User API 的实现route.go
: 路由response.go
: API 响应logger.go
: 标准日志输出jwt.go
: 实现 token 签发认证在前端没有完成的时候,后端可以用 postman 来测试响应结果
go get -v github.com/chuhongwei/BlogServer
go run main.go
OpenAPI
是描述 REST API 的标准规范。 OpenAPI 描述允许人类和计算机无需首先阅读文档或了解实现实现即可发现 API 的功能。
Swagger
为 OpenAPI 的工具集,是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
go-swagger 源码
使用 Swagger 编辑器编写 yaml 文件,生成 API 接口文件。只要在给定的 yaml 文件,修改我们需要修改的部分,然后点击上方的 [Generate Server]
即可生成服务端代码,其中的 index.html
打开后是 API 可视化的页面
之后需要往里面填充与前端交互的逻辑,以及完成配置数据库等对后端的操作
本次项目选用的博客都拉取自 CSDN 博客网站
//获取博客文章的简要信息
GET /articles
//根据 id 获取某篇文章的详细内容
GET /article/{id}
//根据 id 获取文章的评论
GET /article/{id}/comments
//用户提交评论到文章中
POST /article/{id}/comments
//用户登录
POST /user/login
//用户注册
POST /user/register
Path Parameter
:即只包含请求地址的传参,比如 GET /article/3Query Parameter
:拼在请求地址上的传参,比如 Get /articles?page=1Body Parameter
:参数中涉及到传递对象,比如 POST /article/4/comment实现 Article 相关的 API:
// GET /articles?page=3
// 包含一个 query parameter: page
func GetArticles(w http.ResponseWriter, r *http.Request) {
// query parameter
r.ParseForm()
pageStr := r.FormValue("page")
page := int64(1)
if pageStr != "" {
page, _ = strconv.ParseInt(pageStr, 10, 64)
}
articles := db.GetArticles(-1, page)
Response(ResponseMessage{articles,nil,}, w, http.StatusOK)
}
// GET /article/1
// 包含一个 path parameter: id
func GetArticleByID(w http.ResponseWriter, r *http.Request) {
// path parameter
// 分解 URL
str := strings.Split(r.URL.Path, "/")
articleID, err := strconv.ParseInt((str[len(str)-1]), 10, 64)
fmt.Println(articleID)
// Article ID 字符串转换为数字失败
if err != nil {
Response(ResponseMessage{nil, "Invalid Article ID !"}, w, http.StatusBadRequest)
return
}
articles := db.GetArticles(articleID, 0)
if len(articles) != 0 {
Response(ResponseMessage{articles[0],nil,}, w, http.StatusOK)
return
} else {
// Article ID 对应的文章不存在
Response(ResponseMessage{nil, "Article Not Exists !",}, w, http.StatusNotFound)
return
}
}
实现 Comment 相关的 API:
token, isValid := CheckToken(w, r)
var comment db.Comment
err := json.NewDecoder(r.Body).Decode(&comment)
if claims, ok := token.Claims.(jwt.MapClaims); ok {
name, _ := claims["name"].(string)
comment.User = name
}
comment.Date = fmt.Sprintf("%d-%d-%d", time.Now().Year(), time.Now().Month(), time.Now().Day())
articleId := strings.Split(r.URL.Path, "/")[2]
comment.ArticleId, err = strconv.ParseInt(articleId, 10, 64)
for i := 0; i < len(articles); i++ {
articles[i].Comments = append(articles[i].Comments, comment)
}
实现 User 相关的 API:
// 包含 body parameter: userName 和 password, 验证成功返回一个 token 作为该登陆用户的验证信息
func PostUserLogin(w http.ResponseWriter, r *http.Request) {
db.Init()
var user db.User
err := json.NewDecoder(r.Body).Decode(&user)
name := db.GetUser(user.Username).Username
password := db.GetUser(user.Username).Password
// 验证用户名与密码是否对应
if name != user.Username || password != user.Password {
Response(ResponseMessage{nil,"UserName or Password Error !",}, w, http.StatusBadRequest)
return
}
tokenStr, err := SignToken(user.Username, user.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Error when sign the token !")
Response(ResponseMessage{"Sign token error !",nil,}, w, http.StatusOK)
}
// 返回 Token
Response(ResponseMessage{tokenStr,nil,}, w, http.StatusOK)
}
response.go 用来处理响应 Response
type ResponseMessage struct {
OkMessage interface{} `json:"ok,omitempty"`
ErrorMessage interface{} `json:"error,omitempty"`
}
func Response(response interface{}, w http.ResponseWriter, code int) {
jsonData, err := json.Marshal(&response)
if err != nil {
log.Fatal(err.Error())
}
w.Header().Set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(jsonData)
w.WriteHeader(code)
}
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", "Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
- 客户端使用 userName 跟 password 请求登录
- 服务端收到请求,验证 userName 和 password
- 验证成功后,服务端会生成一个 Token,把它发给客户端
- 客户端收到 Token 后把它存储起来,比如放在浏览器的 Cookie 里
- 客户端每次向服务端请求资源时,带着服务端签发的 Token
- 服务端收到请求,验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据,免去登录步骤
const jwtSecret = "serviceHW9"
func SignToken(userName, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Hour * time.Duration(1))
claims := make(jwt.MapClaims)
claims["name"] = userName
claims["pwd"] = password
claims["iat"] = nowTime.Unix()
claims["exp"] = expireTime.Unix()
// crypto.Hash 方案
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
// 该方法内部生成签名字符串,再用于获取完整、已签名的 token
return token.SignedString([]byte(jwtSecret))
}
func CheckToken(w http.ResponseWriter, r *http.Request) (*jwt.Token, bool) {
// 用户登录请求取出 token
token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if (err == nil && token.Valid) {
return token, true
}
w.WriteHeader(http.StatusUnauthorized)
if err != nil {
fmt.Fprint(w, "Authorized access to this resource iss invalid !")
}
if !token.Valid {
fmt.Fprint(w, "Token is invalid !")
}
return token, false
}
数据库使用 BoltDB,它是一个纯粹的 Key/Value 模型的程序,能够提供一个简单,快速,可靠的数据库
type Article struct {
Id int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
Tags []Tag `json:"tags,omitempty"`
Date string `json:"date,omitempty"`
Content string `json:"content,omitempty"`
Comments []Comment `json:"comments,omitempty"`
}
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
将对数据库的操作,封装成函数存放在 db.go 文件
// 获取数据库的路径
//get the database path
func DBPATH() string {
pt, _ := os.Getwd()
fmt.Println(path.Join(pt ,"/source/Blog.db"))
return path.Join(pt ,"/source/Blog.db")
}
// 初始化
//create the bucket for article and user
func Init() {
db, err := bolt.Open(DBPATH(), 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("article"))
if b == nil {
_, err := tx.CreateBucket([]byte("article"))
if err != nil {
log.Fatal(err)
}
}
b = tx.Bucket([]byte("user"))
if b == nil {
_, err := tx.CreateBucket([]byte("user"))
if err != nil {
log.Fatal(err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
}
打开数据库
获取一个事务 tx
根据 tx 获取bucket b
进行更新——b.Put(key, data)
进行查询——b.Get(key)
下面以 Articles 和 Users 为例
func writeOneBlog(id int64,title string,author string,tags []db.Tag,date string,content string,comments []db.Comment){
articles := db.Article{id,title,author,tags,date,content,comments}
users := db.User{author,author}
db.PutArticle(articles)
db.PutUser(users)
}
browae our directory
选择 tarvis ci 这个工具添加这个网站,使用 github 给这个网站授权,并且需要开启一下仓库里面哪一个仓库使用这种服务使用命令
npm install
安装好对应的依赖
使用命令npm run dev
运行项目后进入网址localhost:8080