背景
网关版本升级(从0.12.0升级到0.14.1),升级过程中遇到相同同一请求新版本的网关报401,从access.log中能看到插件是解析Token成功且成功获取到了Token对应的Consumer。因此需要对比一下两者版本的插件逻辑上的区别。
Kong的升级
在看jwt的升级前,大概过一下kong在0.12.0到0.14.1的修改。0.14.1的kong有意要将API的概念干掉,随之引入的是Service加Router的概念。
以API为维度的Kong
Service+Router概念的Kong
相较于0.12.0及以前的版本,最新的Kong支持一个插件可以管理一组服务,达到插件复用的效果。对于服务控制的粒度也更细。
jwt插件
0.14.1
local function do_authentication(conf)
local token, err = retrieve_token(ngx.req, conf)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
local ttype = type(token)
if ttype ~= "string" then
if ttype == "nil" then
return false, {status = 401}
elseif ttype == "table" then
return false, {status = 401, message = "Multiple tokens provided"}
else
return false, {status = 401, message = "Unrecognizable token"}
end
end
-- Decode token to find out who the consumer is
local jwt, err = jwt_decoder:new(token)
if err then
return false, {status = 401, message = "Bad token; " .. tostring(err)}
end
local claims = jwt.claims
local header = jwt.header
local jwt_secret_key = claims[conf.key_claim_name] or header[conf.key_claim_name]
if not jwt_secret_key then
return false, {status = 401, message = "No mandatory '" .. conf.key_claim_name .. "' in claims"}
end
-- Retrieve the secret
local jwt_secret_cache_key = singletons.dao.jwt_secrets:cache_key(jwt_secret_key)
local jwt_secret, err = singletons.cache:get(jwt_secret_cache_key, nil,
load_credential, jwt_secret_key)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
if not jwt_secret then
return false, {status = 403, message = "No credentials found for given '" .. conf.key_claim_name .. "'"}
end
local algorithm = jwt_secret.algorithm or "HS256"
-- Verify "alg"
if jwt.header.alg ~= algorithm then
return false, {status = 403, message = "Invalid algorithm"}
end
local jwt_secret_value = algorithm ~= nil and algorithm:sub(1, 2) == "HS" and
jwt_secret.secret or jwt_secret.rsa_public_key
if conf.secret_is_base64 then
jwt_secret_value = jwt:b64_decode(jwt_secret_value)
end
if not jwt_secret_value then
return false, {status = 403, message = "Invalid key/secret"}
end
-- Now verify the JWT signature
if not jwt:verify_signature(jwt_secret_value) then
return false, {status = 403, message = "Invalid signature"}
end
-- Verify the JWT registered claims
local ok_claims, errors = jwt:verify_registered_claims(conf.claims_to_verify)
if not ok_claims then
return false, {status = 401, message = errors}
end
-- Verify the JWT registered claims
if conf.maximum_expiration ~= nil and conf.maximum_expiration > 0 then
local ok, errors = jwt:check_maximum_expiration(conf.maximum_expiration)
if not ok then
return false, {status = 403, message = errors}
end
end
-- Retrieve the consumer
local consumer_cache_key = singletons.db.consumers:cache_key(jwt_secret.consumer_id)
local consumer, err = singletons.cache:get(consumer_cache_key, nil,
load_consumer,
jwt_secret.consumer_id, true)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
-- However this should not happen
if not consumer then
return false, {status = 403, message = string_format("Could not find consumer for '%s=%s'", conf.key_claim_name, jwt_secret_key)}
end
set_consumer(consumer, jwt_secret, token)
return true
end
local token, err = retrieve_token(ngx.req, conf)
-- Retrieve a JWT in a request.
-- Checks for the JWT in URI parameters, then in cookies, and finally
-- in theAuthorization
header.\
从请求中获取JWT,获取逻辑是依次从url参数、cookies、最后请求头中。
local jwt, err = jwt_decoder:new(token)
对Token进行解码,得到jwt对象。这里展开介绍一下jwt结构: jwt主要由三部分构成:
- header:交代了jwt的前面算法。如图所示是
HS256
; - payload:消息体包含Token的内容;
- signature:将header和payload分别进行Base64编码,转码后的header和playload按分隔符拼接。通过secret计算获得签名。
key = 'secret'
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)
signature = HMAC-SHA256(key, unsignedToken)
Retrieve the secret
local claims = jwt.claims
local header = jwt.header
local jwt_secret_key = claims[conf.key_claim_name] or header[conf.key_claim_name]
if not jwt_secret_key then
return false, {status = 401, message = "No mandatory '" .. conf.key_claim_name .. "' in claims"}
end
local jwt_secret_cache_key = singletons.dao.jwt_secrets:cache_key(jwt_secret_key)
local jwt_secret, err = singletons.cache:get(jwt_secret_cache_key, nil,
load_credential, jwt_secret_key)
获取secret。conf.key_claim_name
是插件配置的key的name为iss
。拿到iss
对应的secret后从缓存中找到secret对应的jwt_secret
记录。jwt_secret
是Kong内部的一张表,存储了consumer与secret的对应关系。通过请求的Token解析出接入方是谁。这里后续可以做很多事情,但jwt插件只是校验Token的合法性。
CREATE TABLE "public"."jwt_secrets" (
"id" uuid NOT NULL,
"consumer_id" uuid,
"key" text COLLATE "pg_catalog"."default",
"secret" text COLLATE "pg_catalog"."default",
"created_at" timestamp(6) DEFAULT timezone('utc'::text, ('now'::text)::timestamp(0) with time zone),
"algorithm" text COLLATE "pg_catalog"."default",
"rsa_public_key" text COLLATE "pg_catalog"."default",
CONSTRAINT "jwt_secrets_pkey" PRIMARY KEY ("id"),
CONSTRAINT "jwt_secrets_consumer_id_fkey" FOREIGN KEY ("consumer_id") REFERENCES "public"."consumers" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "jwt_secrets_key_key" UNIQUE ("key")
)
;
Verify "alg"
Verify the JWT signature
Verify the JWT registered claims
Retrieve the consumer
set_consumer
0.12.0
local function do_authentication(conf)
local token, err = retrieve_token(ngx.req, conf)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
local ttype = type(token)
if ttype ~= "string" then
if ttype == "nil" then
return false, {status = 401}
elseif ttype == "table" then
return false, {status = 401, message = "Multiple tokens provided"}
else
return false, {status = 401, message = "Unrecognizable token"}
end
end
-- Decode token to find out who the consumer is
local jwt, err = jwt_decoder:new(token)
if err then
return false, {status = 401, message = "Bad token; " .. tostring(err)}
end
local claims = jwt.claims
local jwt_secret_key = claims[conf.key_claim_name]
if not jwt_secret_key then
return false, {status = 401, message = "No mandatory '" .. conf.key_claim_name .. "' in claims"}
end
-- Retrieve the secret
local jwt_secret_cache_key = singletons.dao.jwt_secrets:cache_key(jwt_secret_key)
local jwt_secret, err = singletons.cache:get(jwt_secret_cache_key, nil,
load_credential, jwt_secret_key)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
if not jwt_secret then
return false, {status = 403, message = "No credentials found for given '" .. conf.key_claim_name .. "'"}
end
local algorithm = jwt_secret.algorithm or "HS256"
-- Verify "alg"
if jwt.header.alg ~= algorithm then
return false, {status = 403, message = "Invalid algorithm"}
end
local jwt_secret_value = algorithm == "HS256" and jwt_secret.secret or jwt_secret.rsa_public_key
if conf.secret_is_base64 then
jwt_secret_value = jwt:b64_decode(jwt_secret_value)
end
if not jwt_secret_value then
return false, {status = 403, message = "Invalid key/secret"}
end
-- Now verify the JWT signature
if not jwt:verify_signature(jwt_secret_value) then
return false, {status = 403, message = "Invalid signature"}
end
-- Verify the JWT registered claims
local ok_claims, errors = jwt:verify_registered_claims(conf.claims_to_verify)
if not ok_claims then
return false, {status = 401, message = errors}
end
-- Retrieve the consumer
local consumer_cache_key = singletons.dao.consumers:cache_key(jwt_secret.consumer_id)
local consumer, err = singletons.cache:get(consumer_cache_key, nil,
load_consumer,
jwt_secret.consumer_id, true)
if err then
return responses.send_HTTP_INTERNAL_SERVER_ERROR(err)
end
-- However this should not happen
if not consumer then
return false, {status = 403, message = string_format("Could not find consumer for '%s=%s'", conf.key_claim_name, jwt_secret_key)}
end
set_consumer(consumer, jwt_secret)
return true
end
区别:
- 0.12.0 版jwt
do_authentication
的jwt_secret_key
只从jwt payload中获取,并没有另外从请求头中获取;
local claims = jwt.claims
local jwt_secret_key = claims[conf.key_claim_name]
- 0.14.1支持HS算法扩展
local jwt_secret_value = algorithm == "HS256" and jwt_secret.secret or jwt_secret.rsa_public_key
if conf.secret_is_base64 then
jwt_secret_value = jwt:b64_decode(jwt_secret_value)
end
- 0.12.1并没有对Token时效性进行校验
-- Verify the JWT registered claims
if conf.maximum_expiration ~= nil and conf.maximum_expiration > 0 then
local ok, errors = jwt:check_maximum_expiration(conf.maximum_expiration)
if not ok then
return false, {status = 403, message = errors}
end
end
结合对比可以基本确定为0.14.1对Token做过期时间的校验,导致了过期的Token在升级后的网关中被暴露出来,但在老网关中一直没有暴露。