脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - golang使用grpc+go-kit模拟oauth认证的操作

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

2021-05-30 01:04鹿灏楷silves Golang

这篇文章主要介绍了golang使用grpc+go-kit模拟oauth认证的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

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

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

 

golang使用grpc+go-kit模拟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

?
1
2
3
4
5
type Codeserver struc (
    GetCode ()
    ValidCode ()
)
//函数的具体传参今不写了

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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=utf8&parseTime=True&loc=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接口,我们可以很容易的增加其它负载均衡机制

?
1
2
3
type Balancer interface { 
   Endpoint() (endpoint.Endpoint, error) 
}

etcd注册发现

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

二、示例

 

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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端代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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端代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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

将压缩文件解压到指定文件夹,解压后的目录如下: golang使用grpc+go-kit模拟oauth认证的操作

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

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

(2)实例运行

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

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

5、问题汇总

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

?
1
2
3
4
5
6
7
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目录:

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

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

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

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

原文链接:https://blog.csdn.net/Xiang_lhh/article/details/115578538

延伸 · 阅读

精彩推荐
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14