美文网首页Java大数据
Kong 优雅实现微服务网关鉴权,登录场景落地实战篇

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

作者: Java弟中弟 | 来源:发表于2021-11-19 13:16 被阅读0次

    登录实现

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

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

    登录后的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 优雅实现微服务网关鉴权,登录场景落地实战篇

    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 优雅实现微服务网关鉴权,登录场景落地实战篇

    kong.ymal-service

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

    kong.ymal-upstream service

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

    鉴权 lua 脚本

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

    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,
            // 分别为 3001459,1636684996,7180720502,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 优雅实现微服务网关鉴权,登录场景落地实战篇

        本文链接:https://www.haomeiwen.com/subject/stnstrtx.html