微服务划分

image-20220227140838223

登录

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

注意:

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥
  2. 临时登录凭证 code 只能使用一次

实现登录

获取code

步骤1:proto文件以及配置文件编写

在server文件夹建auth/api/auth.proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";
package auth.v1;
option go_package="coolcar/auth/api/gen/v1;authpb";

message LoginRequest{
string code = 1;
}
message LoginResponse{
string access_token = 1;
int32 expires_int = 2;

service AuthService{
rpc Login (LoginRequest) return (LoginResponse)
}

步骤2:同级目录建立auth.yaml文件

1
2
3
4
5
6
7
8
type: google.api.Service
config_version: 3

http:
rules:
- selector: auth.v1.AuthService.Login
post: /v1/auth/login
body: "*"

步骤3:生成ts,js,go文件,运行之前要确保这些文件夹都存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
set PROTO_PATH=.\auth\api
set GO_OUT_PATH=.\auth\api\gen\v1
mkdir %GO_OUT_PATH%
protoc -I=%PROTO_PATH% --go_out=plugins=grpc,paths=source_relative:%GO_OUT_PATH% auth.proto
protoc -I=%PROTO_PATH% --grpc-gateway_out=paths=source_relative,grpc_api_configuration=%PROTO_PATH%\auth.yaml:%GO_OUT_PATH% auth.proto

set PBTS_BIN_DIR= ..\wx\miniprogram\node_modules\.bin
set PBTS_OUT_DIR= ..\wx\miniprogram\service\proto_gen\auth
mkdir %PBTS_OUT_DIR%

%PBTS_BIN_DIR%\pbjs -t static -w es6 ./auth/api/auth.proto --no--create --no--decode --no--verify --no--delimited -o %PBTS_OUT_DIR%/auth_pb.js

%PBTS_BIN_DIR%\pbts -o %PBTS_OUT_DIR%/auth_pb.d.ts %PBTS_OUT_DIR%/auth_pb.js

在生成的js文件头部需要加入:

1
import * as $protobuf from "protobufjs";

步骤4:实现AuthServiceServer接口。

在/server目录运行go get go.uber.org/zap获取日志记录,这个zapLogger可以自定义

创建auth/auth/auth.go文件,先用下面代码测试是否可以拿到code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package auth

import (
"context"
authpb "coolcar/auth/api/gen/v1"

"go.uber.org/zap"
)

type Service struct {
Logger zap.Logger
}

func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
s.Logger.Info("recived code", zap.String("code", req.Code))
return &authpb.LoginResponse{
AccessToken: "token for " + req.Code,
ExpiresInt: 7200,
}, nil
}

步骤4:创建auth/main.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
29
30
31
package main

import (
authpb "coolcar/auth/api/gen/v1"
"coolcar/auth/auth"
"log"
"net"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}
lis, err := net.Listen("tcp", ":8081")
if err != nil {
logger.Fatal("cannot listen : %v", zap.Error(err))
}
s := grpc.NewServer()
authpb.RegisterAuthServiceServer(s, &auth.Service{
Logger: *logger,
})
err2 := s.Serve(lis)
if err2 != nil {
logger.Fatal("cannot server: %v", zap.Error(err2))
}

}

步骤5:创建网关向外界暴露api,创建/server/gateway/main.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
29
30
31
32
33
package main

import (
"context"
authpb "coolcar/auth/api/gen/v1"
"log"
"net/http"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/protobuf/encoding/protojson"
)

func main() {
c := context.Background()
c, cancel := context.WithCancel(c)
defer cancel()

mux := runtime.NewServeMux(runtime.WithMarshalerOption(
runtime.MIMEWildcard, &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseEnumNumbers: true,
UseProtoNames: true,
}},
))
err := authpb.RegisterAuthServiceHandlerFromEndpoint(
c, mux, "localhost:8081", []grpc.DialOption{grpc.WithInsecure()})
if err != nil {
log.Fatalf("cannot register with service: %v", err)
}
log.Fatal(http.ListenAndServe(":8080", mux))

}

步骤6:在小程序端的app.ts中实现登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wx.login({
success: res => {
console.log(res.code)
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.request({
url: "http://localhost:8080/v1/auth/login",
method: "POST",
data: {
code: res.code,
}as auth.v1.ILoginRequest,
success:console.log,
fail: console.log,
})
}
})

在微信开发工具中可以看到:

image-20220301100850103

使用camelcaseKeys来驼峰命名:

1
2
3
4
5
success: res => {
const loginResp : auth.v1.ILoginResponse =
auth.v1.LoginResponse.fromObject(camelcaseKeys(res.data as object))
console.log(loginResp)
},

image-20220301101454427

获取opendId

发请求

1
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

使用下面的库来发请求会方便很多:

1
go get github.com/medivhzhan/weapp/v2

步骤1:在server/wechat/wechat.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
package wechat

import (
"fmt"

"github.com/medivhzhan/weapp/v2"
)

type Service struct {
AppID string
AppSecret string
}

func (s *Service) Resolver(code string) (string, error) {
resp, err := weapp.Login(s.AppID, s.AppSecret, code)
if err != nil {
return "", fmt.Errorf("weapp.Login:%v", err)
}
if err := resp.GetResponseError(); err != nil {
return "", fmt.Errorf("weapp response error: %v", err)
}
return resp.OpenID, nil

}

AppID,AppSecret都在main函数中给出,目前的所有配置都在auth/main.gomain函数中给出

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
package main

import (
authpb "coolcar/auth/api/gen/v1"
"coolcar/auth/auth"
"coolcar/auth/wechat"
"log"
"net"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}
lis, err := net.Listen("tcp", ":8081")
if err != nil {
logger.Fatal("cannot listen : %v", zap.Error(err))
}
s := grpc.NewServer()
authpb.RegisterAuthServiceServer(s, &auth.Service{
OpenIDResolver: &wechat.Service{
AppID: "去微信公众平台获取",
AppSecret: "去微信公众平台获取",
},
Logger: *logger,
})
err2 := s.Serve(lis)
if err2 != nil {
logger.Fatal("cannot server: %v", zap.Error(err2))
}
}

使用MongoDB来将openID和用户绑定

步骤1:拉取mongo的go语言客户端

1
go get go.mongodb.org/mongo-driver/mongo

步骤2:(可跳过)实验一下/server/cmd/mongo/main.go,go语言操作mongodb

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
func main() {
uri := "mongodb://localhost:27017/coolcar?readPreference=primary&ssl=false"
c := context.Background()
mc, err := mongo.Connect(c, options.Client().ApplyURI(uri))
if err != nil {
panic(err)
}
col := mc.Database("coolcar").Collection("account")
//insertRows(c, col)
//findRows(c, col)
findMany(c, col)
}
func findMany(c context.Context, col *mongo.Collection) {
cur, err := col.Find(c, bson.M{})
if err != nil {
panic(err)
}
for cur.Next(c) {
var row struct {
ID primitive.ObjectID `bson:"_id"`
OpenID string `bson:"open_id"`
}
err := cur.Decode(&row)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", row)
}
}
func findRows(c context.Context, col *mongo.Collection) {
res := col.FindOne(c, bson.M{
"open_id": "123",
})
fmt.Printf("%+v\n", res)
var row struct {
ID primitive.ObjectID `bson:"_id"`
OpenID string `bson:"open_id"`
}
err := res.Decode(&row)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", row)
}
func insertRows(c context.Context, col *mongo.Collection) {
res, err := col.InsertMany(c, []interface{}{
bson.M{
"open_id": "123",
},
bson.M{
"open_id": "456",
},
})
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", res)
}

步骤3:建立/auth/dao/mongo.go,要清楚责任划分,这里面的定义的对表的操作,数据库的指定一个是由/auth/main.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
29
30
31
32
33
34

