├── .gitignore ├── LICENSE ├── README.md ├── app ├── config │ ├── config.go │ ├── development.yaml │ ├── product.yaml │ └── test.yaml ├── control ├── db │ └── db.go ├── endpoints │ ├── app.go │ ├── auth.go │ ├── handler_func_gen.go │ ├── health.go │ └── middleware.go ├── main.go ├── models │ └── app.go ├── service │ ├── middleware.go │ └── service.go └── transport │ ├── http.go │ ├── middleware.go │ └── server.go └── cmd └── client ├── main.go └── netrpc └── client.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .swp 3 | build/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Xi Tian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin-microservice-boilerplate 2 | 3 | A microservice boilerplate using golang [gin](https://github.com/gin-gonic/gin), with CAS authentication middleware integrated. Inspired by [go-kit](https://github.com/go-kit/kit). -------------------------------------------------------------------------------- /app/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "path/filepath" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var config *viper.Viper 11 | 12 | // Init is an exported method that takes the environment starts the viper 13 | // (external lib) and returns the configuration struct. 14 | func Init(env string) { 15 | var err error 16 | v := viper.New() 17 | v.SetConfigType("yaml") 18 | v.SetConfigName(env) 19 | v.AddConfigPath("../config/") 20 | v.AddConfigPath("config/") 21 | err = v.ReadInConfig() 22 | if err != nil { 23 | log.Fatal("error on parsing configuration file") 24 | } 25 | config = v 26 | } 27 | 28 | func relativePath(basedir string, path *string) { 29 | p := *path 30 | if p != "" && p[0] != '/' { 31 | *path = filepath.Join(basedir, p) 32 | } 33 | } 34 | 35 | func GetConfig() *viper.Viper { 36 | return config 37 | } 38 | -------------------------------------------------------------------------------- /app/config/development.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | host: user:passwd@tcp(hostname:port)/database 3 | 4 | http: 5 | session_domain: appdomain 6 | secret: secret 7 | 8 | server: 9 | port: :8080 10 | appid: appid 11 | key: secret_key 12 | 13 | env: 14 | mode: debug 15 | 16 | auth: 17 | cas: https://domain 18 | -------------------------------------------------------------------------------- /app/config/product.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | host: user:passwd@tcp(hostname:port)/database 3 | 4 | http: 5 | session_domain: appdomain 6 | secret: secret 7 | 8 | server: 9 | port: :30000 10 | appid: appid 11 | key: secret_key 12 | 13 | env: 14 | mode: product 15 | 16 | auth: 17 | cas: https://domain 18 | -------------------------------------------------------------------------------- /app/config/test.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | host: root:passwd@tcp(localhost:3306)/database 3 | 4 | http: 5 | session_domain: appdomain 6 | secret: secret 7 | 8 | server: 9 | port: :8080 10 | appid: appid 11 | key: secret_key 12 | 13 | env: 14 | mode: debug 15 | 16 | auth: 17 | cas: https://domain 18 | -------------------------------------------------------------------------------- /app/control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=$(cd $(dirname $0)/; pwd) 4 | cd $WORKSPACE 5 | 6 | app=app 7 | conf=product 8 | localconf=development 9 | pidfile=var/app.pid 10 | 11 | function check_pid() { 12 | if [ -f $pidfile ];then 13 | pid=`cat $pidfile` 14 | if [ -n $pid ]; then 15 | running=`ps -p $pid|grep -v "PID TTY" |wc -l` 16 | return $running 17 | fi 18 | fi 19 | return 0 20 | } 21 | 22 | function start() { 23 | check_pid 24 | running=$? 25 | if [ $running -gt 0 ];then 26 | echo -n "$app now is running already, pid=" 27 | cat $pidfile 28 | return 1 29 | fi 30 | 31 | mkdir -p var 32 | 33 | c=$conf 34 | if [ -f $localconf ];then 35 | c=$localconf 36 | fi 37 | env GOTRACEBACK=crash nohup ./$app -e $c &> /dev/null & 38 | sleep 1 39 | running=`ps -p $! | grep -v "PID TTY" | wc -l` 40 | if [ $running -gt 0 ];then 41 | echo $! > $pidfile 42 | echo "$app started..., pid=$!" 43 | else 44 | echo "$app failed to start" 45 | return 1 46 | fi 47 | } 48 | 49 | function stop() { 50 | check_pid 51 | running=$? 52 | if [ $running -gt 0 ];then 53 | pid=`cat $pidfile` 54 | kill $pid 55 | rm -f $pidfile 56 | echo "$app stoped" 57 | else 58 | echo "$app already stoped" 59 | fi 60 | } 61 | 62 | function restart() { 63 | stop 64 | sleep 1 65 | start 66 | } 67 | 68 | function status() { 69 | check_pid 70 | running=$? 71 | if [ $running -gt 0 ];then 72 | echo "started" 73 | else 74 | echo "stoped" 75 | fi 76 | } 77 | 78 | function build() { 79 | go build 80 | if [ $? -ne 0 ]; then 81 | exit $? 82 | fi 83 | } 84 | 85 | function help() { 86 | echo "$0 pid|reload|build|start|stop|restart|status" 87 | } 88 | 89 | function pid() { 90 | cat $pidfile 91 | } 92 | 93 | function reload() { 94 | build 95 | restart 96 | tailf 97 | } 98 | 99 | if [ "$1" == "" ]; then 100 | help 101 | elif [ "$1" == "stop" ];then 102 | stop 103 | elif [ "$1" == "start" ];then 104 | start 105 | elif [ "$1" == "restart" ];then 106 | restart 107 | elif [ "$1" == "status" ];then 108 | status 109 | elif [ "$1" == "build" ];then 110 | build 111 | elif [ "$1" == "pid" ];then 112 | pid 113 | elif [ "$1" == "reload" ];then 114 | reload 115 | else 116 | help 117 | fi 118 | -------------------------------------------------------------------------------- /app/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | _ "github.com/go-sql-driver/mysql" 5 | "github.com/go-xorm/xorm" 6 | ) 7 | 8 | var dbSession *xorm.Engine 9 | 10 | func Init(dataSourceName string) (*xorm.Engine, error) { 11 | if dbSession == nil { 12 | var err error 13 | dbSession, err = xorm.NewEngine("mysql", dataSourceName) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if err = dbSession.Ping(); err != nil { 19 | return nil, err 20 | } 21 | } 22 | return dbSession, nil 23 | } 24 | -------------------------------------------------------------------------------- /app/endpoints/app.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin/binding" 8 | 9 | "github.com/burxtx/gin-microservice-boilerplate/app/models" 10 | "github.com/burxtx/gin-microservice-boilerplate/app/service" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type GetRequest struct { 15 | Id string `form:"id" json:"id" binding:"required"` 16 | } 17 | 18 | type GetResponse struct { 19 | Data models.App `json:"data"` 20 | Err error `json:"err"` 21 | } 22 | 23 | func MakeGetEndpoint(s service.AppService) gin.HandlerFunc { 24 | return func(c *gin.Context) { 25 | fmt.Println("--- gen get handler ---") 26 | var getReq GetRequest 27 | if err := c.ShouldBindWith(&getReq, binding.Query); err == nil { 28 | 29 | } else { 30 | c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()}) 31 | return 32 | } 33 | app, getErr := s.Get(c, getReq.Id) 34 | if getErr != nil { 35 | http.Error(c.Writer, getErr.Error(), http.StatusInternalServerError) 36 | return 37 | } 38 | response := GetResponse{ 39 | Data: app, 40 | Err: nil, 41 | } 42 | c.JSON(200, response) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/endpoints/auth.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/burxtx/gin-microservice-boilerplate/app/config" 8 | "github.com/gin-gonic/gin" 9 | "github.com/gorilla/sessions" 10 | ) 11 | 12 | type AuthEndpoint struct { 13 | } 14 | 15 | func (a *AuthEndpoint) Logout(c *gin.Context) { 16 | cfg := config.GetConfig() 17 | secret := cfg.GetString("http.secret") 18 | session_domain := cfg.GetString("http.session_domain") 19 | store := sessions.NewCookieStore([]byte(secret)) 20 | callback := fmt.Sprintf("%s://%s%s", "http", c.Request.Host, "/") 21 | cas := fmt.Sprintf("%s/logout?service=%s", cfg.GetString("auth.cas"), callback) 22 | session, err := store.Get(c.Request, session_domain) 23 | if err != nil { 24 | http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 25 | } 26 | var msg []byte 27 | if session != nil { 28 | session.Options.MaxAge = -1 29 | saveErr := session.Save(c.Request, c.Writer) 30 | if saveErr != nil { 31 | http.Error(c.Writer, saveErr.Error(), http.StatusInternalServerError) 32 | } 33 | http.Redirect(c.Writer, c.Request, cas, 302) 34 | } else { 35 | msg = []byte("already logout!") 36 | } 37 | c.JSON(200, gin.H{"msg": msg}) 38 | return 39 | } 40 | 41 | func (a *AuthEndpoint) GetCurrentUser(c *gin.Context) { 42 | cfg := config.GetConfig() 43 | secret := cfg.GetString("http.secret") 44 | session_domain := cfg.GetString("http.session_domain") 45 | store := sessions.NewCookieStore([]byte(secret)) 46 | session, err := store.Get(c.Request, session_domain) 47 | if err != nil { 48 | http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 49 | return 50 | } 51 | user := session.Values["user"] 52 | c.JSON(200, gin.H{"user": user}) 53 | } 54 | -------------------------------------------------------------------------------- /app/endpoints/handler_func_gen.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/burxtx/gin-microservice-boilerplate/app/service" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type AppEndpoint struct { 11 | GetEndpoint gin.HandlerFunc 12 | } 13 | 14 | func New(s service.AppService) AppEndpoint { 15 | eps := AppEndpoint{ 16 | GetEndpoint: MakeGetEndpoint(s), 17 | } 18 | fmt.Println("=== endpoints ready ===") 19 | return eps 20 | } 21 | -------------------------------------------------------------------------------- /app/endpoints/health.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type HealthEndpoint struct{} 10 | 11 | func (h *HealthEndpoint) Status(c *gin.Context) { 12 | c.String(http.StatusOK, "Working!") 13 | } 14 | -------------------------------------------------------------------------------- /app/endpoints/middleware.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | -------------------------------------------------------------------------------- /app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/burxtx/gin-microservice-boilerplate/app/config" 9 | "github.com/burxtx/gin-microservice-boilerplate/app/db" 10 | "github.com/burxtx/gin-microservice-boilerplate/app/endpoints" 11 | "github.com/burxtx/gin-microservice-boilerplate/app/service" 12 | "github.com/burxtx/gin-microservice-boilerplate/app/transport" 13 | ) 14 | 15 | func main() { 16 | enviroment := flag.String("e", "development", "") 17 | flag.Usage = func() { 18 | fmt.Println("Usage: server -e {mode}") 19 | os.Exit(1) 20 | } 21 | flag.Parse() 22 | config.Init(*enviroment) 23 | c := config.GetConfig() 24 | datasource := c.GetString("database.host") 25 | dbSession, err := db.Init(datasource) 26 | if err != nil { 27 | panic(fmt.Errorf("Fatal error database connection: %s \n", err)) 28 | } 29 | svc := service.New(dbSession) 30 | eps := endpoints.New(svc) 31 | transport.NewHttpHandler(eps) 32 | } 33 | -------------------------------------------------------------------------------- /app/models/app.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type App struct { 6 | Id int64 `json:"id"` 7 | Title string `json:"title"` 8 | Description string `json:"description"` 9 | CreateTime time.Time `json:"create_time" time_format:"2006-01-02 15:04:05"` 10 | UpdateTime time.Time `xorm:"updated" time_format:"2006-01-02 15:04:05"` 11 | } 12 | -------------------------------------------------------------------------------- /app/service/middleware.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /app/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/burxtx/gin-microservice-boilerplate/app/models" 7 | "github.com/gin-gonic/gin" 8 | "github.com/go-xorm/builder" 9 | "github.com/go-xorm/xorm" 10 | ) 11 | 12 | type AppService interface { 13 | Find(ctx *gin.Context, cond builder.Cond, orderCols ...string) ([]models.App, error) 14 | Get(ctx *gin.Context, id string) (models.App, error) 15 | Add(ctx *gin.Context, audit models.App) (int64, error) 16 | } 17 | 18 | type appService struct{ db *xorm.Engine } 19 | 20 | func (a *appService) Find(c *gin.Context, cond builder.Cond, orderCols ...string) ([]models.App, error) { 21 | all := make([]models.App, 0) 22 | err := a.db.Where(cond).Desc(orderCols...).Find(&all) 23 | if err != nil { 24 | return all, err 25 | } 26 | return all, nil 27 | } 28 | 29 | func (a *appService) Get(c *gin.Context, id string) (models.App, error) { 30 | audit := new(models.App) 31 | has, err := a.db.Where("id=?", id).Get(audit) 32 | if err != nil { 33 | return models.App{}, err 34 | } 35 | if has != true { 36 | return models.App{}, nil 37 | } 38 | return *audit, nil 39 | } 40 | 41 | func (a *appService) Add(c *gin.Context, audit models.App) (int64, error) { 42 | affected, err := a.db.Insert(audit) 43 | if err == nil { 44 | return affected, err 45 | } 46 | return affected, nil 47 | } 48 | 49 | func NewAppService(db *xorm.Engine) AppService { 50 | return &appService{db} 51 | } 52 | 53 | func New(db *xorm.Engine) AppService { 54 | var svc AppService = NewAppService(db) 55 | fmt.Println("=== service ready ===") 56 | return svc 57 | } 58 | -------------------------------------------------------------------------------- /app/transport/http.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | //"fmt" 5 | 6 | "github.com/burxtx/gin-microservice-boilerplate/app/config" 7 | "github.com/burxtx/gin-microservice-boilerplate/app/endpoints" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func NewHttpRouter(eps endpoints.AppEndpoint) *gin.Engine { 12 | c := config.GetConfig() 13 | if c.GetString("env.mode") != "debug" { 14 | gin.SetMode(gin.ReleaseMode) 15 | } 16 | router := gin.New() 17 | router.Use(gin.Logger()) 18 | router.Use(gin.Recovery()) 19 | 20 | health := new(endpoints.HealthEndpoint) 21 | router.GET("/health", health.Status) 22 | auth := new(endpoints.AuthEndpoint) 23 | router.GET("/logout", auth.Logout) 24 | 25 | router.Use(CasAuthMiddleware()) 26 | router.GET("/", func(c *gin.Context) { 27 | c.Redirect(302, "/index.html") 28 | }) 29 | router.GET("/user", auth.GetCurrentUser) 30 | v1 := router.Group("v1") 31 | { 32 | auditGroup := v1.Group("app") 33 | { 34 | auditGroup.GET("/get", eps.GetEndpoint) 35 | 36 | } 37 | } 38 | return router 39 | } 40 | 41 | /* TODO:encode decode methods */ 42 | -------------------------------------------------------------------------------- /app/transport/middleware.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/burxtx/gin-microservice-boilerplate/app/config" 10 | "github.com/gin-gonic/gin" 11 | "github.com/gorilla/sessions" 12 | ) 13 | 14 | func CasAuthMiddleware() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | cfg := config.GetConfig() 17 | secret := cfg.GetString("http.secret") 18 | session_domain := cfg.GetString("http.session_domain") 19 | store := sessions.NewCookieStore([]byte(secret)) 20 | session, getErr := store.Get(c.Request, session_domain) 21 | if getErr != nil { 22 | http.Error(c.Writer, getErr.Error(), http.StatusInternalServerError) 23 | } 24 | var ticket string 25 | callback := fmt.Sprintf("%s://%s%s", "http", c.Request.Host, c.Request.URL.Path) 26 | cas := fmt.Sprintf("%s/login?service=%s", cfg.GetString("auth.cas"), callback) 27 | v := session.Values["user"] 28 | if v != nil { 29 | // http.Redirect(c.Writer, c.Request, cas, 302) 30 | // } else { 31 | c.Next() 32 | return 33 | } 34 | 35 | ticket = getTicketParam(c) 36 | if len(ticket) == 0 { 37 | http.Redirect(c.Writer, c.Request, cas, 302) 38 | return 39 | } 40 | 41 | validateUrl := fmt.Sprintf("%s/validate?service=%s&ticket=%s", cfg.GetString("auth.cas"), callback, ticket) 42 | client := http.Client{} 43 | request, requestErr := http.NewRequest("GET", validateUrl, nil) 44 | if requestErr != nil { 45 | http.Error(c.Writer, requestErr.Error(), http.StatusNotAcceptable) 46 | } 47 | resp, validateErr := client.Do(request) 48 | if validateErr != nil { 49 | http.Error(c.Writer, validateErr.Error(), http.StatusNotAcceptable) 50 | } 51 | defer resp.Body.Close() 52 | body, err := ioutil.ReadAll(resp.Body) 53 | if err != nil { 54 | http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 55 | } 56 | content := string(body) 57 | lines := strings.Split(content, "\n") 58 | if lines[0] != "yes" { 59 | http.Error(c.Writer, "Get username authorization info failed!", http.StatusInternalServerError) 60 | } 61 | session.Values["user"] = lines[1] 62 | session.Save(c.Request, c.Writer) 63 | c.Next() 64 | } 65 | } 66 | 67 | func getTicketParam(c *gin.Context) string { 68 | requestErr := c.Request.ParseForm() 69 | if requestErr != nil { 70 | http.Error(c.Writer, requestErr.Error(), http.StatusBadRequest) 71 | } 72 | return c.Request.Form.Get("ticket") 73 | } 74 | -------------------------------------------------------------------------------- /app/transport/server.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/burxtx/gin-microservice-boilerplate/app/config" 7 | "github.com/burxtx/gin-microservice-boilerplate/app/endpoints" 8 | ) 9 | 10 | func NewHttpHandler(eps endpoints.AppEndpoint) { 11 | config := config.GetConfig() 12 | r := NewHttpRouter(eps) 13 | fmt.Println("=== router ready ===") 14 | r.Run(config.GetString("server.port")) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/rpc" 6 | "os" 7 | 8 | "github.com/burxtx/gin-microservice-boilerplate/app/client/netrpc" 9 | "github.com/burxtx/gin-microservice-boilerplate/app/service" 10 | ) 11 | 12 | func main() { 13 | var netrpcAddr = flag.String("netrpc.addr", "localhost:8003", "Address for net/rpc server") 14 | flag.Parse() 15 | cli, err := rpc.DialHttp("tcp", *netrpcAddr) 16 | if err != nil { 17 | os.Exit(1) 18 | } 19 | defer cli.Close() 20 | var svc service.AppService 21 | svc = netrpc.New() 22 | } 23 | -------------------------------------------------------------------------------- /cmd/client/netrpc/client.go: -------------------------------------------------------------------------------- 1 | package netrpc 2 | 3 | import ( 4 | "net/rpc" 5 | 6 | "github.com/burxtx/gin-microservice-boilerplate/app/models" 7 | "github.com/burxtx/gin-microservice-boilerplate/app/service" 8 | ) 9 | 10 | func New(cli *rpc.Client) service.AppService { 11 | return client{cli} 12 | } 13 | 14 | type client struct { 15 | *rpc.Client 16 | } 17 | 18 | func (c client) Get(id string) (models.App, error) { 19 | var reply service.GetResponse 20 | if err := c.Client.Call("appservice.Get", service.GetRequest{Id: "1"}, &reply); err != nil { 21 | return models.App{}, err 22 | } 23 | return reply.Data, nil 24 | } 25 | 26 | /* func (c *client) Find(cond builder.Cond, orderCols ...string) ([]models.App, error) { 27 | var reply service.FindResponse 28 | err := a.db.Where(cond).Desc(orderCols...).Find(&all) 29 | if err != nil { 30 | return all, err 31 | } 32 | return all, nil 33 | } 34 | 35 | func (c *client) Add(audit models.App) (int64, error) { 36 | var reply service.AddResponse 37 | if err == nil { 38 | return affected, err 39 | } 40 | return affected, nil 41 | } 42 | */ 43 | --------------------------------------------------------------------------------