今天我和你分享的是如何保证微服务实例资源安全的案例。

在上一课时中,我们实践了如何使用 Go 搭建一个基本的授权服务器,它的主要功能是颁发访问令牌和验证访问令牌的有效性。在统一认证与授权服务体系中,还存在资源服务器对用户数据进行保护,它允许携带有效访问令牌的客户端请求用户资源。

在本课时,我们将基于 Go 实现一个基本的资源服务器,让其为用户数据保驾护航。

整体结构

资源服务器会在请求进入具体的资源端点之前,对请求中携带的访问令牌进行校验,比较常用的做法是采用拦截器的方式实现,如下图所示:

资源服务器中请求流程图

请求在进入具体的资源端点之前,会至少经过令牌认证拦截器和权限检查拦截器这两个拦截器,以及其他发挥重要功能的拦截器,比如限流拦截器等。令牌认证拦截器会解析请求中携带的访问令牌,请求授权服务器验证访问令牌的有效性,明确当前请求的客户端和用户信息,并把这些信息写入请求上下文中,如果访问令牌无效,将会拒绝请求,返回认证错误。权限检查拦截器会按照预设的权限规则对请求上下文中的客户端和用户信息进行权限检查,如果权限不足也会拒绝访问,返回鉴权错误。

对此我们可以将资源服务器设计为以下几个模块,如图所示:

资源服务器模块组成图

OAuth2AuthorizationContext(认证上下文处理器),负责从请求解析出访问令牌,委托 ResourceServerTokenService 验证访问令牌的有效性,获取令牌对应的客户端和用户信息。

OAuth2AuthorizationMiddleware(认证中间件),检查请求上下文是否存在客户端和用户信息。

AuthorityAuthorizationMiddleware(权限检查中间件),从请求上下文中获取客户端和用户信息,并根据预设的权限规则对请求的客户端和用户信息进行鉴权。

ResourceServerTokenService(资源服务器令牌服务),帮助资源服务器检验访问令牌的有效性以及获取访问令牌绑定的客户端和用户信息。

接下来,我们就来详细讲解一下如何实现资源服务器。

认证上下文处理器

客户端在请求资源服务器中被保护的端点时,默认会把访问令牌放到 Authorization 请求头,然后资源服务器会在请求进入 Endpoint 之前,从 Authorization 请求头中获取访问令牌用于验证用户身份。

OAuth2AuthorizationContext(认证上下文处理器)用于从 Authorization 请求头解析出访问令牌,并使用 ResourceServerTokenService 根据访问令牌获取用户信息和客户端信息。构建 OAuth2AuthorizationContext 的代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func makeOAuth2AuthorizationContext(tokenService service. ResourceServerTokenService, logger log.Logger) kithttp.RequestFunc {
	return func(ctx context.Context, r *http.Request) context.Context {
		// 获取访问令牌
		accessTokenValue := r.Header.Get("Authorization")
		var err error
		if accessTokenValue != ""{
			// 获取令牌对应的用户信息和客户端信息
			oauth2Details, err := tokenService.GetOAuth2DetailsByAccessToken(accessTokenValue)
			if err != nil{
				return context.WithValue(ctx, endpoint.OAuth2ErrorKey, err)
			}
			return context.WithValue(ctx, endpoint.OAuth2DetailsKey, oauth2Details)
		}else {
			err = ErrorTokenRequest
		}
		return context.WithValue(ctx, endpoint.OAuth2ErrorKey, err)
	}
}

在上述代码中,如果 Authorization 请求头不存在访问令牌或者访问令牌无效,将在 context 中设置令牌无效的错误信息。接着OAuth2AuthorizationContext 会使用 ResourceServerTokenService 根据访问令牌解析出令牌对应的用户信息和客户端信息,如果解析成功,说明当前请求的客户端已经得到了用户的授权。最后再把用户信息和客户端信息放入 context 中,便于接下来的认证与鉴权使用。

认证中间件

在请求正式进入 Endpoint 之前,我们需要验证请求上下文 context 中是否存在 OAuth2Details,是否存在客户端和用户信息。对此,我们对每个需要进行认证的端点添加认证中间件(OAuth2AuthorizationMiddleware),代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func MakeClientAuthorizationMiddleware(logger log.Logger) endpoint.Middleware {
	return func(next endpoint.Endpoint) endpoint.Endpoint {

		return func(ctx context.Context, request interface{}) (response interface{}, err error) {
         // 检查是否出现认证错误
			if err, ok := ctx.Value(OAuth2ErrorKey).(error); ok{
				return nil, err
			}
			// 检查请求上下文中是否存在客户端和用户信息
			if _, ok := ctx.Value(OAuth2ClientDetailsKey).(*model.ClientDetails); !ok{
				return  nil, ErrInvalidClientRequest
			}
			return next(ctx, request)
		}
	}
}