type Mongo struct {
col *mongo.Collection
}

func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("account"),
}
}

func (m *Mongo) ResolverAccountID(c context.Context, openID string) (string, error) {
res := m.col.FindOneAndUpdate(c, bson.M{
"open_id": openID,
}, bson.M{
"$set": bson.M{
"open_id": openID,
},
}, options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))

if err := res.Err(); err != nil {
return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
}
var row struct {
ID primitive.ObjectID `bson:"_id"`
}

err := res.Decode(&row)
if err != nil {
return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
}
return row.ID.Hex(), nil
}

步骤4:测试同目录创建/mongo_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestResolverAccountID(t *testing.T) {
c := context.Background()
s := "mongodb://localhost:27017/coolcar?readPreference=primary&ssl=false"
mc, err := mongo.Connect(c, options.Client().ApplyURI(s))
if err!=nil{
t.Fatalf("cannot connection db: %v",err)
}
m := NewMongo(mc.Database("coolcar"))
id, err := m.ResolverAccountID(c, "123")
if err != nil{
t.Errorf("faild resolved account id 123: %v",err)
}else{
want := "621ecd8d42ffa4ec7cf1a22c"
if id != want{
t.Errorf("resolve account id : want: %q, got: %q",want, id)
}
}

}

image-20220302104759460

步骤5:修改auth.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
s.Logger.Info("recived code", zap.String("code", req.Code))
openId, err := s.OpenIDResolver.Resolver(req.Code)
if err != nil {
//这个错误可能包含了内部服务的信息
return nil, status.Errorf(codes.Unavailable, "cannot resolve opendId : %v", err)
}

accountID, err := s.Mongo.ResolverAccountID(c, openId)
if err != nil {
s.Logger.Error("cannot resolve account id : %v", zap.Error(err))
return nil, status.Errorf(codes.Internal, "")
}

return &authpb.LoginResponse{
AccessToken: "token for account id :" + accountID,
ExpiresInt: 7200,
}, nil
}

步骤6:修改/auth/main.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
29
30
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}
lis, err := net.Listen("tcp", ":8081")
if err != nil {
logger.Fatal("cannot listen : %v", zap.Error(err))
}
c := context.Background()
mgURI := "mongodb://localhost:27017/coolcar?readPreference=primary&ssl=false"
mongoClient, err := mongo.Connect(c, options.Client().ApplyURI(mgURI))
if err != nil {
logger.Fatal("cannot connect mongodb: %v", zap.Error(err))
}

s := grpc.NewServer()
authpb.RegisterAuthServiceServer(s, &auth.Service{
OpenIDResolver: &wechat.Service{
AppID: "wx0ebc7b6b12ef1585",
AppSecret: "534a58b2185cdbc3dba6168500b7ad3d",
},
Mongo: dao.NewMongo(mongoClient.Database("coolcar")),
Logger: *logger,
})
err2 := s.Serve(lis)
if err2 != nil {
logger.Fatal("cannot server: %v", zap.Error(err2))
}
}

步骤7:重启网关和/auth/main.go服务测试

image-20220302111316555

查看数据库

image-20220302111341410

代码重构

有很多代码都是统一的格式,并且经常复用

例1:mongo.go里面的set

1
2
3
4
5
bson.M{
"$set": bson.M{
"open_id": openID,
},
}

创建/server/shared/mongo/mongo.go

1
2
3
4
5
6
func Set(v interface{}) bson.M {
return bson.M{
"$set": v,
}
}

源代码修改为:

1
2
3
mgo.Set(bson.M{
"open_id": openID,
})

例2:

1
2
3
var row struct {
ID primitive.ObjectID `bson:"_id"`
}

/server/shared/mongo/mongo.go

1
2
3
type ObjID struct {
ID primitive.ObjectID `bson:"_id"`
}

源代码修改为:

1
var row mgo.ObjID