实现JWT

步骤1:创建接口

auth/auth/auth.go定义token接口

1
2
3
type TokenGenerator interface {
GeneratorToken(accountID string, expire time.Duration) (string, error)
}

在Service中加入TokenGenerator

1
2
3
4
5
6
7
type Service struct {
OpenIDResolver OpenIDResolver
Mongo *dao.Mongo
TokenGenerator TokenGenerator
TokenExpire time.Duration
Logger zap.Logger
}

并在Login函数中使用

1
tkn, err := s.TokenGenerator.GeneratorToken(accountID,7200)

步骤2:实现接口

安装:

1
go get github.com/dgrijalva/jwt-go

创建/server/token/jwt.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
type JWTTokenGen struct {
PrivateKey *rsa.PrivateKey
Issuer string
// 希望时间可以又外面控制,这样测试的时候不会因为时间导致不一样
nowFun func() time.Time
}

// 构造函数

func NewJWTTokenGen(issue string, privatekey *rsa.PrivateKey) *JWTTokenGen {
return &JWTTokenGen{
Issuer: issue,
nowFun: time.Now,
PrivateKey: privatekey,
}
}

func (t *JWTTokenGen) GeneratorToken(accountID string, expire time.Duration) (string, error) {
nowSec := t.nowFun().Unix()
tkn := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.StandardClaims{
Issuer: t.Issuer,
IssuedAt: nowSec,
ExpiresAt: nowSec + int64(expire.Seconds()),
Subject: accountID,
})
//签名
return tkn.SignedString(t.PrivateKey)
}

步骤3:使用

将生产的私钥和公钥保存到文件里面

image-20220303184622251

1
2
3
4
5
6
7
8
9
10
11
12
13
// 读取私钥
f, err := os.Open("auth/private.key")
if err != nil {
logger.Fatal("cannot open private.key:%v", zap.Error(err))
}
b, err3 := ioutil.ReadAll(f)
if err3 != nil {
logger.Fatal("cannot read private.key:%v", zap.Error(err3))
}
pk, err4 := jwt.ParseRSAPrivateKeyFromPEM(b)
if err4 != nil {
logger.Fatal("cannot Parse private.key:%v", zap.Error(err4))
}
1
2
3
4
5
6
7
8
9
10
authpb.RegisterAuthServiceServer(s, &auth.Service{
OpenIDResolver: &wechat.Service{
AppID: "wx0ebc7b6b12ef1585",
AppSecret: "534a58b2185cdbc3dba6168500b7ad3d",
},
Mongo: dao.NewMongo(mongoClient.Database("coolcar")),
Logger: *logger,
TokenExpire: 2 * time.Hour,
TokenGenerator: token.NewJWTTokenGen("coolcar/auth", pk),
})

验证Token

每个微服务都需要去验证token

步骤1:创建/shared/auth/tokentoken.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
type JWTTokenVerifier struct {
PublicKey *rsa.PublicKey
}

func (v *JWTTokenVerifier) Verify(token string) (string, error) {
//1.解析token,结构得对
t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{},
//验证签名
func(*jwt.Token) (interface{}, error) {
return v.PublicKey, nil
})
if err != nil {
return "", fmt.Errorf("cannot parse token: %v", err)
}
// 验证token是由有效
if !t.Valid {
return "", fmt.Errorf("token not valid")
}

clm, ok := t.Claims.(*jwt.StandardClaims)
//保证是StandardClaim
if !ok {
return "", fmt.Errorf("token is not StandardClaim")
}
//检查过期时间等等
if err := clm.Valid(); err != nil {
return "", fmt.Errorf("claim not vaild:%v", err)
}
return clm.Subject, nil
}

