Go-kit微服务| JWT身份认证

前言

本篇博客其实和Go-Kit微服务框架似乎关联性并不是很大,应该说使用于大多数的场景的服务器的JWT认证部分,主要关联起Go-Kit主要还是结合我具体的实现过程,关于JWT是什么在此就不多做叙述,还是直接切入实现的流程,如果想要了解可以自行搜索或者翻翻我的个人博客也有写过一些小小的见解。

回到主题,Go-Kit微服务其实也有自己实现相关的认证部分,如go-kit微服务:JWT身份认证,但是其使用主要针对于某一个微服务,如果存在多个微服务则需要完成多次的实现。本次我们的项目根据业务与需求,划分成了多个微服务,本着避免代码冗余以及可重用的原则,我们决定自行编写一个简单的JWT中间件,为各个微服务统一提供服务。

实现

JWT 包下载

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

简单使用

生成 Token

Payload 结构体

1
2
3
4
5
6
7
type jwtCustomClaims struct {
jwt.StandardClaims

// 追加自己需要的信息
Id string `json:"id"`
Role int `json:"role"`
}

编写生成 token 的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 生成 token
* SecretKey 是一个 const 常量
*/
func CreateToken(SecretKey []byte, issuer string, id string, role int) (tokenString string, err error) {
claims := &jwtCustomClaims{
jwt.StandardClaims{
ExpiresAt: int64(time.Now().Add(time.Hour * 24 * 365 * 100).Unix()),
Issuer: issuer,
},
id,
role,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err = token.SignedString(SecretKey)
return
}

解析 Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 解析 token
*/
func ParseToken(tokenSrt string, SecretKey []byte) (claims jwt.MapClaims, err error) {
var token *jwt.Token
token, err = jwt.Parse(tokenSrt, func(*jwt.Token) (interface{}, error) {
return SecretKey, nil
})
if err != nil {
return claims, err
}
claims = token.Claims.(jwt.MapClaims)
return
}

从 Request 中获取与解析 Token

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
/**
* 提取 request内的token信息
*/
func GetTokenInfo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var mapClaims jwt.MapClaims
myToken := ""
// 如果token存在于Authorization中
token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
checkErr(err)
if token != nil {
var ok bool
mapClaims, ok = token.Claims.(jwt.MapClaims)
if ok {
myToken = strings.Split(r.Header["Authorization"][0], " ")[1]
}
} else {
// 如果token存在于header中
for k, v := range r.Header {
if strings.ToLower(k) == "token" {
myToken = v[0]
break
}
}
if myToken != "" {
mapClaims, err = ParseToken(myToken, []byte(SecretKey))
if err != nil {
next.ServeHTTP(w, r)
return
}
}
}
// fmt.Println(myToken)
if myToken == "" {
next.ServeHTTP(w, r)
} else {
ctx := context.WithValue(r.Context(), "id", mapClaims["id"].(string))
ctx = context.WithValue(ctx, "role", int(mapClaims["role"].(float64)))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
})
}

Go-Kit 相关

完成上述后似乎大致明确使用,但是如果我们想让token携带的信息能够被我们的微服务直接使用而不是还要自行查数据库或者别的,此时我们需要考虑将其放进requestcontext中,这样当我们解析token后,将信息放入requestcontext中保存,相关的微服务获取其中的值就相当简单了。但是问题来了,对于本项目,我们自行实现了一个简易的API网关来对请求分发到不同的相应的微服务进行处理。初始,我们直接在API网关部分直接解析请求中的token,并将解析得到的信息放入requestcontext中,本以为就这样可以愉快的完成了我的工作。但是,在调用的时候却发现requestcontext部分的内容为空,查阅网上各种博客,无果,只能自己看源码找结果了。

问题定位与原因分析

我们先从反向代理的处理请求一步步找起,其实在Golang实现简单的API网关中我们已经发现其并不会修改相关的request的内容,因此我们决定从Go-Kit的微服务逐步看起。

如果对于Go-Kit如何构造基础服务还比较迷茫,可以参考下微服务架构 | GoKit-CLI使用

开始auth\cmd\main.go

1
2
3
func main() {
service.Run()
}

