前言
本篇博客其实和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
7type 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 | /** |
从 Request 中获取与解析 Token
1 | /** |
Go-Kit 相关
完成上述后似乎大致明确使用,但是如果我们想让token
携带的信息能够被我们的微服务直接使用而不是还要自行查数据库或者别的,此时我们需要考虑将其放进request
的context中,这样当我们解析token
后,将信息放入request
的context
中保存,相关的微服务获取其中的值就相当简单了。但是问题来了,对于本项目,我们自行实现了一个简易的API网关
来对请求分发到不同的相应的微服务进行处理。初始,我们直接在API网关部分
直接解析请求中的token
,并将解析得到的信息放入request
的context
中,本以为就这样可以愉快的完成了我的工作。但是,在调用的时候却发现request
的context
部分的内容为空,查阅网上各种博客,无果,只能自己看源码找结果了。
问题定位与原因分析
我们先从反向代理的处理请求一步步找起,其实在Golang实现简单的API网关中我们已经发现其并不会修改相关的request
的内容,因此我们决定从Go-Kit
的微服务逐步看起。
如果对于
Go-Kit
如何构造基础服务还比较迷茫,可以参考下微服务架构 | GoKit-CLI使用
开始auth\cmd\main.go
1
2
3func main() {
service.Run()
}
查看run
函数的具体实现,发现其大多为相关日志服务其余高级服务的定义(表示不是很懂,就不下瞎说了,Go-Kit
源代码还在阅读中)。但是在下面发现了一个比较关键的函数initHttpHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func 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服务器的执行过程
查看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.go
中func (srv *Server) Serve(l net.Listener) error
函数,也就是前面没有放源码的srv.Serve(l)
对应函数1
2baseCtx := 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
再提取信息放入到生成好的req
的context
中,示例操作如下
修改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)