服务器之家

服务器之家 > 正文

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

时间:2021-05-30 01:04     来源/作者:鹿灏楷silves

我们使用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

标签:

相关文章

热门资讯

2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
yue是什么意思 网络流行语yue了是什么梗
yue是什么意思 网络流行语yue了是什么梗 2020-10-11
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总 2020-11-13
2021德云社封箱演出完整版 2021年德云社封箱演出在线看
2021德云社封箱演出完整版 2021年德云社封箱演出在线看 2021-03-15
返回顶部