生命不止,继续go go go !!!
之前介绍过golang中restful api的博客,是使用redis作为持久化,httprouter作为框架:
Go实战–通过httprouter和redis框架搭建restful api服务(github.com/julienschmidt/httprouter)
今天,继续echo框架,这次加入mongodb作为持久化存储,使用jwt进行验证,来搭建一套rest api,类似Twitter。
其中,很多知识点之前都有介绍过:
关于golang中使用mongodb科技参考:
Go实战–golang使用ssl连接MongoDB(mgo)
关于golang中的使用jwt(JSON Web Token):
Go实战–golang中使用JWT(JSON Web Token)
代码结构:
./model
post.go
user.go
./handler handler.go post.go user.go main.go
model
这里没有什么好说的,就是建立model,需要注意的就是golang中struct中的标签。
一个用户user,一个邮箱post。
user.go
package model
import (
"gopkg.in/mgo.v2/bson"
)
type (
User struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email"`
Password string `json:"password,omitempty" bson:"password"`
Token string `json:"token,omitempty" bson:"-"`
Followers []string `json:"followers,omitempty" bson:"followers,omitempty"`
}
)
post.go
package model
import (
"gopkg.in/mgo.v2/bson"
)
type (
Post struct {
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
To string `json:"to" bson:"to"`
From string `json:"from" bson:"from"`
Message string `json:"message" bson:"message"`
}
)
handler
handler.go
handler中提出出来的公共部分。
package handler
import (
"gopkg.in/mgo.v2"
)
type (
Handler struct {
DB *mgo.Session
}
)
const (
// Key (Should come from somewhere else).
Key = "secret"
)
post.go
post中加入两个功能,一个创建post,一个拉取post。
关于”net/http”可以参考:
Go语言学习之net/http包(The way to go)
关于”strconv”可以参考:
Go语言学习之strconv包(The way to go)
package handler
import (
"go_echo_examples/twitter/model"
"net/http"
"strconv"
"github.com/labstack/echo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) CreatePost(c echo.Context) (err error) {
u := &model.User{
ID: bson.ObjectIdHex(userIDFromToken(c)),}
p := &model.Post{
ID: bson.NewObjectId(),From: u.ID.Hex(),}
if err = c.Bind(p); err != nil {
return
}
// Validation
if p.To == "" || p.Message == "" {
return &echo.HTTPError{Code: http.StatusBadRequest,Message: "invalid to or message fields"}
}
// Find user from database
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
return
}
// Save post in database
if err = db.DB("twitter").C("posts").Insert(p); err != nil {
return
}
return c.JSON(http.StatusCreated,p)
}
func (h *Handler) FetchPost(c echo.Context) (err error) {
userID := userIDFromToken(c)
page,_ := strconv.Atoi(c.QueryParam("page"))
limit,_ := strconv.Atoi(c.QueryParam("limit"))
// Defaults
if page == 0 {
page = 1
}
if limit == 0 {
limit = 100
}
// Retrieve posts from database
posts := []*model.Post{}
db := h.DB.Clone()
if err = db.DB("twitter").C("posts").
Find(bson.M{"to": userID}).
Skip((page - 1) * limit).
Limit(limit).
All(&posts); err != nil {
return
}
defer db.Close()
return c.JSON(http.StatusOK,posts)
}
user.go
这部分包括用户注册、登录、添加follow。
关于time包可以参考:
Go语言学习之time包(获取当前时间戳等)(the way to go)
package handler
import (
"go_echo_examples/twitter/model"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func (h *Handler) Signup(c echo.Context) (err error) {
// Bind
u := &model.User{ID: bson.NewObjectId()}
if err = c.Bind(u); err != nil {
return
}
// Validate
if u.Email == "" || u.Password == "" {
return &echo.HTTPError{Code: http.StatusBadRequest,Message: "invalid email or password"}
}
// Save user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").Insert(u); err != nil {
return
}
return c.JSON(http.StatusCreated,u)
}
func (h *Handler) Login(c echo.Context) (err error) {
// Bind
u := new(model.User)
if err = c.Bind(u); err != nil {
return
}
// Find user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
Find(bson.M{"email": u.Email,"password": u.Password}).One(u); err != nil {
if err == mgo.ErrNotFound {
return &echo.HTTPError{Code: http.StatusUnauthorized,Message: "invalid email or password"}
}
return
}
//-----
// JWT
//-----
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["id"] = u.ID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response
u.Token,err = token.SignedString([]byte(Key))
if err != nil {
return err
}
u.Password = "" // Don't send password
return c.JSON(http.StatusOK,u)
}
func (h *Handler) Follow(c echo.Context) (err error) {
userID := userIDFromToken(c)
id := c.Param("id")
// Add a follower to user
db := h.DB.Clone()
defer db.Close()
if err = db.DB("twitter").C("users").
UpdateId(bson.ObjectIdHex(id),bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil {
if err == mgo.ErrNotFound {
return echo.ErrNotFound
}
}
return
}
func userIDFromToken(c echo.Context) string {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return claims["id"].(string)
}
main
最后的main.go就相对很简单了。
main.go
package main
import (
"go_echo_examples/twitter/handler"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/labstack/gommon/log"
"gopkg.in/mgo.v2"
)
func main() {
e := echo.New()
e.Logger.SetLevel(log.ERROR)
e.Use(middleware.Logger())
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(handler.Key),Skipper: func(c echo.Context) bool {
// Skip authentication for and signup login requests
if c.Path() == "/login" || c.Path() == "/signup" {
return true
}
return false
},}))
// Database connection
db,err := mgo.Dial("localhost")
if err != nil {
e.Logger.Fatal(err)
}
// Create indices
if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{
Key: []string{"email"},Unique: true,}); err != nil {
log.Fatal(err)
}
// Initialize handler
h := &handler.Handler{DB: db}
// Routes
e.POST("/signup",h.Signup)
e.POST("/login",h.Login)
e.POST("/follow/:id",h.Follow)
e.POST("/posts",h.CreatePost)
e.GET("/Feed",h.FetchPost)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
测试
启动mongodb服务
mongod.exe --dbpath d:\mongodb_data\db
成功的话,结果:
2017-11-27T00:17:22.201-0700 I CONTROL [initandlisten] MongoDB starting : pid=17792 port=27017 dbpath=d:\mongodb_data\db 64-bit host=LAPTOP-MNU6522J
2017-11-27T00:17:22.202-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/Windows Server 2008 R2
2017-11-27T00:17:22.202-0700 I CONTROL [initandlisten] db version v3.4.6
2017-11-27T00:17:22.203-0700 I CONTROL [initandlisten] git version: c55eb86ef46ee7aede3b1e2a5d184a7df4bfb5b5
2017-11-27T00:17:22.203-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL 1.0.1u-fips 22 Sep 2016
2017-11-27T00:17:22.204-0700 I CONTROL [initandlisten] allocator: tcmalloc
2017-11-27T00:17:22.204-0700 I CONTROL [initandlisten] modules: none
2017-11-27T00:17:22.204-0700 I CONTROL [initandlisten] build environment:
2017-11-27T00:17:22.204-0700 I CONTROL [initandlisten] distmod: 2008plus-ssl
2017-11-27T00:17:22.205-0700 I CONTROL [initandlisten] distarch: x86_64
2017-11-27T00:17:22.205-0700 I CONTROL [initandlisten] target_arch: x86_64
2017-11-27T00:17:22.205-0700 I CONTROL [initandlisten] options: { storage: { dbPath: "d:\mongodb_data\db" } }
2017-11-27T00:17:22.261-0700 I - [initandlisten] Detected data files in d:\mongodb_data\db created by the 'wiredTiger' storage engine,so setting the active storage engine to 'wiredTiger'.
2017-11-27T00:17:22.271-0700 I STORAGE [initandlisten] wiredtiger_open config: create,cache_size=3540M,session_max=20000,eviction=(threads_min=4,threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0),2017-11-27T00:17:24.247-0700 I CONTROL [initandlisten]
2017-11-27T00:17:24.248-0700 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2017-11-27T00:17:24.259-0700 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2017-11-27T00:17:24.260-0700 I CONTROL [initandlisten]
2017-11-27T15:17:25.037+0800 I FTDC [initandlisten] Initializing full-time diagnostic data capture with directory 'd:/mongodb_data/db/diagnostic.data'
2017-11-27T15:17:25.046+0800 I NETWORK [thread1] waiting for connections on port 27017
运行main.go
mongodb控制台:
2017-11-27T15:23:39.223+0800 I NETWORK [thread1] connection accepted from 127.0.0.1:51150 #2 (1 connection now open)
2017-11-27T15:23:39.501+0800 I INDEX [conn2] build index on: twitter.users properties: { v: 2,unique: true,key: { email: 1 },name: "email_1",ns: "twitter.users" }
2017-11-27T15:23:39.501+0800 I INDEX [conn2] building index using bulk method; build may temporarily use up to 500 megabytes of RAM
2017-11-27T15:23:39.529+0800 I INDEX [conn2] build index done. scanned 0 total records. 0 secs
2017-11-27T15:23:39.530+0800 I COMMAND [conn2] command twitter.$cmd command: createIndexes { createIndexes: "users",indexes: [ { name: "email_1",ns: "twitter.users",unique: true } ] } numYields:0 reslen:113 locks:{ Global: { acquireCount: { r: 1,w: 1 } },Database: { acquireCount: { W: 1 } },Collection: { acquireCount: { w: 1 } } } protocol:op_query 303ms
用户注册
使用postman或是curl命令,这里使用curl命令了:
curl -X POST http://localhost:1323/signup -H "Content-Type:application/json" -d '{"email" :"wangshubo1989@126.co"m","password":"test"}'
curl -X POST http://localhost:1323/login -H "Content-Type:application/json" -d '{"email" :"wangshubo1989@126.com","password":"test"}'
返回:
{"id":"5a1bbe92271c7c5ac875e40e","email":"wangshubo1989@126.com","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwMjcwOTgsImlkIjoiNWExYmJlOTIyNzFjN2M1YWM4NzVlNDBlIn0.V__5q0fipKfPhcGop1rdioX5lFc7qSVz9bVfJ5zycvo"}
Follow用户
curl -X POST http://localhost:1323/follow/5a1bbe92271c7c5ac875e40e -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwMjcwOTgsImlkIjoiNWExYmJlOTIyNzFjN2M1YWM4NzVlNDBlIn0.V__5q0fipKfPhcGop1rdioX5lFc7qSVz9bVfJ5zycvo"
发送消息(邮件)
curl -X POST http://localhost:1323/posts -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwMjcwOTgsImlkIjoiNWExYmJlOTIyNzFjN2M1YWM4NzVlNDBlIn0.V__5q0fipKfPhcGop1rdioX5lFc7qSVz9bVfJ5zycvo" -H "Content-Type: application/json" -d '{"to":"58465b4ea6fe886d3215c6df","message":"hello"}'