Kong网关jwt插件实现细节

3,304 阅读4分钟

背景

网关版本升级(从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
  1. 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 the Authorization header.\

从请求中获取JWT,获取逻辑是依次从url参数、cookies、最后请求头中。

  1. 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) 

  1. 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")
)
;
  1. Verify "alg"
  2. Verify the JWT signature
  3. Verify the JWT registered claims
  4. Retrieve the consumer
  5. 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

区别:

  1. 0.12.0 版jwt do_authenticationjwt_secret_key只从jwt payload中获取,并没有另外从请求头中获取;
local claims = jwt.claims

local jwt_secret_key = claims[conf.key_claim_name]
  1. 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
  1. 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在升级后的网关中被暴露出来,但在老网关中一直没有暴露。