我們用一個系列來講解從需求到上線、從代碼到k8s部署、從日志到監控等各個方面的微服務完整實踐。
整個項目使用了go-zero開發的微服務,基本包含了go-zero以及相關go-zero作者開發的一些中間件,所用到的技術棧基本是go-zero項目組的自研組件,基本是go-zero全家桶了。
實戰項目地址:https://github.com/Mikaelemmmm/go-zero-looklook
一、用戶中心業務架構圖
二、依賴關系
usercenter-api(用戶中心api) 依賴 identity-rpc(授權認證rpc)、usercenter-rpc(用戶中心rpc)
usercenter-rpc(用戶中心rpc)依賴 identity-rpc(授權中心rpc)
我們看項目usercenter/cmd/api/desc/usercenter.api ,所有的用戶api對外的http方法都在這里面
這里面有4個業務注冊、登陸、獲取用戶信息、微信小程序授權
三、注冊舉例
1、注冊api服務
我們在寫api服務代碼的時候是先要在usercenter.api中定義好service中的方法,然后在desc/user中寫request、response,這樣拆分開的好處是不那么臃腫
a、在usercenter.api中定義注冊方法如下
// 用戶模塊v1版本的接口
@server(
prefix: usercenter/v1
group: user
)
service usercenter {
@doc "注冊"
@handler register
post /user/register (RegisterReq) returns (RegisterResp)
.....
}
b、在app/usercenter/cmd/api/desc/user/user.api中定義RegisterReq\RegisterResp
type (
RegisterReq {
Mobile string `json:"mobile"`
Password string `json:"password"`
}
RegisterResp {
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
)
c、goctl生成api代碼
1)命令行進入app/usercenter/cmd/api/desc目錄下。
2)去項目目錄下deploy/script/gencode/gen.sh中,復制如下一條命令,在命令行中執行(命令行要切換到app/usercenter/cmd目錄)
$ goctl api go -api *.api -dir ../ -style=goZero
d、打開app/usercenter/cmd/api/internal/logic/user/register.go文件
這里就很容易了,直接調用user的rpc服務即可
這里有個小技巧,很多同學感覺rpc服務返回的字段跟api定義差不多,每次都要手動去復制很麻煩,那么go有沒有像java一樣的BeanCopyUtils.copy 這種工具呢?答案肯定是有的,可以看上面的代碼copier.Copy ,這個庫是gorm作者的另一款新作,是不是很興奮。 那我們繼續看看調用后端的rpc是什么樣子的。
2、注冊rpc服務
-
定義protobuf文件
我們在app/usercenter/cmd/rpc/pb中新建usercenter.proto,寫入注冊方法
//req 、resp message RegisterReq { string mobile = 1; string nickname = 2; string password = 3; string authKey = 4; string authType = 5; } message RegisterResp { string accessToken = 1; int64 accessExpire = 2; int64 refreshAfter = 3; } //service service usercenter { rpc register(RegisterReq) returns(RegisterResp); ...... }
-
使用goctl生成代碼,這里不需要自己手動敲
1)命令行進入app/usercenter/cmd/rpc/pb目錄下。
2)去項目目錄下deploy/script/gencode/gen.sh中,復制如下兩條命令,在命令行中執行(命令行要切換到app/usercenter/cmd目錄)
$ goctl rpc protoc *.proto --go_out=../ --go-grpc_out=../ --zrpc_out=../ $ sed -i "" 's/,omitempty//g' *.pb.go
-
打開app/usercenter/cmd/rpc/internal/logic/registerLogic.go寫邏輯代碼
注冊設計到2張表,一個user表,一個user_auth表,user是存儲用戶基本信息的,user_auth是可以根據不同平台授權登陸的相關信息,所以這里設計到本地事務,由於go-zero的事務要在model中才能使用,但是我在model中做了個處理,把它在model中暴露出來,就可以在logic中使用
model中定義了Trans方法暴露事務給logic
在logic中直接使用
由於項目支持小程序、手機號,小程序注冊不需要密碼,所以在處理密碼時候做了個處理,手機號注冊就要傳遞密碼,小程序注冊就不需要傳遞密碼,至於手機號注冊密碼不能為空要在手機號注冊時候的api服務自己判斷
在usercenter-rpc注冊成功之后,需要請求token給前端登陸,直接請求identity-rpc頒發該用戶的token
identity-rpc中如下
message GenerateTokenReq { int64 userId = 1; } message GenerateTokenResp { string accessToken = 1; int64 accessExpire = 2; int64 refreshAfter = 3; } service identity{ //生成token,只針對用戶服務開放訪問 rpc generateToken(GenerateTokenReq) returns(GenerateTokenResp); ..... }
generatetokenlogic.go
// GenerateToken 生成token,只針對用戶服務開放訪問. func (l *GenerateTokenLogic) GenerateToken(in *pb.GenerateTokenReq) (*pb.GenerateTokenResp, error) { now := time.Now().Unix() accessExpire := l.svcCtx.Config.JwtAuth.AccessExpire accessToken, err := l.getJwtToken(l.svcCtx.Config.JwtAuth.AccessSecret, now, accessExpire, in.UserId) if err != nil { return nil, errors.Wrapf(ErrGenerateTokenError, "getJwtToken err userId:%d , err:%v", in.UserId, err) } //存入redis userTokenKey := fmt.Sprintf(globalkey.CacheUserTokenKey, in.UserId) err = l.svcCtx.RedisClient.Setex(userTokenKey, accessToken, int(accessExpire)) if err != nil { return nil, errors.Wrapf(ErrGenerateTokenError, "SetnxEx err userId:%d, err:%v", in.UserId, err) } return &pb.GenerateTokenResp{ AccessToken: accessToken, AccessExpire: now + accessExpire, RefreshAfter: now + accessExpire/2, }, nil }
注冊成功並去identity-rpc拿到token、token過期時間、置換token的時間給api服務
四、業務獲取登陸用戶id
當我們在獲取用戶信息,或者下單等場景下總要獲取登陸用戶的id,前一篇我們講到,我們在授權identity服務中校驗完token,解析出來的userId會放到header中返回給nginx的authReuest
在文件app/identity/cmd/api/internal/handler/verify/tokenHandler.go
nginx通過authRequest然后訪問后端的服務時候,會把header內容傳遞給后端服務,因為我們在nginx中配置了如下
那這樣的話,我們在后端服務就可以拿到這個userId了,比如我們現在訪問usercenter/v1/user/detail獲取當前登陸用戶信息
ok,可以看到我們通過 ctxdata.GetUidFromCtx(l.ctx)就可以拿到,為什么這么神奇呢?我們點開看看這個方法
實際上就是從ctx中拿到的userId,是不是很奇怪,我們明明在nignx就放在了header中,你在go的業務代碼中為什么能通過ctx拿到?
1、【小技巧】middleware
當nginx在header中攜帶了x-user就是userId來訪問后端服務的時候,我們后端服務在啟動時main函數會加載一個全局中間件,比如usercenter-api中的main
app/usercenter/cmd/api/usercenter.go
這里定義了全局中間件,只要有請求到我們usercenter-ap某個方法之前,都會先進入全局中間件中,中間件具體內容如下
所以是不是一下就明白了,在請求我們usercenter/v1/user/detail時候,會先進入這個中間件,在這個中間件內,我們通過nginx的header中的X-User拿到解析后的userId放到ctx中,那繼續進入到usercenter/v1/user/detail時候,我們是不是就可以通過ctx直接取出來在業務中用啦,一切真相大白。
同樣其他用戶中心服務登陸、獲取登陸用戶信息、小程序授權登陸都是一個道理,這里就不再啰嗦了,自行看代碼即可
【注】小程序授權登陸,記得修改配置文件,這里的配置文件是假的,改成自己的
項目地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支持我們!
微信交流群
關注『微服務實踐』公眾號並點擊 交流群 獲取社區群二維碼。