在上述代码中,在请求进入业务处理的 Endpoint 之前,OAuth2AuthorizationMiddleware 认证中间件会检查 context 中的OAuth2Details 是否存在,如果不存在,说明请求没有经过认证,请求将会被拒绝访问。如果存在,说明请求已经携带了有效的访问令牌,将被允许通过该中间件。

权限检查中间件

访问资源服务器受保护资源的端点时,不仅需要请求中携带有效的访问令牌,还需要访问令牌绑定的客户端和用户具备足够的权限。

在 OAuth2AuthorizationContext(认证上下文处理器)中我们获取到了用户信息和客户端信息,可以根据它们具备的权限列表和预设的权限规则,判断本次请求是否具备访问端点的权限。对此我们需要添加 AuthorityAuthorizationMiddleware(权限检查中间件),它会根据预设的权限规则,对访问令牌绑定的用户权限进行检查,只有具备足够权限的用户请求,才能够进入 Endpoint 中执行业务逻辑。构建 AuthorityAuthorizationMiddleware的代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func MakeAuthorityAuthorizationMiddleware(authority string, logger log.Logger) endpoint.Middleware  {
	return func(next endpoint.Endpoint) endpoint.Endpoint {

		return func(ctx context.Context, request interface{}) (response interface{}, err error) {
         // 检查是否出现认证错误
			if err, ok := ctx.Value(OAuth2ErrorKey).(error); ok{
				return nil, err
			}
		   // 检查是否具备预设权限
			if details, ok := ctx.Value(OAuth2DetailsKey).(*model.OAuth2Details); !ok{
				return  nil, ErrInvalidClientRequest
			}else {
				for _, value := range details.User.Authorities{
					if value == authority{
						return next(ctx, request)
					}
				}
				return nil, ErrNotPermit
			}
		}
	}
}

在上述代码中,我们先从 context 中获取到访问令牌中解析出的用户信息和客户端信息,然后对用户的权限进行检查,只有具备预设权限的用户才能继续访问接口,否则返回权限不足的错误。此处实现的权限检查中间件判断逻辑比较单一,用户只需具备对应的权限即可通过判定,但在实际生产环境中,我们可以组合更加复杂的权限判断逻辑以满足业务需求。

资源服务器令牌服务

ResourceServerTokenService(资源服务器令牌服务)的作用是资源服务器验证访问令牌的有效性和解析出令牌绑定的客户端和用户信息,它提供以下接口:

1
2
3
4
type ResourceServerTokenService interface {
	// 根据访问令牌获取对应的用户信息和客户端信息
	GetOAuth2DetailsByAccessToken(tokenValue string) (*OAuth2Details, error)
}

一般来说,资源服务器都是通过远程调用的方式,访问授权服务器的 oauth/check_token 端点来验证访问令牌的有效性。但是由于访问令牌的类型为 JWT,令牌中的信息是自包含的,所以我们在资源服务器中就可以直接验证访问令牌,并从令牌中解析出用户信息和客户端信息,如下代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (tokenService *DefaultTokenService) GetOAuth2DetailsByAccessToken(tokenValue string) (*OAuth2Details, error) {
	// 借助 JwtTokenStore 从令牌中解析出信息
	accessToken, err := tokenService.tokenStore.ReadAccessToken(tokenValue)

	if err != nil{
		return nil, err
	}
	if accessToken.IsExpired(){
		return nil, ErrExpiredToken
	}

	return tokenService.tokenStore.ReadOAuth2Details(tokenValue)
}

在上述代码中,我们就是直接通过 JwtTokenStore 从 JWT 样式的令牌中解析出令牌绑定的客户端和用户信息。

接下来我们通过构造一些资源端点来验证资源服务器保护受限资源的能力。

访问受限资源端点

受保护资源是资源服务器中被保护的用户数据。请求必须持有访问令牌,且访问令牌绑定的用户具备足够的权限才允许访问资源端点,也就是说在请求到达受保护资源的端点前,需要被认证中间件和权限检查中间件对请求中携带的访问令牌进行校验。

我们分别构造以下 3 个端点:

/index,任意请求可访问;

/sample,携带有效访问令牌的请求可访问;

/admin,携带有效访问令牌,且访问令牌绑定的用户具备 Admin 权限的请求可访问。

在 transport 层,为了保证认证中间件和权限检查中间件能够获取访问令牌绑定的用户和客户端信息,在请求处理前我们添加了OAuth2AuthorizationContext(认证上下文处理器)从请求头中解析并验证token,代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 添加认证上下文处理器
oauth2AuthorizationOptions := []kithttp.ServerOption{
		kithttp.ServerBefore(makeOAuth2AuthorizationContext(tokenService, logger)),
		kithttp.ServerErrorHandler(transport.NewLogErrorHandler(logger)),
		kithttp.ServerErrorEncoder(encodeError),
	}
   // index 端点
	r.Methods("Get").Path("/index").Handler(kithttp.NewServer(
		endpoints.IndexEndpoint,
		decodeSimpleRequest,
		encodeJsonResponse,
		oauth2AuthorizationOptions...,
	))
   ...其他端点

接着在 main 函数中,为需要进行认证和权限检查的 SampleEnpoint 和 AdminEndpoint 添加认证中间件和权限检查中间件,代码如下所示:

1
2
3
4
5
6
7
 sampleEndpoint := endpoint.MakeSampleEndpoint(srv)
  // 添加认证中间件
  sampleEndpoint = endpoint.MakeOAuth2AuthorizationMiddleware(config.KitLogger)(sampleEndpoint)
  adminEndpoint := endpoint.MakeAdminEndpoint(srv)
// 添加认证中间件和权限检查中间件 
  adminEndpoint = endpoint.MakeOAuth2AuthorizationMiddleware(config.KitLogger)(adminEndpoint)
  adminEndpoint = endpoint.MakeAuthorityAuthorizationMiddleware("Admin", config.KitLogger)(adminEndpoint)

在上述代码中,我们可以发现 SampleEndpoint 被 OAuth2AuthorizationMiddleware(认证中间件)装饰,而AdminEndpoint被 OAuth2AuthorizationMiddleware(认证中间件)和 AuthorityAuthorizationMiddleware(权限检查中间件)同时装饰。

接下来我们在授权服务器内内置两名用户信息:①用户名 aoho1、密码 123456,权限为 sample;②用户名 aoho2、密码 123456,权限为 admin。启动资源服务器,直接访问 /index 端点,可以直接获取到请求结果,如下所示:

1
2
3
4
{
    "result": "hello, wecome to index",
    "error": ""
}

然后我们直接访问 /sample 端点,将会获取到以下拒绝访问的错误:

1
2
3
{
    "error": "invalid request token"
}

对此,我们需要使用 aoho1 用户的用户名和密码请求授权服务器获取对应的访问令牌。携带访问令牌再次请求 /sample 端点,请求的 curl 命令如下:

1
2
3
4
curl -X GET \
  http://localhost:10099/simple \
  -H 'Authorization: ...' \
  -H 'Host: localhost:10099' \

我们在 Authorization 请求头中携带了访问令牌,即可获取到期望的请求数据,如下所示:

1
2
3
4
{
    "result": "hello aoho1, welcome to sample",
    "error": ""
}

当我们以同样的访问令牌,即 aoho1 用户授权的访问令牌请求 /admin 端点时将会返回权限不足的错误,如下所示:

1
2
3
{
    "error": "not permit"
}

对此,我们需要使用 aoho2 用户授权访问令牌请求 /admin 端点。从授权服务器获取到 aoho2 用户授权的访问令牌后,携带其访问令牌再次访问 /admin 端点,即可获取到如下预期的结果:

1
2
3
4
{
    "result": "hello aoho2, welcome to admin",
    "error": ""
}

通过组合认证中间件和权限检查中间件,我们可以检查请求中是否携带合法的访问令牌以及访问令牌绑定的用户是否具备足够的访问权限,这样就有效地在接口层级保护数据资源了。

小结

在统一认证与授权服务体系中,资源服务器的主要职责为保护用户保存在系统中的数据,允许携带有效访问令牌的客户端请求资源,拒绝无授权的请求访问。

在本课时,我们基于 Go 实现了一个基本的资源服务器。它首先通过认证上下文处理器从请求中解析出访问令牌,并借助资源服务器令牌服务验证访问令牌的有效性;接着再使用认证中间件和权限检查中间件对令牌绑定的客户端和用户信息进行认证和权限检查,允许携带有效访问令牌和满足预设权限的客户端请求获取到数据。希望通过本模块这4 个课时的学习,能帮助你充分了解如何构建微服务中统一认证与授权体系。

最后,对于如何实践统一认证与授权服务体系,你还有什么其他见解呢?欢迎在留言区与我分享。

-– ### 精选评论