Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权

文章目录

  • 前言
  • 一、前置条件
  • 二、实现步骤
    • 1. 基于JWT访问授权
      • a. Jwks配置
      • b. RequestAuthentication配置
      • c. AuthorizationPolicy配置
    • 2. 基于EnvoyFilter的API鉴权
    • 3. 配置结果查询
    • 4. 测试结果
      • a. 不带Token访问
      • b. 错误的Token访问
      • c. 正常的Token访问
  • 总结


前言

本文主要记录由SpringCloud Zuul1.0对接Oauth 2.0服务做API鉴权,转型为由Istio IngressGateway实现。主要利用Istio的RequestAuthentication, AuthorizationPolicy以及EnvoyFilter等功能实现。由于官方示例未能生效,亦尚未深入探究,故以文字记录主要实现步骤,以备参考(所有域名设置为demo.com)。


一、前置条件

本文基于Istio 1.6.8,kubernetes 1.5.6, 有关Istio的安装配置,请参考 文章Istio学习笔记:Istio及Kiali的安装与配置 或Istio 官方文档。OAuth2.0授权服务器需要实现非对称加密。授权实现主要参考文档Istio Doc v1.6中的Task->Security->Authorizatoin.

二、实现步骤

1. 基于JWT访问授权

a. Jwks配置

主要配置如下(其他略去),注意配置iss和sub。

@GetMapping("/.well-known/jwks.json")
public Map<String, Object> keys() {
   return this.jwkSet.toJSONObject();
}
@Bean
public JWKSet jwkSet() {
	if (keyType.equals("RSA")) {
		KeyStoreKeyFactoryUtil keyStoreKeyFactory =
				new KeyStoreKeyFactoryUtil(privateKey, keyPwd.toCharArray());
		RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey)keyStoreKeyFactory.getKeyPair(keyPair).getPublic())
				.keyUse(KeyUse.SIGNATURE)
				.algorithm(JWSAlgorithm.RS256)
				.keyID("bael-key-id");
		return new JWKSet(builder.build());
	}else{
		return null;
	}
}
@Bean
public TokenEnhancer tokenEnhancer(final DataSource dataSource) {
	return (accessToken, authentication) -> {

		JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);

		clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);
		clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);

		Map<String, Object> retMap = new HashMap<String, Object>();
		String clientId = authentication.getOAuth2Request().getClientId();

		String changeToken = authentication.getOAuth2Request().getRequestParameters().get("longTokenFlag");
		//长token 逻辑
		if ("Y".equals(changeToken)) {
			Map<String, String> paramMap = authentication.getOAuth2Request().getRequestParameters();
			Object user_name = paramMap.get("user_name");
			if (user_name != null && !user_name.equals("")) {
				retMap.put("user_name", user_name);
			}
			Object ori_client_id = paramMap.get("ori_client_id");
			if (ori_client_id != null && !ori_client_id.equals("")) {
				retMap.put("ori_client_id", ori_client_id);
				clientId = ori_client_id + "";
			}
			retMap.put("long_token_flag", "Y");
		}
		retMap.put("key_version", keyVersion);
		retMap.put("iss","demo.com");

		//为token设置userId值
		String userId = authentication.getOAuth2Request().getRequestParameters().get("userId");
		Object principal = authentication.getPrincipal();
		if (StringUtils.isEmpty(userId)) {
			//参数[userId]为空时,取username值
			if (principal instanceof UserDetails){
				userId = ((UserDetails)principal).getUsername();
			}else {
				userId = String.valueOf(principal);
			}
		}
		retMap.put("user_id", userId);
		
		if (principal instanceof UserDetails){
			String nickName = ((UserDetails)principal).getUsername();
			retMap.put("nick_name", nickName);
			retMap.put("sub", nickName);

		}else {
			retMap.put("nick_name", "");
			retMap.put("sub", "");
		}
		
	   
		//为token设置cas tgc key值
		String tgcKey = authentication.getOAuth2Request().getRequestParameters().get(TgcKeyCache.TOKEN_TGC_KEY);
		if (StringUtils.isNotEmpty(tgcKey)) {
			retMap.put(TgcKeyCache.TOKEN_TGC_KEY, tgcKey);
		}

		ClientDetails cd = clientDetailsService.loadClientByClientId(clientId);
		Map<String, Object> map = cd.getAdditionalInformation();
		retMap.putAll(map);
		((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap);

		return accessToken;
	};
}

b. RequestAuthentication配置

通过访问授权服务器的服务/oauth/oauthCustom/.well-known/jwks.json即可以获取jwks的配置,当前采用配值的方式,如下:

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  labels:
    kiali_wizard: RequestAuthentication
  name: jwt-request-auth
  namespace: istio-system
spec:
  jwtRules:
  - forwardOriginalToken: true
    fromHeaders:
    - name: Authorization
      prefix: 'Bearer '
    issuer: demo.com
    jwks: '{
        "keys": [{
                "kty": "RSA",
                "e": "AQAB",
                "use": "sig",
                "kid": "bael-key-id",
                "alg": "RS256",
                "n": "xSUlCIvHxZNIVTC-ku7QRqAR5QPV4jjw9N2LWbkB5Cnw0YrSz-ruzKXj4fcMsGwtYdd3XY_PhDtRxNiW_6JV9MyrK4twZJJanroI_fSThOLcxOulS-mSH8v5041q3re3pkGBIejFh74hp8xlbgCoXWybZIyCuMgT3RI8o4a6ICN-H9QfbLr9Y7DXijlgrdAMf1Oski_HmW8w9Ufz3xTqeycMpbIb5SWlcqu2ksCYnlNd2k5HyAt4ecxFveBVCczCMrxbAvis82T4wHqZZ2ge4fBizFNNnijC0w6xZTXXEIE6AJS8falSnrZWtHkRpCPqAAYSGrHP1j_cbvaqGkmHKw"
        }]
      }'
  selector:
    matchLabels:
      istio: ingressgateway

c. AuthorizationPolicy配置

设置所有至istio ingressgateway的请求,携带的jwt的requestPrincipals 必须是iss/sub(jwks中的设置)。并设置需要jwt访问的目标为/apis/**(白名单列表除外)。

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  labels:
    kiali_wizard: AuthorizationPolicy
  name: jwt-auth-policy
  namespace: istio-system
spec:
  action: ALLOW
  rules:
  - from:
    - source:
        requestPrincipals:
        - demo.com/*
  - to:
    - operation:
        notPaths:
        - /apis/*
  - to:
    - operation:
        paths:
        - /apis/oauth/oauthProxy/getUserToken
  selector:
    matchLabels:
      istio: ingressgateway

2. 基于EnvoyFilter的API鉴权

由于官方示例在安装的环境中无法经过lua 调用auth服务。最终经过多番折腾出可用的版本,后续将进一步学习探究。所有访问/apis/**的服务均需要调用后台的API鉴权服务,以验证用户是否被授权访问。

kind: EnvoyFilter
apiVersion: networking.istio.io/v1alpha3
metadata:
  name: gateway-auth-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: envoy.http_connection_manager
              subFilter:
                name: envoy.router
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.lua
          typed_config:
            '@type': type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
            inlineCode: >
              -- whilelist check

              whitelist={"/apis/oauth/oauthProxy/getUserToken"}

              function isIgnoreUrls(apiUrl)
                               
                --check if in the whitelist
                for key,value in ipairs(whitelist) do
                  if value==apiUrl then
                    return true
                  end
                end 
                
                local paramIndex=string.find(apiUrl,"?")
                if paramIndex ~= nil then
                  apiUrl=string.sub(apiUrl,1,paramIndex-1)
                end
                -- only auth the url starts with '/apis'
                local apiIndex =string.find(apiUrl, "/apis/")
                if apiIndex == 1 then
                  return false
                else
                  return true
                end
              end

              -- resolve apiUrl for authenticatioin. exampmle:
              -- if apiUrl like '/apis/xxx/getXX?', then we should remove '/apis/xxx',xxx used to mean the service short name
              function getAuthPath(apiUrl)
                local apisIndex=string.find(apiUrl, "/apis/")
                if apisIndex == 1 then
                  local paramIndex=string.find(apiUrl,"?")
                  if(paramIndex ~= nil) then
                    apiUrl=string.sub(apiUrl,1,paramIndex-1)
                  end 
                  apiUrl=string.sub(apiUrl,7)
                  local startIndex=string.find(apiUrl,"/")
                  apiUrl=string.sub(apiUrl,startIndex)
                end
                return apiUrl
              end


              function envoy_on_request(handle)
                  local apiUrl=handle:headers():get(":path")
                  handle:logWarn("Demo gateway-auth-filter: begin with path==>"..apiUrl)

                  --return if ignored or should by authenticated
                  if isIgnoreUrls(apiUrl) then
                      handle:logWarn("Demo gateway-auth-filter: end ignored with path==>"..apiUrl)
                      return
                  else 
                    local authUrl=getAuthPath(apiUrl)
                      handle:logWarn("authUrl=================="..authUrl)
                      local headers, retRs = handle:httpCall(
                        "outbound_.8089_.v1_.oauth-server.cloud-istio.svc.cluster.local",
                        {
                         [":method"] = "POST",
                         [":path"] = "/security/user/isPermitted?apiUrl="..authUrl,
                         [":authority"] = "oauth-server.cloud-istio.svc.cluster.local",
                         ["authorization"] = handle:headers():get("authorization")
                        },
                       "authorize call",
                       5000)
                       if  retRs=="false" then
                         handle:logErr("Demo gateway-auth-filter: end no permission with path==>"..apiUrl)
                         handle:respond({[":status"] = "403"})   
                       end
                  end
                  handle:logWarn("Demo gateway-auth-filter: end success with path==>"..apiUrl)
              end

              function envoy_on_response(handle)
                handle:headers():add("x-from-proxy","istio-ingressgateway")
              end


3. 配置结果查询

istioctl pc listener $(kubectl get pod -n istio-system | grep -i istio-ingressgateway | awk '{print $1}')   -o json -n istio-system

名为envoy.filters.http.jwt_authn、istio_authn及envoy.filters.http.rbac的httpFilters包括了我们的请求认证与授权策略配置结果。
Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权_第1张图片

名为envoy.lua的httpFilter则包括了我们设置的EnvoyFilter里的配置。
Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权_第2张图片

4. 测试结果

a. 不带Token访问

Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权_第3张图片

b. 错误的Token访问

Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权_第4张图片

c. 正常的Token访问

Istio学习笔记:IngressGateway实现基于OAuth2.0+JWT的API鉴权_第5张图片

总结

以上仅记录istio中实现基于jwt的请求级的访问控制与授权的过程。关于应用的访问授权,以及应用间的网络传输如TLS等,后续将进一步学习。仅供个人参考!

你可能感兴趣的:(云原生,Istio,ServiceMesh)