go grpc实战 微授权服务器

83 阅读2分钟

微授权服务器

前置任务

  1. protoc(翻译proto文件为go文件)
  2. go grpc包(google.golang.org/grpc)
  3. sqlite3驱动(github.com/mattn/go-sqlite3)
  4. redis驱动(github.com/go-redis/redis/v8)

准备任务

需要用到的proto文件

syntax = "proto3";
package main;
option go_package = "/auth;auth"; //这里格式为"/路径;翻译过后的包名",这里/为当前目录,如果就在当前目录生成就一个/就OK了,加了路径名后会自动创建目录
service author
{
    rpc auth(request) returns(response);
    rpc verfiy(request) returns(response);
    rpc logout(request) returns(response);
}
message request
{
    string userid = 1;
    string passwd = 2;
}
message response
{
    int32 code = 3;
    string words = 4;
}

命令行使用protoc翻译proto文件(这里不清楚读者的proto文件具体叫什么名,就直接默认翻译本目录下全部proto文件)

protoc -I. --go_out=plugins=grpc:. *.proto

这个时候生成的xxx.pb.go文件中为你自动生成了authorserver的接口,也就是这里指定的三个rpc函数。这里我们继承来这个接口

package main

import (
	"context"
	"encoding/base32"
	"math/rand"
	"strconv"
	"time"

	"github.com/oswaldoooo/examples/authserver/auth"
)

type authserve struct{}

var backup_resources = make(map[string]any)

func (s *authserve) Auth(ctx context.Context, req *auth.Request) (*auth.Response, error) {
	row := db.QueryRow("select info from userinfo where userid=? and passwd=?", req.Userid, req.Passwd)
	var resp auth.Response
	if row != nil {
		var info string
		err := row.Scan(&info)
		if err == nil {
			resp.Code = 1
			session := base32.StdEncoding.EncodeToString([]byte(strconv.Itoa(rand.Int())))
			err = rediscli.Set(context.Background(), session, info, time.Minute*time.Duration(30)).Err()
			if err == nil {
				resp.Words = session
			} else {
				backup_resources[session] = info
			}
		} else {
			resp.Code = 2
		}
	} else {
		resp.Code = 2
	}
	return &resp, nil
}

func (s *authserve) Verfiy(ctx context.Context, req *auth.Request) (*auth.Response, error) {
	cmd := rediscli.Get(context.Background(), req.Userid)
	var resp auth.Response
	if cmd != nil && cmd.Err() == nil {
		err := cmd.Scan(&resp.Words)
		if err == nil {
			resp.Code = 1
		} else {
			resp.Code = 2
		}
	} else {
		resp.Code = 2
	}
	if resp.Code == 2 {
		if info, ok := backup_resources[req.Userid]; ok {
			resp.Code = 1
			resp.Words = info.(string)
		}
	}
	return &resp, nil
}

func (s *authserve) Logout(ctx context.Context, req *auth.Request) (*auth.Response, error) {
	cmd := rediscli.Get(context.Background(), req.Userid)
	var resp auth.Response
	if cmd != nil && cmd.Err() == nil {
		err := cmd.Scan(&resp.Words)
		if err == nil {
			resp.Code = 1
			err = rediscli.Del(context.Background(), req.Userid).Err()
			if err != nil {
				errorlog.Println("[redis error]", err.Error())
			}
		} else {
			resp.Code = 2
		}
	} else {
		resp.Code = 2
	}
	if resp.Code == 2 {
		if _, ok := backup_resources[req.Userid]; ok {
			resp.Code = 1
			delete(backup_resources, req.Userid)
		}
	}
	resp.Code = 1
	return &resp, nil
}

有时候接口函数可能有好几个,咋这里用vscode的快捷继承,如果你使用的其它ide也打算使用快捷继承的话,请自行google。

屏幕录制2023-09-07 11.37.45.gif main 函数

package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"log"
	"net"
	"os"

	"github.com/go-redis/redis/v8"
	_ "github.com/mattn/go-sqlite3"
	"github.com/oswaldoooo/examples/authserver/auth"
	"google.golang.org/grpc"
)

var db *sql.DB
var rediscli *redis.Client
var errorlog *log.Logger
var redisadd string

func init() {
	var err error
	db, err = sql.Open("sqlite3", "userinfo.db")
	if err == nil {
		row := db.QueryRowContext(context.Background(), "select info from userinfo")
		if row.Err() == nil {
			var info string
			if row.Scan(&info) == nil {
				return
			}
		}
		_, err = db.ExecContext(context.Background(), "create table userinfo(userid varchar(20) not null UNIQUE,passwd varchar(100) not null,info varchar(100) not null)")
		if err != nil {
			fmt.Println("[error]", err.Error())
			os.Exit(1)
		}
	}
	rediscli = redis.NewClient(&redis.Options{Addr: redisadd})
}
func main() {
	errout := flag.String("errout", "", "--errout")
	flag.StringVar(&redisadd, "redadd", "localhost:6379", "--redadd xxx:xxx")
	flag.Parse()
	var finfo *os.File
	var err error
	if len(*errout) == 0 {
		finfo = os.Stderr
	} else {
		finfo, err = os.OpenFile(*errout, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
		if err != nil {
			fmt.Println("[error] open " + *errout + " failed")
			finfo = os.Stderr
		}
	}
	errorlog = log.New(finfo, "[error]", log.LUTC|log.Llongfile)
	ser := grpc.NewServer()
	auth.RegisterAuthorServer(ser, &authserve{})
	var listener net.Listener
	listener, err = net.Listen("tcp", ":9000")
	if err == nil {
		errorlog.Println(ser.Serve(listener))
	} else {
		errorlog.Println(err.Error())
	}
	errorlog.Println("server exit")
}

测试的客户端代码

package main

import (
	"context"
	"fmt"

	"github.com/oswaldoooo/examples/authserver/auth"
	"google.golang.org/grpc"
)

func main() {
	con, err := grpc.DialContext(context.Background(), "127.0.0.1:9000", grpc.WithInsecure())
	if err == nil {
		con.Connect()
		cli := auth.NewAuthorClient(con)
		var response *auth.Response
		response, err = cli.Auth(context.Background(), &auth.Request{Userid: "authserver", Passwd: "123456"})
		if err == nil {
			if response.Code == 1 {
				fmt.Println("get session", response.Words)
				response, err = cli.Verfiy(context.Background(), &auth.Request{Userid: response.Words})
				if err == nil {
					if response.Code == 1 {
						fmt.Println("user info is", response.Words)
					} else {
						fmt.Println("verify session failed")
					}
				}
			} else {
				fmt.Println("auth failed")
			}
		}
	}
	if err != nil {
		fmt.Println("[error]", err.Error())
	}
}

看看效果

截屏2023-09-07 11.43.18.png 大体这里就完成了,但是还没完全结束。

  1. 客户端密码传输前加个密再传避免明文传输(可自行按需添加)
  2. 注册和注销操作(还是数据库的crud操作,自行添加)
  3. 这里grpc通信是insecure,本质和裸奔性质差不多。如有需要自行查询grpc go加密操作