我们使用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,用于操作redisif 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()//加载redisif 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获取clientidUser := GetUserId(req.Code)mysql , err := UseMysql()if err != nil {log.Println('get secrete default')}var c Clientmysql.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 intName stringPassword stringAl stringUId string}func usemysql () (*gorm.DB , error) {return gorm.Open('mysql' , 'root:123456@/oauth?charset=utf8&parseTime=True&loc=Local')}//调用codeserver接口,进行拿取codefunc 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 Usercon.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接口,返回codecode :=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.HandlerbookInfoHandler 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的EndPointfunc 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的EndPointfunc makeGetBookInfoEndpoint() endpoint.Endpoint {return func(ctx context.Context, request interface{}) (interface{}, error) {//请求详情时返回 书籍信息req := request.(*book.BookInfoParams)b := new(book.BookInfo)b.BookId = req.BookIdb.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 = ServerInstancectx = 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创建的endPointendpointer := 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)}} //通过传入的 实例地址 创建对应的请求endPointfunc 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 +0x1abexit status 2
说明golang.org/x/net/包下的 trace 与go.etcd.io/etcd/vendor/golang.org/x/net/ 包下trace有冲突,解决方法:找到go.etcd.ioetcdvendor目录:
由于已经在src目录下存在golang.org 与google.golang.org两个包
以上为个人经验,希望能给大家一个参考,也希望大家多多支持优爱好网。如有错误或未考虑完全的地方,望不吝赐教。
相关文章: