Kong 优雅实现微服务网关鉴权,登录场景落地实战篇

2,451 阅读10分钟

目录[toc]

登录实现

B 端登录之后,浏览器存 cookie

登录后的cookie

cookie 内容如下:

  • key: xxx_V2
  • value: 3001459%7C1636684996%7C7180720502%7Cb61a12ef865072964aa359e6a9ef2e0b1846dee9

value 分为四段值,由 %7C 隔开,这个Ascll码值是代表符号"|".

四段分别代表 userId, 时间戳 time, 随机数 nonce, 加密算法得到的 sign

登录代码实现细节,cookie设计

/**
     * 用户登录逻辑
     * @global array $_SESSION
     * @param type $user_id
     */
    public static function do_login($user_id) {
        global $_SESSION;
        $_SESSION["uid"] = $user_id;
        KZUser::add_auth_to_session($user_id);

        // 更新帐号失败尝试次数
        unset($_SESSION["login_try"]);
        unset($_SESSION["need_vcode"]);

        //send the cookie
        $login_time = time();
        $nonce = mt_rand(1000000000, 9999999999);
        $cookie_params = array($user_id, $login_time, $nonce);
        // COOKIE_KEY 即加密的密钥,和网关那边配置的一样
        $cookie_params[] = hash_hmac("sha1", implode("", $cookie_params), COOKIE_KEY, false);
        // 测试环境特殊处理
        $domain = str_replace(".t1.com", DOMAIN_POSTFIX, COOKIE_DOMAIN);
        // SESSION_COOKIE_NAME=xxx_V2
        setcookie(SESSION_COOKIE_NAME, implode("|", $cookie_params), $login_time + 604800, "/", $domain, false, true);
    }

上面就是计算 setcookie 函数具体实现。

$cookie_params[] = hash_hmac("sha1", implode("", $cookie_params), COOKIE_KEY, false);

cookie 最后一个值 sign,会通过加密计算,利用 sha1 算法进行加密计算得到一个16 进制字符串得到sign。

其中 COOKIE_KEY 即加密的密钥,和网关那边配置的一样。网关那边会通过同样的加密算法,加密参数,加密密钥进行加密计算,如果参数一样那么算出来的sign值会和cookie一致,那么说明是有效的cookie。

 setcookie(SESSION_COOKIE_NAME, implode("|", $cookie_params), $login_time + 604800, "/", $domain, false, true);

最后数组用"|"连接成一个字符串即得到前面的四段 cookie 值。

网关介绍

API 网关是什么

API网关是随着微服务(Microservice)概念兴起的一种架构模式。原本一个庞大的单体应用(All in one)业务系统被拆分成许多微服务(Microservice)系统进行独立的维护和部署,服务拆分带来的变化是 API 的规模成倍增长,API的管理难度也在日益增加,使用API网关发布和管理API逐渐成为一种趋势。

一般来说,API网关是运行于外部请求与内部服务之间的一个流量入口,实现对外部请求的协议转换、鉴权、流控、参数校验、监控等通用功能

为什么需要网关

微服务架构下,单体应用被切割成多个微服务,如果将所有的微服务直接对外暴露,势必会出现安全方面的各种问题。

客户端如果可以直接向每个微服务发送请求,其问题主要如下:

  1. 客户端需求和每个微服务暴露的细粒度 API 不匹配。
  2. 每个公司可能有java,python,go多种语言,在不同的场景下又会使用不同的协议,部分服务使用的协议不是 Web 友好协议,可能使用 Thrift 二进制 RPC,也可能使用 AMQP 消息传递协议,或者其他的 grpc 之类的协议。
  3. 微服务的划分可能随着时间变化,微服务难以重构。如果合并两个服务,或者将一个服务拆分成两个或更多服务,这类重构就非常困难了。
  4. 不同的客户端可能需要不同的数据,例如 Web,H5,APP,Android,IOS。

网关开源组件业内用的多的有 Zuul、SpringCloud Gateway、Kong、Nginx 等。

从技术角度来看,什么是Kong?

这一段摘自KONG官方介绍文档
您可能已经听说过Kong基于Nginx,利用了其稳定性和高效率。但这究竟是怎么实现的呢?

更确切地说,Kong 是一个在 Nginx 中运行的 Lua 应用程序,并且可以通过 lua-nginx模块实现。Kong不是用这个模块编译Nginx,而是与 OpenResty 一起分发,OpenResty已经包含了lua-nginx-module。OpenResty 不是 Nginx 的分支,而是一组扩展其功能的模块。

这为可插拔架构奠定了基础,可以在运行时启用和执行Lua脚本(称为“插件”)。 因此,我们认为Kong是微服务架构的典范:它的核心是实现数据库抽象,路由和插件管理。 插件可以存在于单独的代码库中,并且可以在几行代码中注入到请求生命周期的任何位置。

简而言之,Kong 是 Mashape 开源的高性能高可用 API 网关和 API 服务管理层,一款基于 Nginx_Lua 模块写的高可用服务网关,由于 Kong 是基于 Nginx 的,所以可以水平扩展多个 Kong 服务器。通过前置的负载均衡配置把请求均匀地分发到各个 Server,来应对大批量的网络请求。

为什么使用 Kong

在众多 API GATEWAY 框架中,Mashape 开源的高性能高可用 API 网关和 API 服务管理层——KONG(基于 NGINX)特点尤为突出,它可以通过插件扩展已有功能,这些插件(使用 lua 编写)在API请求响应循环的生命周期中被执行。于此同时,KONG本身提供包括 HTTP 基本认证、密钥认证、CORS、TCP、UDP、文件日志、API请求限流、请求转发及 NGINX 监控等基本功能。

除了Kong的基本网关功能,Kong 的云原生属性:与平台无关,Kong 可以从裸机运行到 Kubernetes,是我们青睐的原因,我们的服务都是 k8s 部署调度的。

Kong 经常用到的术语有:

  • client : 指下游客户向 Kong 代理端口发出请求。
  • upstream service :指自己位于 Kong 后面的 API/Service,客户端请求被转发到这些API/Service。
  • Service : 顾名思义,服务实体是上游服务的抽象。服务的示例包括数据转换微服务、计费API等。
  • Route : 这是指 Kong 路由实体。Route 是进入 Kong 的入口点,并为要匹配的请求定义规则,然后路由到给定的服务。
  • Plugin : 指 Kong 的 “plugins”,它是在代理生命周期中运行的业务逻辑片段。插件可以通过管理 API 进行配置——可以是全局的(所有传入的流量),也可以是在特定的路由和服务上配置。

Kong 网关解析 cookie

kong 项目简介,流量转发

Kong 项目构成

这个项目只是做了鉴权,属于内网网关,流量在这之前还会经过一道外网网关,那边有流控,请求分发,配置证书等功能,内网网关只是做鉴权,流量打到这边鉴权之后不同的路由转发到 k8s 的不同 service 里调用具体的服务。

项目部署了线上网关和测试环境网关两种环境,提供的plugins插件鉴权有 APP端的鉴权,C端和B端鉴权,这里我主要讲的是B端的鉴权,其他的简单介绍一下:

  • shop-resolve 里面主要是 小程序和H5 两种环境下的店铺信息鉴权,是公司一个电商项目需要用的;
  • idk-client-type-resolve 里面主要是 小程序和H5 两种环境下的访问鉴权;
  • health-check 没什么好说的,是k8s的健康检查需要的
  • 其他三个 app-resolve,buser-reslove,cuser-resolve 分别是app环境,B端,C端的鉴权

kong.ymal-service

kong.ymal 里面配置了很多业务线的 Service,比如快站云服务可以通过路由(www.kuaizhan.com,cloud.kuaizhan.com)过来,配置里面使用了鉴权插件 bUser-resolve,根据这个 name 配置请求转发 upstream service 如下:

kong.ymal-upstream service

upstream service 指自己位于 Kong 后面的 API/Service,客户端请求被转发到这些API/Service,此处是转发到 k8s 的 service 流量入口到具体的服务处理请求。

鉴权 lua 脚本

B端鉴权lua脚本

local ngx_re = require "ngx.re"
local BasePlugin = require "kong.plugins.base_plugin"
local string = require "resty.string"
local BuserResolveHandler = BasePlugin:extend()

function BuserResolveHandler:new()
    BuserResolveHandler.super.new(self, "buser-resolve")
end

function BuserResolveHandler:access(conf)
    BuserResolveHandler.super.access(self)
    local cookieName = "cookie_" .. "KUAIZHAN_V2"
    local kuaizhanV2 = ngx.var[cookieName]
--     ngx.log(ngx.ERR, "kuaizhanV2", kuaizhanV2)
    local uid = 0
    ngx.req.set_header("X-User-Id", uid)
		
    // cookie 内容 3001459%7C1636684996%7C7180720502%7Cb61a12ef865072964aa359e6a9ef2e0b1846dee9
    if xxx_V2 and xxx_V2 ~= '' then
        local res, err = ngx_re.split(xxxV2, "%7C")
        if err then
            return
        end
        // 解析得到 userId, time, nonce, sign,
        // 分别为 300145916366849967180720502,b61a12ef865072964aa359e6a9ef2e0b1846dee9
        local userId, time, nonce, sign = res[1], res[2], res[3], res[4]
        // 根据密钥,时间,签名,利用 hmac_sha1 算法,十六进制算法,得到摘要签名
        local digest = ngx.hmac_sha1(conf.secret, userId .. time .. nonce)
        local theSign = string.to_hex(digest)
        -- TODO 加上过期时间判断
        // 计算签名和cookie解析得到sign 是否相同,相同则赋值uid
        if theSign == sign then
            uid = userId
        end
        ngx.log(ngx.ERR, "theSign:", theSign, "sign:", sign, "uid:", uid)
    end

    ngx.log(ngx.ERR, "get x-user-id:" .. uid)
    // nginx 请求头header里面存放 X-User-Id,再转发到各个业务线
    ngx.req.set_header("X-User-Id", uid)
end

return BuserResolveHandler

解析过程如下四步:

  • cookie 内容 3001459%7C1636684996%7C7180720502%7Cb61a12ef865072964aa359e6a9ef2e0b1846dee9
  • 解析得到 userId, time, nonce, sign, 分别为 3001459,1636684996,7180720502,b61a12ef865072964aa359e6a9ef2e0b1846dee9
  • 根据密钥,用户id,时间,随机数,利用 hmac_sha1 算法,十六进制算法,得到摘要签名,计算签名和cookie解析得到sign 是否相同,相同则赋值uid
  • nginx 请求头header里面存放 X-User-Id,再转发到各个k8s service 中去(k8s service是各个服务集群的流量入口)

conf.secret 里面配置的密钥,很多业务线公用一个登录的话,比如官网很多业务线,那么插件的 secret 都是一样的配置,同理,这个 secret 和刚刚设置 cookie 的时候,使用的是一个值。

服务解析请求

因为这种方式在网关层就实现了在 header 里面设置了用户id信息,到了各个业务方直接写一个解析器解析请求头的 userId;

然后写一个注解类似于 @LoginRequired 的自定义注解,配合变量 BUser 来使用,这个 BUser 对象里就包含解析得到的 userId;

最后注解作用于 controller 接口,就可以完成请求的登录信息拦截了。

由于这个实现太偏业务,而且比较简单这里就不贴具体的代码实现了。

此方案实现的优缺点

优点:

  1. 实现简单,易维护易懂
  2. 可以实现统一网关鉴权和流量分发

单点登录问题

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们主域名叫做 xxx.com,另一个业务线有个二级域名 aaa.xxx.com,现在要实现在官网登录了,那么也就在另一个业务线登录了。实现如下:

登录以后,可以将Cookie的域设置为顶域 xxx.com,这样所有子域的系统都可以访问到顶域的 Cookie。我们在设置 Cookie 时,只能设置顶域和自己的域,不能设置其他的域。因此这边另一个业务线可以直接访问到顶域的登录状态,然后在 kong 解析那边的访问请求,密钥设置成一样的就可以鉴权了。

但是多端,或者跨顶域情况下的单点登录是没法做的。

登录续期问题

不知道大家有没有关注到第一张 cookie 截图后面有个失效时间,这种由于依赖 cookie,因此只要到期就会被踢下线,需要重新登录,一定程度上影响客户体验。

现在的很多应用都利用 token + redis 方案实现了登录续期,例如连续十天内有过登录,那么不需要再次登录,后端实现自动续期,十天以上都没有登录过的才失效需要重新登录。

但是续期服务需要 redis 成本,这种省成本。

注销问题

这种方案的注销,只是简单的删除 cookie,如果有心人拿到 cookie 仍然是可以用的,这个 cookie 在有效期内不会失效。

token+redis 方案可以做到真实的 token 失效。

以上就是此篇分享的所有内容呢了,我是《小龙飞》公众号号主小飞龙,欢迎关注我的公众号查看更多往期精彩内容,原创不易,如果对您有收获可以点个赞点个在看鼓励一下号主吧~~~

备注:文章中部分专业解释参考 kong 官方文档