简单 web 服务与客户端开发实战
概述
利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。
- 任务目标
- 选择合适的 API,实现从接口或资源(领域)建模,到 API 设计的过程
- 使用 API 工具,编制 API 描述文件,编译生成服务器、客户端原型
- 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
- 使用 API 设计工具提供 Mock 服务,两个团队独立测试 API
- 使用 travis 测试相关模块
项目地址
详细代码见我们的组织
功能简述
我们小组完成了一个简单的Web博客。
我们实现了以下功能:
- 注册账户
- 登陆账户
- 查看博客
- 查看评论
- 发布评论
设计过程
Swagger生成接口文件
使用Swagger可以快速生成接口文件。只要输入yaml文件,点击上方 【Generate Client】则会生成一系列文件。
我们实现的接口如下:
GET /article/{id}/comments
GET /article/{id}
GET /articles
POST /article/{id}/comments
POST /user/login
POST /user/register
我在小组中负责后端有关于User的API设计。
生成的后端文件结构如下:
- db包存放数据库相关代码,数据库使用BoltDB
- model包存放需要用到的数据结构
- go包实现api代码
model
项目中需要声明几个数据结构,在model文件夹里的文件实现。
- User
User结构用于存储用户信息,包括用户名和密码。
package model
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
- Comment
Comment类存放评论信息,包括评论用户、文章id、评论信息、评论内容等
package model
type Comment struct {
User string `json:"user,omitempty"`
ArticleId int64 `json:"article_id,omitempty"`
Date string `json:"date,omitempty"`
Content string `json:"content,omitempty"`
}
API实现
response.go
response.go用来统一处理Response
MyResponse结构用来存储Response信息
Options单独处理OPTIONS(当需要loken认证时,会提前收到一个OPTIONS包)
Response用来发送回复信息
type MyResponse struct {
OkMessage interface{} `json:"ok,omitempty"`
ErrorMessage interface{} `json:"error,omitempty"`
}
func Options(w http.ResponseWriter, r *http.Request){
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", "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.WriteHeader(http.StatusOK)
}
func Response(response interface{}, w http.ResponseWriter, code int) {
jsonData, jErr := json.Marshal(&response)
if jErr != nil {
log.Fatal(jErr.Error())
}
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", "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.Write(jsonData)
w.WriteHeader(code)
}
api_user.go
api_user.go实现User相关的API。
登录功能和注册功能大同小异,总体上都是读取body当中的参数,将其转为User结构体,再从数据库中获得用户名对应的User信息,然后对用户信息进行验证,例如验证用户是否存在,账户与密码是否对应等等,最后返回token。
以登录函数为例:
func UserLoginPost(w http.ResponseWriter, r *http.Request) {
db.Init()
var user model.User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
Response(MyResponse{
nil,
"parameter error",
}, w, http.StatusBadRequest)
return
}
check := db.GetUser(user.Username)
if check.Username != user.Username || check.Password != user.Password {
Response(MyResponse{
nil,
"username or password error",
}, w, http.StatusBadRequest)
return
}
发布评论需要对用户进行验证。首先验证用户token,然后读入body参数,并解码为Comment结构,再根据token更新User,获得文章id,然后获取当前时间,然后把评论加入到数据库中。
func ArticleIdCommentPost(w http.ResponseWriter, r *http.Request) {
db.Init()
token, isValid := ValidateToken(w, r)
if isValid == false {
Response(MyResponse{
nil,
"authentication fail",
}, w, http.StatusBadRequest)
return
}
var comment model.Comment
err := json.NewDecoder(r.Body).Decode(&comment)
if err != nil {
Response(MyResponse{
nil,
err.Error(),
}, w, http.StatusBadRequest)
return
}
if v, ok := token.Claims.(jwt.MapClaims); ok {
name, _ := v["name"].(string)
comment.User = name
}
articleId := strings.Split(r.URL.Path, "/")[2]
comment.ArticleId, err = strconv.ParseInt(articleId, 10, 64)
if err != nil {
Response(MyResponse{
nil,
err.Error(),
}, w, http.StatusBadRequest)
return
}
comment.Date = fmt.Sprintf("%d-%d-%d", time.Now().Year(), time.Now().Month(), time.Now().Day())
articles := db.GetArticles(comment.ArticleId, 0)
if len(articles) == 0 {
Response(MyResponse{
nil,
"articles not found",
}, w, http.StatusBadRequest)
return
}
for i := 0; i < len(articles); i++ {
articles[i].Comments = append(articles[i].Comments, comment)
}
err = db.PutArticles(articles)
if err != nil {
Response(MyResponse{
nil,
err.Error(),
}, w, http.StatusBadRequest)
return
}
Response(MyResponse{
comment,
nil,
}, w, http.StatusOK)
}
Token
参考了相关网络,利用jwt实现了部分 API 支持 Token 认证。
如添加评论需要对用户进行验证:
token, isValid := ValidateToken(w, r)
登录页需要验证:
tokenString, err := SignToken(user.Username)
具体的token实现代码如下:
func ValidateToken(w http.ResponseWriter, r *http.Request) (*jwt.Token, bool) {
token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "Unauthorized access to this resource")
return token, false
}
if !token.Valid {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "token is invalid")
return token, false
}
return token, true
}
func SignToken(userName string) (string, error) {
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["name"] = userName
token.Claims = claims
return token.SignedString([]byte(SecretKey))
}