查看run函数的具体实现,发现其大多为相关日志服务其余高级服务的定义(表示不是很懂,就不下瞎说了,Go-Kit源代码还在阅读中)。但是在下面发现了一个比较关键的函数initHttpHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func initHttpHandler(endpoints endpoint.Endpoints, g *group.Group) {
options := defaultHttpOptions(logger, tracer)
// Add your http options here

httpHandler := http.NewHTTPHandler(endpoints, options)
httpListener, err := net.Listen("tcp", *httpAddr)
if err != nil {
logger.Log("transport", "HTTP", "during", "Listen", "err", err)
}
g.Add(func() error {
logger.Log("transport", "HTTP", "addr", *httpAddr)
return http1.Serve(httpListener, httpHandler)
}, func(error) {
httpListener.Close()
})

}

这个不就是一个HTTP服务初始化的函数吗?,在此需要引用一下官方HTTP服务器的执行过程
Http服务器的执行过程

查看return http1.Serve(httpListener, httpHandler)这个相对应的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Serve accepts incoming HTTP connections on the listener l,
// creating a new service goroutine for each. The service goroutines
// read requests and then call handler to reply to them.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error.
func Serve(l net.Listener, handler Handler) error {
srv := &Server{Handler: handler}
return srv.Serve(l)
}

继续深入查看具体实现。(具体实现太长,就不完整贴出),再看源码中go c.serve(ctx)对应函数,部分源码如下所示
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
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
......

for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
if err != nil {
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

if err == errTooLarge {
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
c.closeWriteAndWait()
return
}
if isCommonNetReadError(err) {
return // don't reply
}

publicErr := "400 Bad Request"
if v, ok := err.(badRequestError); ok {
publicErr = publicErr + ": " + string(v)
}

fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
}

// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
......
}
}

然后发现req其实是在w, err := c.readRequest(ctx)部分就提取好的,在req := w.req对其赋值。主要看下readRequest函数的实现,其部分源码如下
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
// Read next request from connection.
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {

// ......

req, err := readRequest(c.bufr, keepHostHeader)

// ......

ctx, cancelCtx := context.WithCancel(ctx)
req.ctx = ctx
req.RemoteAddr = c.remoteAddr
req.TLS = c.tlsState
if body, ok := req.Body.(*body); ok {
body.doEarlyClose = true
}

// Adjust the read deadline if necessary.
if !hdrDeadline.Equal(wholeReqDeadline) {
c.rwc.SetReadDeadline(wholeReqDeadline)
}

w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),

// We populate these ahead of time so we're not
// reading from req.Header after their Handler starts
// and maybe mutates it (Issue 14940)
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
if isH2Upgrade {
w.closeAfterReply = true
}
w.cw.res = w
w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
return w, nil
}

其实核心在于ctx, cancelCtx := context.WithCancel(ctx),此时我们又要逐步回退看看这个ctx是什么。追溯到net/http/server.gofunc (srv *Server) Serve(l net.Listener) error函数,也就是前面没有放源码的srv.Serve(l)对应函数
1
2
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)

Tips: Golang 中的 context 包提供了 Background 方法和 TODO 方法,用来返回一个emptyContext
发现ctx只是个emptyContext,因此req.ctx = ctx的时候,我们之前传递的ctx并没有保留下来,到这里初始的问题终于解决了。

解决

鉴于上述问题,我们决定在每个微服务部分调用该中间件自行解析token再提取信息放入到生成好的reqcontext中,示例操作如下

修改service/pkg/http/handler_gen.go的NewHTTPHandler函数,加入中间件处理

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

// authentication/pkg/http/handler_gen.go

import MyJwt "github.com/money-hub/MoneyDodo.service/middleware"

// NewHTTPHandler returns a handler that makes a set of endpoints available on
// predefined paths.
func NewHTTPHandler(endpoints endpoint.Endpoints, options map[string][]http.ServerOption) http1.Handler {
m := mux.NewRouter()
m.Use(MyJwt.GetTokenInfo) // 添加中间键处理
makeGetOpenidHandler(m, endpoints, options["GetOpenid"])
makeAdminLoginHandler(m, endpoints, options["AdminLogin"])
makeEnterpriseLoginHandler(m, endpoints, options["EnterpriseLogin"])
makeLogoutHandler(m, endpoints, options["Logout"])
return m
}

// 使用
eg:
ctx.Value("id").(string)
ctx.Value("role").(int)

相关链接