微服务划分
登录
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
注意:
- 会话密钥
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
- 临时登录凭证 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) 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, }) } })
|
在微信开发工具中可以看到:
使用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) },
|
获取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.go
main函数中给出
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") 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) } }
}
|
步骤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
服务测试
查看数据库
代码重构
有很多代码都是统一的格式,并且经常复用
例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"` }
|
源代码修改为: