本文主要记录由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.
主要配置如下(其他略去),注意配置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;
};
}
通过访问授权服务器的服务/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
设置所有至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
由于官方示例在安装的环境中无法经过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
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包括了我们的请求认证与授权策略配置结果。
名为envoy.lua的httpFilter则包括了我们设置的EnvoyFilter里的配置。
以上仅记录istio中实现基于jwt的请求级的访问控制与授权的过程。关于应用的访问授权,以及应用间的网络传输如TLS等,后续将进一步学习。仅供个人参考!