我們使用grpc對外的接口,進行服務(wù),模擬對外認證的接口
首先我們要了解oauth的基本認證過程
第三方的服務(wù)端,在oauth2.0中作為一個客戶端的身份,進行請求數(shù)據(jù)。
用戶進行選擇第三方的登陸,比如選擇到某一個第三方的平臺進行登陸,則會跳轉(zhuǎn)到第三方登陸平臺
用戶輸入用戶名密碼,在第三方平臺進行登陸,,如果登陸成功,則返回code。
客戶端,也就是我們想要登陸的網(wǎng)站,將會讀取code,并且將會攜帶這個code,和第三方網(wǎng)站所頒發(fā)的密碼,進行請求token,如果code和注冊時所得到的密碼,都驗證成功,此時,第三方客戶端會返回一個token。
我們登陸的網(wǎng)站會攜帶這個token去請求用戶身份資源的服務(wù)器,如果token比對成功,則返回用戶的信息所以我們需要一些服務(wù)
codeserver,作用,分發(fā)code,驗證code的準確性
tokenserver,作用分發(fā)token,驗證token的準確性
loginserver,作用,登陸成功后,調(diào)用codeserver得到code
userdetailserver,作用調(diào)用tokenserver的token驗證,驗證token是否合法,如果合法,進行返回用戶的基本信息繼續(xù),我們大概看一下這些功能具體怎樣實現(xiàn)。
實現(xiàn)
codeserver
type Codeserver struc (
GetCode ()
ValidCode ()
)
//函數(shù)的具體傳參今不寫了
其實我們的code和token,主要是使用redis數(shù)據(jù)庫進行實現(xiàn),并且給申請的code和token設(shè)置過期時間, 也就是說,在數(shù)據(jù)庫中實現(xiàn)一個定時的作用,如果,申請完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)//插入數(shù)據(jù)庫,用于獲取token時進行驗證
con.Do("set" , randstr , req.UserId , "EX" , 120)
con.Do("EXPIRE" , req.UserId , 20)//設(shè)置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層等等,就先不寫了,我們就這篇文章主要是看怎樣模擬實現(xiàn)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數(shù)據(jù)庫中進行查找,請求所攜帶的密碼,是否與第三方注冊時給的密碼是否相同,如果不相同,則不返回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進行插入數(shù)據(jù)庫,并設(shè)置過期時間,如果避免token被多次利用
con.Do("set" , str , User , "EX" , 120)
//設(shè)置userid和token的對應(yīng)關(guān)系,避免沒有對應(yīng)上,客戶端拿到token之后隨便拿取其他人的用戶濾數(shù)據(jù)
if err != nil {
return nil , err
}
return tokenservice.RepGetToken{Toen: str} , nil
}
//判斷token是都合法,給userdetailserver用,當服務(wù)器接到token后,需要調(diào)用這個接口,查看token是否合法,如果合法返回用戶數(shù)據(jù)
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")
}
//調(diào)用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)
//在數(shù)據(jù)庫中進行查找,如果沒找到該用戶,說明該用戶不存在,或者用戶輸入錯誤
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")
}
//如果認證成功,則進行調(diào)用codeserver接口,返回code
code :=getcode(req.Id)
if code == "" {
return userauth.AuthResponse{IsTrue: false} , nil
}
return userauth.AuthResponse{Code: code , IsTrue: true} , nil
}
基本原理就是這樣,但是我們還是差一個userdetail的服務(wù)端
這個服務(wù)端,主要作用就是拿到請求的token,并進行檢驗,如果檢驗成功,返回用戶數(shù)據(jù),至于怎樣檢驗,就是調(diào)用tokenserver中的檢驗接口。
這里就不寫了,留給讀者完成。
我寫的這三個接口在gitee上有源碼,是基于golang寫的,使用的框架有g(shù)rpc,go-kit的服務(wù)框架。
具體地址gitee.com/silves-xiang
補充:go-kit實踐之2:go-kit 實現(xiàn)注冊發(fā)現(xiàn)與負載均衡
一、介紹
grpc提供了簡單的負載均衡,需要自己實現(xiàn)服務(wù)發(fā)現(xiàn)resolve。我們既然要使用go-kit來治理微服務(wù),那么我們就使用go-kit的注冊發(fā)現(xiàn)、負載均衡機制。
go-kit官方【stringsvc3】例子中使用的負載均衡方案是通過服務(wù)端轉(zhuǎn)發(fā)進行,翻找下源碼go-kit的服務(wù)注冊發(fā)現(xiàn)、負載均衡在【sd】包中。下面我們介紹怎么通過go-kit進行客戶端負載均衡。
go-kit提供的注冊中心
1、 etcd
2、 consul
3、 eureka
4、 zookeeper
go-kit提供的負載均衡
1、 random[隨機]
2、 roundRobin[輪詢]
只需實現(xiàn)Balancer接口,我們可以很容易的增加其它負載均衡機制
type Balancer interface {
Endpoint() (endpoint.Endpoint, error)
}
etcd注冊發(fā)現(xiàn)
etcd和zookeeper類似是一個高可用、強一致性的存儲倉庫,擁有服務(wù)發(fā)現(xiàn)功能。 我們就通過go-kit提供的etcd包來實現(xiàn)服務(wù)注冊發(fā)現(xiàn)
二、示例
1、protobuf文件及生成對應(yīng)的go文件
syntax = "proto3";
// 請求書詳情的參數(shù)結(jié)構(gòu) book_id 32位整形
message BookInfoParams {
int32 book_id = 1;
}
// 書詳情信息的結(jié)構(gòu) book_name字符串類型
message BookInfo {
int32 book_id = 1;
string book_name = 2;
}
// 請求書列表的參數(shù)結(jié)構(gòu) page、limit 32位整形
message BookListParams {
int32 page = 1;
int32 limit = 2;
}
// 書列表的結(jié)構(gòu) BookInfo結(jié)構(gòu)數(shù)組
message BookList {
repeated BookInfo book_list = 1;
}
// 定義 獲取書詳情 和 書列表服務(wù) 入?yún)⒊鰠⒎謩e為上面所定義的結(jié)構(gòu)
service BookService {
rpc GetBookInfo (BookInfoParams) returns (BookInfo) {}
rpc GetBookList (BookListParams) returns (BookList) {}
}
生成對應(yīng)的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
}
//一下兩個方法實現(xiàn)了 protoc生成go文件對應(yīng)的接口:
/*
// BookServiceServer is the server API for BookService service.
type BookServiceServer interface {
GetBookInfo(context.Context, *BookInfoParams) (*BookInfo, error)
GetBookList(context.Context, *BookListParams) (*BookList, error)
}
*/
//通過grpc調(diào)用GetBookInfo時,GetBookInfo只做數(shù)據(jù)透傳, 調(diào)用BookServer中對應(yīng)Handler.ServeGRPC轉(zhuǎn)交給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 //直接返回斷言的結(jié)果
}
//通過grpc調(diào)用GetBookList時,GetBookList只做數(shù)據(jù)透傳, 調(diào)用BookServer中對應(yīng)Handler.ServeGRPC轉(zhuǎn)交給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
}
//創(chuàng)建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:"微服務(wù)入門到精通"})
b.BookList=append(b.BookList,book.BookInfo{BookId:2,BookName:"區(qū)塊鏈入門到精通"})
return b,nil
}
}
//創(chuàng)建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服務(wù)的IP地址
prefix = "/services/book/" //服務(wù)的目錄
ServerInstance = "127.0.0.1:50052" //當前實例Server的地址
key = prefix + ServerInstance //服務(wù)實例注冊的路徑
value = ServerInstance
ctx = context.Background()
//服務(wù)監(jiān)聽地址
serviceAddress = ":50052"
)
//etcd連接參數(shù)
option := etcdv3.ClientOptions{DialTimeout: time.Second * 3, DialKeepAlive: time.Second * 3}
//創(chuàng)建連接
client, err := etcdv3.NewClient(ctx, []string{etcdServer}, option)
if err != nil {
panic(err)
}
//創(chuàng)建注冊
registrar := etcdv3.NewRegistrar(client, etcdv3.Service{Key: key, Value: value}, log.NewNopLogger())
registrar.Register() //啟動注冊服務(wù)
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) //網(wǎng)絡(luò)監(jiān)聽,注意對應(yīng)的包為:"net"
if err != nil {
fmt.Println(err)
return
}
gs := grpc.NewServer(grpc.UnaryInterceptor(grpc_transport.Interceptor))
book.RegisterBookServiceServer(gs, bookServer) //調(diào)用protoc生成的代碼對應(yīng)的注冊方法
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"
//監(jiān)聽的服務(wù)前綴
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()
//創(chuàng)建實例管理器, 此管理器會Watch監(jiān)聽etc中prefix的目錄變化更新緩存的服務(wù)實例數(shù)據(jù)
instancer, err := etcdv3.NewInstancer(client, prefix, logger)
if err != nil {
panic(err)
}
//創(chuàng)建端點管理器, 此管理器根據(jù)Factory和監(jiān)聽的到實例創(chuàng)建endPoint并訂閱instancer的變化動態(tài)更新Factory創(chuàng)建的endPoint
endpointer := sd.NewEndpointer(instancer, reqFactory, logger) //reqFactory自定義的函數(shù),主要用于端點層(endpoint)接受并顯示數(shù)據(jù)
//創(chuàng)建負載均衡器
balancer := lb.NewRoundRobin(endpointer)
/**
我們可以通過負載均衡器直接獲取請求的endPoint,發(fā)起請求
reqEndPoint,_ := balancer.Endpoint()
*/
/**
也可以通過retry定義嘗試次數(shù)進行請求
*/
reqEndPoint := lb.Retry(3, 3*time.Second, balancer)
//現(xiàn)在我們可以通過 endPoint 發(fā)起請求了
req := struct{}{}
if _, err = reqEndPoint(ctx, req); err != nil {
panic(err)
}
}
//通過傳入的 實例地址 創(chuàng)建對應(yīng)的請求endPoint
func reqFactory(instanceAddr string) (endpoint.Endpoint, io.Closer, error) {
return func(ctx context.Context, request interface{}) (interface{}, error) {
fmt.Println("請求服務(wù): ", 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并啟動
由于本實例服務(wù)發(fā)現(xiàn)采用了etcd,因此在運行之前需要先安裝etcd并運行。
(2)etcd是一個分布式一致性鍵值存儲,其主要用于分布式系統(tǒng)的共享配置和服務(wù)發(fā)現(xiàn)。etcd由Go語言編寫.
下載地址: https://github.com/coreos/etcd/releases
將壓縮文件解壓到指定文件夾,解壓后的目錄如下:
其中etcd.exe是服務(wù)端,etcdctl.exe是客戶端。點擊etcd.exe運行etcd服務(wù)。(注:設(shè)置環(huán)境變量自由決定,此實例也可以不用設(shè)置)
(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目錄:
由于已經(jīng)在src目錄下存在golang.org 與google.golang.org兩個包
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
您可能感興趣的文章:- 基于golang中container/list包的用法說明
- Golang中List的實現(xiàn)方法示例詳解
- golang中for range的取地址操作陷阱介紹
- golang如何去除多余空白字符(含制表符)
- 用golang如何替換某個文件中的字符串
- golang正則之命名分組方式
- 解決golang中container/list包中的坑