• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    golang使用grpc+go-kit模拟oauth认证的操作

    我们使用grpc对外的接口,进行服务,模拟对外认证的接口

    首先我们要了解oauth的基本认证过程

    第三方的服务端,在oauth2.0中作为一个客户端的身份,进行请求数据。

    用户进行选择第三方的登陆,比如选择到某一个第三方的平台进行登陆,则会跳转到第三方登陆平台

    用户输入用户名密码,在第三方平台进行登陆,,如果登陆成功,则返回code。

    客户端,也就是我们想要登陆的网站,将会读取code,并且将会携带这个code,和第三方网站所颁发的密码,进行请求token,如果code和注册时所得到的密码,都验证成功,此时,第三方客户端会返回一个token。

    我们登陆的网站会携带这个token去请求用户身份资源的服务器,如果token比对成功,则返回用户的信息所以我们需要一些服务

    codeserver,作用,分发code,验证code的准确性

    tokenserver,作用分发token,验证token的准确性

    loginserver,作用,登陆成功后,调用codeserver得到code

    userdetailserver,作用调用tokenserver的token验证,验证token是否合法,如果合法,进行返回用户的基本信息继续,我们大概看一下这些功能具体怎样实现。

    实现

    codeserver

    type Codeserver struc (
    	GetCode ()
    	ValidCode ()
    )
    //函数的具体传参今不写了
    

    其实我们的code和token,主要是使用redis数据库进行实现,并且给申请的code和token设置过期时间, 也就是说,在数据库中实现一个定时的作用,如果,申请完code,长时间不申请token则这个code会过期,就会让用户重新进行登陆,重新获取code

    func (s ServicesA) GetCode(c context.Context, req *codeserver.GetCodeReuqest) (*codeserver.RCodeResponse, error) {
    	con , err := UseRedis()//加载redis,用于操作redis
    	if err != nil {
    		return nil , errors.New("the redis databases is not work")
    	}
    	randstr :=  GetRandomString(10)//随机生成一个字符串作为code
    	_ , err = con.Do("hset" , req.UserId , "code" , randstr)//插入数据库,用于获取token时进行验证
    	con.Do("set" , randstr , req.UserId , "EX" , 120)
    	con.Do("EXPIRE" , req.UserId , 20)//设置code的过期时间
    	if err != nil {
    		return nil , errors.New("data is not insert")
    	}
    	return codeserver.RCodeResponse{Code: randstr} , nil
    }
    //检查code是否合法
    func (s ServicesA) Isvalid(c context.Context, req *codeserver.ValidRequest) (*codeserver.ValidResponse, error) {
    	con , err := UseRedis()//加载redis
    	if err != nil {
    		return nil , errors.New("the databses is not work")
    	}
    	r , err := con.Do("get" , req.Code)//找到code,如果能找到code,则合法,找不到则不合法
    	if err != nil {
    		return nil , err
    	}
    	if r == nil {
    		return codeserver.ValidResponse{IsValid: false} , nil
    	} else {
    		return codeserver.ValidResponse{IsValid: true} , nil
    	}
    }
    

    至于其他的endpoint层和transport层等等,就先不写了,我们就这篇文章主要是看怎样模拟实现oauth

    tokenserver

    func Isvalid (request *codeserver.ValidRequest) bool {
    	lis , err := grpc.Dial("127.0.0.1:8081" , grpc.WithInsecure())
    	if err != nil {
    		log.Println(err)
    		return false
    	}
    	client := codeserver.NewCodeServerClient(lis)
    	rep , err := client.Isvalid(context.Background() , request)
    	if err != nil {
    		log.Println(err)
    		return false
    	}
    	if rep.IsValid {
    		return true
    	} else {
    		return false
    	}
    }
    func (s ServiceAI) GetToken(ctx context.Context, req *tokenservice.ReqGetToken) (*tokenservice.RepGetToken, error) {
    //判断code是否合法
    	if !Isvalid(codeserver.ValidRequest{UserId: req.UserId , Code: req.Code}) {
    		return nil , errors.New("code is not valid ")
    	}
    	con , err := UseRedis()
    	if err != nil {
    		return nil , errors.New("connet database default")
    	}
    	//通过code获取clientid
    	User := GetUserId(req.Code)
    	mysql , err := UseMysql()
    	if err != nil {
    		log.Println("get secrete default")
    	}
    	var c Client
    	mysql.Table("client").Where("id = ?",req.ClientId).Find(c)
    //在mysql数据库中进行查找,请求所携带的密码,是否与第三方注册时给的密码是否相同,如果不相同,则不返回token。
    	if c.Secret !=req.Secret {
    		fmt.Println(c.Secret , " " , req.Secret)
    		return nil , errors.New("not pi pei")
    	}
    	str := GetRandomString(11)
    	_ , err = con.Do("hset" , User , "token" ,  str)
    	con.Do("EXPIRE" , User , 120)
    	//将生成的token进行插入数据库,并设置过期时间,如果避免token被多次利用
    	con.Do("set" , str , User , "EX" , 120)
    	//设置userid和token的对应关系,避免没有对应上,客户端拿到token之后随便拿取其他人的用户滤数据
    	if err != nil {
    		return nil , err
    	}
    	return tokenservice.RepGetToken{Toen: str} , nil
    }
    //判断token是都合法,给userdetailserver用,当服务器接到token后,需要调用这个接口,查看token是否合法,如果合法返回用户数据
    func (s ServiceAI) IsValidToken(ctx context.Context, req *tokenservice.IsValidTokenReq) (*tokenservice.IsValidToeknRep, error) {
    	con , err := UseRedis()
    	if err != nil {
    		log.Println(err)
    		return nil , err
    	}
    	r , err := con.Do("get" ,req.Token)
    	if err != nil {
    		return nil , err
    	}
    	if r == nil {
    		return tokenservice.IsValidToeknRep{IsValid: false} , nil
    	}
    	rep := string(r.([]uint8))
    	return tokenservice.IsValidToeknRep{IsValid: true , Userid: rep} , nil
    }
    

    useroauthserver

    type User struct {
    	Id int
    	Name string
    	Password string
    	Al string
    	UId string
    }
    func usemysql () (*gorm.DB , error) {
    	return gorm.Open("mysql" , "root:123456@/oauth?charset=utf8parseTime=Trueloc=Local")
    }
    //调用codeserver接口,进行拿取code
    func getcode (userid string) string {
    	con , err := grpc.Dial(":8081" , grpc.WithInsecure())
    	if err != nil {
    		log.Println(err , errors.New("get code default"))
    	}
    	client := codeserver.NewCodeServerClient(con)
    	rep , err := client.GetCode(context.Background() , codeserver.GetCodeReuqest{UserId: userid})
    	if err != nil || rep == nil{
    		log.Println(err)
    		return ""
    	}
    	return rep.Code
    }
    //认证用户,将上传的用户名和密码进行比对。
    func (a AuthServicesA) AuthT(ctx context.Context, req *userauth.AuthRequest) (*userauth.AuthResponse, error) {
    	con , err := usemysql()
    	if err != nil {
    		log.Println(err)
    		return nil , errors.New("the database is connect default")
    	}
    	var u User
    	con.Table("user").Where("uid =?" , req.Id).Find(u)
    	//在数据库中进行查找,如果没找到该用户,说明该用户不存在,或者用户输入错误
    	if u == nil {
    		return nil , errors.New("the id is wrong ")
    	}
    	if req.Password != u.Password {
    		return nil , errors.New("the user password is wrong")
    	}
    //如果认证成功,则进行调用codeserver接口,返回code
    	code :=getcode(req.Id)
    	if code == "" {
    		return userauth.AuthResponse{IsTrue: false} , nil
    	}
    	return userauth.AuthResponse{Code: code , IsTrue: true} , nil
    }
    

    基本原理就是这样,但是我们还是差一个userdetail的服务端

    这个服务端,主要作用就是拿到请求的token,并进行检验,如果检验成功,返回用户数据,至于怎样检验,就是调用tokenserver中的检验接口。

    这里就不写了,留给读者完成。

    我写的这三个接口在gitee上有源码,是基于golang写的,使用的框架有grpc,go-kit的服务框架。

    具体地址gitee.com/silves-xiang

    补充:go-kit实践之2:go-kit 实现注册发现与负载均衡

    一、介绍

    grpc提供了简单的负载均衡,需要自己实现服务发现resolve。我们既然要使用go-kit来治理微服务,那么我们就使用go-kit的注册发现、负载均衡机制。

    go-kit官方【stringsvc3】例子中使用的负载均衡方案是通过服务端转发进行,翻找下源码go-kit的服务注册发现、负载均衡在【sd】包中。下面我们介绍怎么通过go-kit进行客户端负载均衡。

    go-kit提供的注册中心

    1、 etcd

    2、 consul

    3、 eureka

    4、 zookeeper

    go-kit提供的负载均衡

    1、 random[随机]

    2、 roundRobin[轮询]

    只需实现Balancer接口,我们可以很容易的增加其它负载均衡机制

    type Balancer interface {  
       Endpoint() (endpoint.Endpoint, error)  
    }

    etcd注册发现

    etcd和zookeeper类似是一个高可用、强一致性的存储仓库,拥有服务发现功能。 我们就通过go-kit提供的etcd包来实现服务注册发现

    二、示例

    1、protobuf文件及生成对应的go文件

    syntax = "proto3";
     
    // 请求书详情的参数结构  book_id 32位整形
    message BookInfoParams {
        int32 book_id = 1;
    } 
     
    // 书详情信息的结构   book_name字符串类型
    message BookInfo {
        int32 book_id = 1;
        string  book_name = 2;
    }
     
    // 请求书列表的参数结构  page、limit   32位整形
    message BookListParams {
        int32 page = 1;
        int32 limit = 2;
    }
      
    // 书列表的结构    BookInfo结构数组
    message BookList {
        repeated BookInfo book_list = 1;
    }
    // 定义 获取书详情  和 书列表服务   入参出参分别为上面所定义的结构
    service BookService {
        rpc GetBookInfo (BookInfoParams) returns (BookInfo) {}
        rpc GetBookList (BookListParams) returns (BookList) {}
    }

    生成对应的go语言代码文件:protoc --go_out=plugins=grpc:. book.proto (其中:protobuf文件名为:book.proto)

    2、Server端代码

    package main 
    import (
    	"MyKit"
    	"context"
    	"fmt"
    	"github.com/go-kit/kit/endpoint"
    	"github.com/go-kit/kit/log"
    	"github.com/go-kit/kit/sd/etcdv3"
    	grpc_transport "github.com/go-kit/kit/transport/grpc"
    	"google.golang.org/grpc"
    	"net"
    	"time"
    )
     
    type BookServer struct {
    	bookListHandler grpc_transport.Handler
    	bookInfoHandler grpc_transport.Handler
    }
     
    //一下两个方法实现了 protoc生成go文件对应的接口:
    /*
    // BookServiceServer is the server API for BookService service.
    type BookServiceServer interface {
    	GetBookInfo(context.Context, *BookInfoParams) (*BookInfo, error)
    	GetBookList(context.Context, *BookListParams) (*BookList, error)
    }
    */
    //通过grpc调用GetBookInfo时,GetBookInfo只做数据透传, 调用BookServer中对应Handler.ServeGRPC转交给go-kit处理
    func (s *BookServer) GetBookInfo(ctx context.Context, in *book.BookInfoParams) (*book.BookInfo, error) {
     
    	_, rsp, err := s.bookInfoHandler.ServeGRPC(ctx, in)
    	if err != nil {
    		return nil, err
     
    	}
    	/*
    	if info,ok:=rsp.(*book.BookInfo);ok {
    		return info,nil
    	}
    	return nil,errors.New("rsp.(*book.BookInfo)断言出错")
    	*/
    	return rsp.(*book.BookInfo), err //直接返回断言的结果
    }
     
    //通过grpc调用GetBookList时,GetBookList只做数据透传, 调用BookServer中对应Handler.ServeGRPC转交给go-kit处理
    func (s *BookServer) GetBookList(ctx context.Context, in *book.BookListParams) (*book.BookList, error) {
    	_, rsp, err := s.bookListHandler.ServeGRPC(ctx, in)
    	if err != nil {
    		return nil, err
    	}
    	return rsp.(*book.BookList), err
    }
     
    //创建bookList的EndPoint
    func makeGetBookListEndpoint()endpoint.Endpoint  {
    	return func(ctx context.Context, request interface{}) (response interface{}, err error) {
    		b:=new(book.BookList)
    		b.BookList=append(b.BookList,book.BookInfo{BookId:1,BookName:"Go语言入门到精通"})
    		b.BookList=append(b.BookList,book.BookInfo{BookId:2,BookName:"微服务入门到精通"})
    		b.BookList=append(b.BookList,book.BookInfo{BookId:2,BookName:"区块链入门到精通"})
    		return b,nil
    	}
    }
     
    //创建bookInfo的EndPoint
    func makeGetBookInfoEndpoint() endpoint.Endpoint {
    	return func(ctx context.Context, request interface{}) (interface{}, error) {
    		//请求详情时返回 书籍信息
    		req := request.(*book.BookInfoParams)
    		b := new(book.BookInfo)
    		b.BookId = req.BookId
    		b.BookName = "Go入门到精通"
    		return b, nil
    	}
    }
     
    func decodeRequest(_ context.Context, req interface{}) (interface{}, error) {
    	return req, nil
    }
     
    func encodeResponse(_ context.Context, rsp interface{}) (interface{}, error) {
    	return rsp, nil
    }
     
    func main() {
    	var (
    		etcdServer     = "127.0.0.1:2379"        //etcd服务的IP地址
    		prefix         = "/services/book/"       //服务的目录
    		ServerInstance = "127.0.0.1:50052"       //当前实例Server的地址
    		key            = prefix + ServerInstance //服务实例注册的路径
    		value          = ServerInstance
    		ctx            = context.Background()
    		//服务监听地址
    		serviceAddress = ":50052"
    	)
    	//etcd连接参数
    	option := etcdv3.ClientOptions{DialTimeout: time.Second * 3, DialKeepAlive: time.Second * 3}
    	//创建连接
    	client, err := etcdv3.NewClient(ctx, []string{etcdServer}, option)
    	if err != nil {
    		panic(err)
    	}
    	//创建注册
    	registrar := etcdv3.NewRegistrar(client, etcdv3.Service{Key: key, Value: value}, log.NewNopLogger())
    	registrar.Register() //启动注册服务
    	bookServer := new(BookServer)
    	bookListHandler := grpc_transport.NewServer(
    		makeGetBookListEndpoint(),
    		decodeRequest,
    		encodeResponse,
    	)
    	bookServer.bookListHandler = bookListHandler
     
    	bookInfoHandler := grpc_transport.NewServer(
    		makeGetBookInfoEndpoint(),
    		decodeRequest,
    		encodeResponse,
    	)
    	bookServer.bookInfoHandler = bookInfoHandler
     
    	listener, err := net.Listen("tcp", serviceAddress) //网络监听,注意对应的包为:"net"
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	gs := grpc.NewServer(grpc.UnaryInterceptor(grpc_transport.Interceptor))
    	book.RegisterBookServiceServer(gs, bookServer) //调用protoc生成的代码对应的注册方法
    	gs.Serve(listener)                             //启动Server
     
    }

    3、Client端代码

    package main 
    import (
    	"MyKit"
    	"context"
    	"fmt"
    	"github.com/go-kit/kit/endpoint"
    	"github.com/go-kit/kit/log"
    	"github.com/go-kit/kit/sd"
    	"github.com/go-kit/kit/sd/etcdv3"
    	"github.com/go-kit/kit/sd/lb"
    	"google.golang.org/grpc"
    	"io"
    	"time"
    )
     
    func main() {
     
    	var (
    		//注册中心地址
    		etcdServer = "127.0.0.1:2379"
    		//监听的服务前缀
    		prefix = "/services/book/"
    		ctx    = context.Background()
    	)
    	options := etcdv3.ClientOptions{
    		DialTimeout:   time.Second * 3,
    		DialKeepAlive: time.Second * 3,
    	}
    	//连接注册中心
    	client, err := etcdv3.NewClient(ctx, []string{etcdServer}, options)
    	if err != nil {
    		panic(err)
    	}
    	logger := log.NewNopLogger()
    	//创建实例管理器, 此管理器会Watch监听etc中prefix的目录变化更新缓存的服务实例数据
    	instancer, err := etcdv3.NewInstancer(client, prefix, logger)
    	if err != nil {
    		panic(err)
    	}
    	//创建端点管理器, 此管理器根据Factory和监听的到实例创建endPoint并订阅instancer的变化动态更新Factory创建的endPoint
    	endpointer := sd.NewEndpointer(instancer, reqFactory, logger) //reqFactory自定义的函数,主要用于端点层(endpoint)接受并显示数据
    	//创建负载均衡器
    	balancer := lb.NewRoundRobin(endpointer)
     
    	/**
    	我们可以通过负载均衡器直接获取请求的endPoint,发起请求
    	reqEndPoint,_ := balancer.Endpoint()
    	*/
     
    	/**
    	也可以通过retry定义尝试次数进行请求
    	*/
    	reqEndPoint := lb.Retry(3, 3*time.Second, balancer)
     
    	//现在我们可以通过 endPoint 发起请求了
    	req := struct{}{}
    	if _, err = reqEndPoint(ctx, req); err != nil {
    		panic(err)
    	}
    }
     
    //通过传入的 实例地址  创建对应的请求endPoint
    func reqFactory(instanceAddr string) (endpoint.Endpoint, io.Closer, error) {
    	return func(ctx context.Context, request interface{}) (interface{}, error) {
    		fmt.Println("请求服务: ", instanceAddr)
    		conn, err := grpc.Dial(instanceAddr, grpc.WithInsecure())
    		if err != nil {
    			fmt.Println(err)
    			panic("connect error")
    		}
    		defer conn.Close()
    		bookClient := book.NewBookServiceClient(conn)
    		bi, _ := bookClient.GetBookInfo(context.Background(), book.BookInfoParams{BookId: 1})
    		fmt.Println("获取书籍详情")
    		fmt.Println("bookId: 1", " => ", "bookName:", bi.BookName)
     
    		bl, _ := bookClient.GetBookList(context.Background(), book.BookListParams{Page: 1, Limit: 10})
    		fmt.Println("获取书籍列表")
    		for _, b := range bl.BookList {
    			fmt.Println("bookId:", b.BookId, " => ", "bookName:", b.BookName)
    		}
    		return nil, nil
    	}, nil, nil
    }

    4、运行

    (1)安装etcd并启动

    由于本实例服务发现采用了etcd,因此在运行之前需要先安装etcd并运行。

    (2)etcd是一个分布式一致性键值存储,其主要用于分布式系统的共享配置和服务发现。etcd由Go语言编写.

    下载地址: https://github.com/coreos/etcd/releases

    将压缩文件解压到指定文件夹,解压后的目录如下:

    其中etcd.exe是服务端,etcdctl.exe是客户端。点击etcd.exe运行etcd服务。(注:设置环境变量自由决定,此实例也可以不用设置)

    (2)实例运行

    先运行Server端,在运行Client端,效果如下:

    5、问题汇总

    如果运行时,提示一下错误:

    panic: /debug/requests is already registered. You may have two independent copies of golang.org/x/net/trace in your binary, trying to maintain separate state. This may involve a vendored copy of golang.org/
    x/net/trace.
     
    goroutine 1 [running]:
    go.etcd.io/etcd/vendor/golang.org/x/net/trace.init.0()
            D:/GoSrc/src/go.etcd.io/etcd/vendor/golang.org/x/net/trace/trace.go:116 +0x1ab
    exit status 2

    说明golang.org/x/net/包下的 trace 与go.etcd.io/etcd/vendor/golang.org/x/net/ 包下trace有冲突,解决方法:找到go.etcd.io\etcd\vendor目录:

    由于已经在src目录下存在golang.org 与google.golang.org两个包

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。

    您可能感兴趣的文章:
    • 基于golang中container/list包的用法说明
    • Golang中List的实现方法示例详解
    • golang中for range的取地址操作陷阱介绍
    • golang如何去除多余空白字符(含制表符)
    • 用golang如何替换某个文件中的字符串
    • golang正则之命名分组方式
    • 解决golang中container/list包中的坑
    上一篇:go如何删除字符串中的部分字符
    下一篇:基于golang中container/list包的用法说明
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    golang使用grpc+go-kit模拟oauth认证的操作 golang,使用,grpc+go-kit,模拟,