步骤2:表格测试

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const PublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----`

func TestVerify(t *testing.T) {

pk, err := jwt.ParseRSAPublicKeyFromPEM([]byte(PublicKey))
if err != nil {
t.Fatalf("cannot pares publickey:%v", err)
}
v := JWTTokenVerifier{
PublicKey: pk,
}

cases := []struct {
name string
tkn string
want string
wantErr bool
now time.Time
}{
{
name: "valid_token",
tkn: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiY29vbGNhci9hdXRoIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GcHiRgOuFiiQJAMKJemV2j5Vr8uZslvOJksONETcsXxTpDqEJPHiwLsc94W3cvVpYrJO6O6c8mywxYjOWkk7iBoyEWMmbapsE8T3dDyFRq2xnV-1DZerlTNVuO4gT2fq3eNOEE-XXu0y0zlnCW7LMnOZdstHAkMD-ZQP0vKZuLJjP_AMhfd3BcsVXTMLVKjW0aG-UwkAhsathBa24NaLy2AsCIljSGNjmQ4gp9CihlHDRyUCRxBPuKDf0ym-tBSUgWk9zFugKlx-nSCYLSXgMPJ0CzgSDvmoXkC3HNM1VWOo-qd-QtInMYYuQs_RSPK8VVDj7EV7llHcjbki-OtRPA",
want: "1234567890",
wantErr: false,
now: time.Unix(1516239022, 0),
},
{
name: "expired_token",
tkn: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiY29vbGNhci9hdXRoIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GcHiRgOuFiiQJAMKJemV2j5Vr8uZslvOJksONETcsXxTpDqEJPHiwLsc94W3cvVpYrJO6O6c8mywxYjOWkk7iBoyEWMmbapsE8T3dDyFRq2xnV-1DZerlTNVuO4gT2fq3eNOEE-XXu0y0zlnCW7LMnOZdstHAkMD-ZQP0vKZuLJjP_AMhfd3BcsVXTMLVKjW0aG-UwkAhsathBa24NaLy2AsCIljSGNjmQ4gp9CihlHDRyUCRxBPuKDf0ym-tBSUgWk9zFugKlx-nSCYLSXgMPJ0CzgSDvmoXkC3HNM1VWOo-qd-QtInMYYuQs_RSPK8VVDj7EV7llHcjbki-OtRPA",
wantErr: true,
now: time.Unix(1517239022, 0),
},
{
name: "bad_token",
tkn: "eyJhbGciOiJ123213234XVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiY29vbGNhci9hdXRoIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GcHiRgOuFiiQJAMKJemV2j5Vr8uZslvOJksONETcsXxTpDqEJPHiwLsc94W3cvVpYrJO6O6c8mywxYjOWkk7iBoyEWMmbapsE8T3dDyFRq2xnV-1DZerlTNVuO4gT2fq3eNOEE-XXu0y0zlnCW7LMnOZdstHAkMD-ZQP0vKZuLJjP_AMhfd3BcsVXTMLVKjW0aG-UwkAhsathBa24NaLy2AsCIljSGNjmQ4gp9CihlHDRyUCRxBPuKDf0ym-tBSUgWk9zFugKlx-nSCYLSXgMPJ0CzgSDvmoXkC3HNM1VWOo-qd-QtInMYYuQs_RSPK8VVDj7EV7llHcjbki-OtRPA",
want: "1234567890",
wantErr: true,
now: time.Unix(1517239022, 0),
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
jwt.TimeFunc = func() time.Time {
return c.now
}
accountID, err := v.Verify(c.tkn)
if !c.wantErr && err != nil {
if err != nil {
t.Errorf("verification faild: %v", err)
}
}
if c.wantErr && err == nil {
if err != nil {
t.Errorf("want error but no error")
}
}
if accountID != c.want {
t.Errorf("wrong accoid.want id:%q\ngotid:%q", c.tkn, accountID)
}
})
}
tkn := "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiY29vbGNhci9hdXRoIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.GcHiRgOuFiiQJAMKJemV2j5Vr8uZslvOJksONETcsXxTpDqEJPHiwLsc94W3cvVpYrJO6O6c8mywxYjOWkk7iBoyEWMmbapsE8T3dDyFRq2xnV-1DZerlTNVuO4gT2fq3eNOEE-XXu0y0zlnCW7LMnOZdstHAkMD-ZQP0vKZuLJjP_AMhfd3BcsVXTMLVKjW0aG-UwkAhsathBa24NaLy2AsCIljSGNjmQ4gp9CihlHDRyUCRxBPuKDf0ym-tBSUgWk9zFugKlx-nSCYLSXgMPJ0CzgSDvmoXkC3HNM1VWOo-qd-QtInMYYuQs_RSPK8VVDj7EV7llHcjbki-OtRPA"
jwt.TimeFunc = func() time.Time {
return time.Unix(1516239022, 0)
}
accountID, err := v.Verify(tkn)
if err != nil {
t.Errorf("verification faild: %v", err)
}
want := "1234567890"
if accountID != want {
t.Errorf("wrong accoid.want id:%q\ngotid:%q", want, accountID)
}

}

Context

划分任务的边界

  • Deadline:多久做完
  • Cancel:取消
  • Key—Value:传递值
  • 任务生命周期:
    • 同一个任务的步骤:共享同一个context
    • 子任务:从但前的context造一个新的context,继承原有的context的所有参数。
    • 后台任务:全新任务,不受当前context限制

登录拦截器

创建shared/auth/auth.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 将拦截器的代码封装起来
func Interceptor(publicKeyFile string) (grpc.UnaryServerInterceptor, error) {
f, err := os.Open(publicKeyFile)
if err != nil {
return nil, fmt.Errorf("cannot open public key file: %v", err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("cannot read public key file: %v", err)
}
pk, err := jwt.ParseRSAPublicKeyFromPEM(b)
if err != nil {
return nil, fmt.Errorf("cannot parse public key : %v", err)
}

i := &interceptor{
verifier: &token.JWTTokenVerifier{
PublicKey: pk,
},
}
return i.HandReq, nil
}

type tokenVerifier interface {
Verify(token string) (string, error)
}
type interceptor struct {
verifier tokenVerifier
}
// 拦截器的处理逻辑
func (i *interceptor) HandReq(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
tkn, err := tokenFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "")
}
aid, err := i.verifier.Verify(tkn)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "token not valid: %v", err)
}
return handler(ContestWithAccontId(ctx, AccountID(aid)), req)
}
// 获取context的token
func tokenFromContext(c context.Context) (string, error) {
m, ok := metadata.FromIncomingContext(c)
if !ok {
return "", status.Error(codes.Unauthenticated, "")
}
tkn := ""
for _, v := range m["authorization"] {
if strings.HasPrefix(v, "Bearer ") {
tkn = v[len("Bearer "):]
}
}
if tkn == "" {
return "", status.Error(codes.Unauthenticated, "")
}
return tkn, nil
}

type accountIDKey struct{}

//应用Identifier Type
type AccountID string

func (a AccountID) String() string {
return string(a)
}
// 向context注入aid
func ContestWithAccontId(c context.Context, aid AccountID) context.Context {
return context.WithValue(c, accountIDKey{}, aid)
}
// 提供外面使用函数
func AccountIDFromContext(c context.Context) (AccountID, error) {
v := c.Value(accountIDKey{})
aid, ok := v.(AccountID)
if !ok {
return "", status.Error(codes.Unauthenticated, "")
}
return aid, nil
}

注册拦截器:在一个启动的server中加入:

1
2
3
4
5
6
7
// 公钥文件的位置
in, err := auth.Interceptor("shared/auth/public.key")
if err != nil {
logger.Fatal("cannot create auth interceptor")
}
// 注册拦截器
s := grpc.NewServer(grpc.UnaryInterceptor(in))

在需要登录权限的server加下面代码即可

1
2
3
4
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}

重构微服务代码

提取代码放到/shared/server/grpc.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
type GRPCConfig struct {
Name string
Addr string
AuthPublicKeyFile string
RegisterFunc func(*grpc.Server)
Logger *zap.Logger
}

func RunGRPCServer(c *GRPCConfig) error {
nameField := zap.String("name", c.Name)
lis, err := net.Listen("tcp", c.Addr)
if err != nil {
c.Logger.Fatal("cannot listen : %v", nameField, zap.Error(err))
}
var opts []grpc.ServerOption
if c.AuthPublicKeyFile != "" {
in, err := auth.Interceptor("shared/auth/public.key")
if err != nil {
c.Logger.Fatal("cannot create auth interceptor", nameField)
}
opts = append(opts, grpc.UnaryInterceptor(in))
}
s := grpc.NewServer(opts...)
c.RegisterFunc(s)
c.Logger.Info("server stated", nameField, zap.String("addr", c.Addr))
return s.Serve(lis)
}

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
35
36
37
38
39
40
41
42
43
44
45
46
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", 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))
}

// 读取私钥
f, err := os.Open("auth/private.key")
if err != nil {
logger.Fatal("cannot open private.key:%v", zap.Error(err))
}
b, err3 := ioutil.ReadAll(f)
if err3 != nil {
logger.Fatal("cannot read private.key:%v", zap.Error(err3))
}
pk, err4 := jwt.ParseRSAPrivateKeyFromPEM(b)
if err4 != nil {
logger.Fatal("cannot Parse private.key:%v", zap.Error(err4))
}
err = server.RunGRPCServer(&server.GRPCConfig{
Name: "auth",
Addr: ":8081",
Logger: logger,
RegisterFunc: func(s *grpc.Server) {
authpb.RegisterAuthServiceServer(s, &auth.Service{
OpenIDResolver: &wechat.Service{
AppID: "wx0ebc7b6b12ef1585",
AppSecret: "534a58b2185cdbc3dba6168500b7ad3d",
},
Mongo: dao.NewMongo(mongoClient.Database("coolcar")),
Logger: *logger,
TokenExpire: 2 * time.Hour,
TokenGenerator: token.NewJWTTokenGen("coolcar/auth", pk),
})
},
})
if err != nil {
logger.Fatal("cannot server: %v", zap.Error(err))
}
}

rental/main.go修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}

err = server.RunGRPCServer(&server.GRPCConfig{
Name: "rental",
Addr: ":8082",
Logger: logger,
AuthPublicKeyFile: "shared/auth/public.key",
RegisterFunc: func(s *grpc.Server) {
rentalpb.RegisterTripServiceServer(s, &trip.Service{
Logger: logger,
})
},
})
if err != nil {
logger.Fatal("cannot server: %v", zap.Error(err))
}
}

客户端携带Token

image-20220309193944000

步骤一:包装request请求,

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import camelcaseKeys from "camelcase-keys"
import { auth } from "./proto_gen/auth/auth_pb"

export namespace Coolcar{
// 服务器地址
const serverAddr = 'https://localhost:8080'
const AUTH_ERR = 'AUTH_ERR'
const authData = {
token: '',
expiryMs: 0,
}
export interface RequestOption<REQ, RES>{
method: 'GET'|'PUT'|'POST'|'DELETE',
path: string
// 强类型
data: REQ
respMarshaller: (r: object)=>RES
}
// 是否需要携带token
export interface AuthOption{
attachAuthHeader: boolean
retryOnAuthError: boolean
}

export async function sendRequestWithAuthRetry<REQ, RES>(o: RequestOption<REQ, RES>,a?: AuthOption): Promise<RES>{
const authOpt = a||{
attachAuthHeader:true,
retryOnAuthError:true,
}
try {
await login()
return sendRequest(o,authOpt)
}catch(err){
if(err===AUTH_ERR && authOpt.retryOnAuthError){
authData.token = ''
authData.expiryMs=0
return sendRequestWithAuthRetry(o,{
attachAuthHeader:authOpt.attachAuthHeader,
retryOnAuthError:false,
})
}else{
throw err
}
}
}
export async function login() {
if(authData.token && authData.expiryMs >= Date.now()){
return
}
const wxResp = await wxLogin()
const reqTimeMs = Date.now()
const resp = await sendRequest<auth.v1.ILoginRequest, auth.v1.ILoginResponse>({
method: 'POST',
path:'/v1/auth/login',
data: {
code: wxResp.code,
},
respMarshaller: auth.v1.LoginResponse.fromObject,
},{
attachAuthHeader:false,
retryOnAuthError:false,
})
authData.token = resp.accessToken!
authData.expiryMs = reqTimeMs + resp.expiresInt! * 1000
}

function sendRequest<REQ, RES>(o: RequestOption<REQ, RES>,a: AuthOption): Promise<RES>{
return new Promise((resolve, reject)=>{
const header: Record<string, any> = {}
if (a.attachAuthHeader){
if(authData.token && authData.expiryMs >= Date.now()){
header.authorization = 'Bearer ' + authData.token
}else{
reject(AUTH_ERR)
return
}
}
wx.request({
url: serverAddr + o.path,
method: o.method,
data: o.data,
header,
success: res =>{
if(res.statusCode === 401){
reject(AUTH_ERR)
}else if(res.statusCode >= 400){
reject(res)
}else{
o.respMarshaller(camelcaseKeys(res.data as object,{deep:true}))
}
},
fail:reject,
})
})
}

function wxLogin(): Promise<WechatMiniprogram.LoginSuccessCallbackResult>{
return new Promise((resolve,reject)=>{
wx.login({
success:resolve,
fail:reject,
})
})
}
}