在几分钟内保护您的 API:使用 JWT 的基于令牌的 RSocket

rsocket 提供了一个强大的消息传递系统,构建在反应式流框架之上,并支持多种协议,包括 tcp、websocket、http 1.1 和 http 2。其与编程语言无关的交互模型,例如 request_response、request_fnf 、request_stream、request_channel,满足微服务、api网关、sidecar代理、消息队列等多种通信场景。

在保护通信安全时,基于 rsocket 的应用程序可以轻松采用基于 tls 和基于 token 的解决方案。虽然 rsocket 可以在 tcp 或 websocket 上重用 tls,但本文重点介绍基于令牌的实现,以演示基于角色的访问控制 (rbac) 功能。

作为全球最广泛采用的 oauth2 技术,json web token (jwt) 由于其与编程语言无关的性质而成为理想的选择。经过深入研究,我坚信将 rsocket 与 jwt 结合起来是实现服务之间安全通信的绝佳方法,特别是对于 open api。有关保护 api 的更详细指南,请访问computerstechnicians.com。现在,让我们更深入地探讨其中的复杂性。

实现 rsocket 进行安全通信

首要问题是如何在 rsocket 中利用 token 进行服务间通信。

有两种方法将令牌从请求者传输到响应者。一种方法涉及在设置期间将令牌嵌入到元数据 api 中,而另一种方法涉及在每个请求中将令牌作为元数据发送,并附上作为数据的有效负载。

除此之外,路由在授权中起着关键作用,指示响应方的资源。在rsocket扩展中,存在路由元数据扩展来扩展四种交互模型。如果请求者和响应者都支持标签有效负载,那么在顶层定义授权就很简单。

了解 json web 令牌 (jwt)

要理解本文,了解 jwt 以下五个方面就足够了。

  1. jwt 包含 json web 签名 (jws)、json web 加密 (jwe)、json web 密钥 (jwk) 和 json web 算法 (jwa)。

    1. hs256 是一种对称密钥算法,而 rs256/es256 是一种基于公钥基础设施 (pki) 的非对称密钥算法。两者都在 jwa 规范中定义。 hs256 将 hmac(密钥哈希消息验证码)与 sha-256 相结合,而 rs256 使用 rsassa 与 sha-256 配对,es256 采用 ecdsa(椭圆曲线数字签名算法)与 sha-256 一起使用。

    2. 就秘密长度而言,假设采用 hs256 算法来生成令牌,鉴于 hs256 需要至少 256 位的秘密(考虑到 1 个字符相当于 8 位),秘密字符应超过 32 个。

    3. 响应者使用访问令牌进行解码、验证和授权目的,而刷新令牌用于重新生成令牌,特别是当访问令牌已过期或无效时。

    4. 用户退出后需要进行适当的处​​理,尤其是在此期间访问令牌仍然有效的情况下,以防止未经授权的访问。

    安全数据交换

    现在,让我们开始演示吧。我们有两种类型的 api:令牌和资源。只有在令牌经过验证和认证后才能访问资源 api。

    工作流程

    • 我们使用登录 api 为请求者生成令牌,这需要用户名和密码。身份验证成功后,响应方签名、保存并将访问令牌和刷新令牌返回给请求方。
    • 刷新api用于更新令牌,需要刷新令牌。解码授权后,响应方签名、保存并返回access token和refresh token给请求方。

    • 我们定义 info/list/hire/fire 作为资源 api 来演示各种读/写操作。

    • 注销 api 处理令牌被盗的情况,如前所述,以防止未经授权的访问。

    在几分钟内保护您的 API:使用 JWT 的基于令牌的 RSocket

    身份验证

    鉴于我们使用基于角色的访问控制(rbac)作为我们的授权机制,身份验证组件应该提供一个身份存储库(用户-角色-权限)来存储和检索响应者中的身份信息,确保安全身份验证。

    此外,我们提供了一个令牌存储库来存储、撤销和读取令牌,用于验证从令牌解码的身份验证。由于授权信息在令牌内被加密和压缩,因此我们使用存储库信息来仔细检查这些授权。如果匹配,我们就可以确认请求的真实性和合法性。

    身份验证

    api interaction model role
    sign in request/response all users
    sign out fire-and-forget authenticated users
    refresh token request/response all users
    user information request/response user, administrator
    user list request/stream user, administrator
    hire user request/response administrator
    terminate user request/response administrator

    实施具有增强安全性的 spring boot

    令牌签名

    为了实现不同编程语言的无缝集成,在本演示中为加密和压缩中使用的加密算法和常量建立统一的标准至关重要。

    在此实现中,我们选择hs256作为首选算法,访问令牌有效期为5分钟,刷新令牌有效期为7天。

    public static final long access_token_validity_period = 5;
    public static final long refresh_token_validity_period = 7;
    private static final macalgorithm encryption_mechanism = macalgorithm.hs256;
    private static final string hashing_algorithm = "hmacsha256";

    让我们检查生成的访问令牌代码:

    public static usertoken generateaccesstoken(hellouser  user) {
        algorithm encryption_technique = algorithm.hmac256(access_secret_key);
        return generatetoken(user, encryption_technique, access_token_validity_period, chronounit.minutes);
    }
    
    private static usertoken generatetoken(hellouser  user, algorithm encryptiontechnique, long expirationtime, chronounit timeunit) {
        string tokenidentifier = uuid.randomuuid().tostring();
        instant currenttime = instant.now();
        instant expirationtimeinstant;
        if (currenttime.issupported(timeunit)) {
            expirationtimeinstant = currenttime.plus(expirationtime, timeunit);
        } else {
            log.error("unit param is not supported");
            return null;
        }
        string token = jwt.create()
                .withjwtid(tokenidentifier)
                .withsubject(user.getuserid())
                .withclaim("scope", user.getrole())
                .withexpiresat(date.from(expirationtimeinstant))
                .sign(encryptiontechnique);
        return usertoken.builder().tokenid(tokenidentifier).token(token).user(user).build();
    }
    重要提示: 上述代码中的声明键名称不是任意选择的,因为框架中使用“scope”作为从令牌中解码角色的默认方法。

    随后,token解码器代码如下:

    java 公共静态reactivejwtdecoder acquireaccesstokendecoder(){ secretkeyspec secretkey = new secretkeyspec(access_secret_key.getbytes(), hmac_sha_256); 返回 nimbusreactivejwtdecoder.withsecretkey(secretkey) .messageauthenticationalgorithm(mac_algorithm) 。建造(); } 公共静态reactivejwtdecoder jwtaccesstokendecoder(){ 返回新的hellojwtdecoder(acquireaccesstokendecoder()); } //hellojwtdecoder @overridepublic mono 解码(字符串标记)抛出 jwtexception { 返回reactivejwtdecoder.decode(token).doonnext(jwt -> { 字符串 id = jwt.getid(); hellouser auth = tokenrepository.retrieveauthfromaccesstoken(id); 如果(验证==空){ 抛出新的 jwtexception(“无效的 hellouser”); } //待办事项 hellojwtservice.settokenid(id); }); }

    中的解码方法

    hellojwtdecoder 将在每个请求处理周期由框架触发,将 token 字符串值转换为 jwt:

    @beanpayloadsocketacceptorinterceptor authorization(rsocketsecurity rsocketsecurity) {
        rsocketsecurity security = pattern(rsocketsecurity)
                .jwt(jwtspec -> {
                    try {
                        jwtspec.authenticationmanager(jwtreactiveauthenticationmanager(jwtdecoder()));
                    } catch (exception e) {
                        throw new runtimeexception(e);
                    }
                });
        return security.build();
    }
    
    @beanpublic reactivejwtdecoder jwtdecoder() throws exception {
        return tokenutils.jwtaccesstokendecoder();
    }
    
    @beanpublic jwtreactiveauthenticationmanager jwtreactiveauthenticationmanager(reactivejwtdecoder decoder) {
        jwtauthenticationconverter converter = new jwtauthenticationconverter();
        jwtgrantedauthoritiesconverter authoritiesconverter = new jwtgrantedauthoritiesconverter();
        authoritiesconverter.setauthorityprefix("role_");
        converter.setjwtgrantedauthoritiesconverter(authoritiesconverter);
        jwtreactiveauthenticationmanager manager = new jwtreactiveauthenticationmanager(decoder);
        manager.setjwtauthenticationconverter(new reactivejwtauthenticationconverteradapter(converter));
        return manager;
    }

    撤销令牌

    为了简化demo运行的环境,这里撤销token的方式是通过guava缓存来实现的。您可以使用一些强大的组件(例如 redis)来做到这一点。

    时间一到,访问令牌将自动撤销。

    另一方面,当请求者发送注销时,该缓存将作为事件驱动被调用。

    cache<string hellouser> accesstokentable = cachebuilder.newbuilder()
                .expireafterwrite(tokenutils.access_expire, timeunit.minutes).build();
    
    public void deleteaccesstoken(string tokenid) {
      accesstokentable.invalidate(tokenid);
    }</string>

    身份验证

    authenticate 函数专为登录而设计,其运行原理与 http 基本身份验证机制相同,并且非常简单:

    hellouser   user = userrepository.retrieve(principal);
    if (user.getpassword().equals(credential)) {
      return user;
    }

    相比之下,专为刷新而定制的替代身份验证涉及一系列更复杂的步骤:

    • 获取解码器并利用它将令牌字符串值解码为 jwt 对象

    • 实施反应式方法将 jwt 映射到身份验证

    • 从存储库检索身份验证详细信息

    • 验证数据库中的身份验证信息和令牌是否相同

    • 以流式方式返回认证对象

    return reactivejwtdecoder.decode(refreshtoken).map(jwt -> {
        try {
            hellouser   user = hellouser  .builder().userid(jwt.getsubject()).role(jwt.getclaim("scope")).build();
            log.info("verification successful. user: {}", user);
            hellouser   auth = tokenrepository.getauthfromrefreshtoken(jwt.getid());
            if (user.equals(auth)) {
                return user;
            }
        } catch (exception e) {
            log.error("", e);
        }
        return new hellouser  ();
    });

    权限管理

    正如我之前提到的,这个演示是基于基于角色的访问控制(rbac)构建的;路由是至关重要的方面。为了简洁起见,我将不再展示开放 api 版本,而是提供一个简洁的概述:

    // hellosecurityconfig
    protected rsocketsecurity pattern(rsocketsecurity security) {
        return security.authorizepayload(authorize -> authorize
                .route("signin.v1").permitall()
                .route("refresh.v1").permitall()
                .route("signout.v1").authenticated()
                .route("hire.v1").hasrole(admin)
                .route("fire.v1").hasrole(admin)
                .route("info.v1").hasanyrole(user, admin)
                .route("list.v1").hasanyrole(user, admin)
                .anyrequest().authenticated()
                .anyexchange().permitall()
        );
    }
    
    // hellojwtsecurityconfig
    @configuration@enablersocketsecuritypublic class hellojwtsecurityconfig extends hellosecurityconfig {
      @bean  payloadsocketacceptorinterceptor authorization(rsocketsecurity rsocketsecurity) {
        rsocketsecurity security = pattern(rsocketsecurity)
        ...

    我将基于路由的 rbac 定义放在父类中,以便于使用其他方式扩展安全性,例如tls。

    springboot 提供了 messagemapping 注解来让我们定义消息传递的路由,这意味着 rsocket 中的流式 api。

    @messagemapping("signin.v1")    mono<hellotoken> signin(hellouser hellouser) {
        ...</hellotoken>

    先决条件

    从 2.2.0-release 开始,spring boot 已纳入 rsocket 支持。此外,从2.3版本开始,它还提供了rsocket安全功能。由于在我撰写本文时 2.3.0 尚未普遍可用,因此我展示的版本是 2.3.0.m4。

    • spring-boot.版本2.3.0.m4

    • spring.版本 5.2.5.release

    • spring-security.version 5.3.1.release

    • rsocket.版本1.0.0-rc6

    • reactor-netty.version 0.9.5.release

    • netty.版本 4.1.45.final

    • reactor-core.version 3.3.3.release

    • jjwt.版本 0.9.1

    编译、执行和测试

    bash build.sh
    bash run_responder.sh
    bash run_requester.sh
    bash curl_test.sh

    curl 测试

    echo "Logging in as user"
    read accessToken refreshToken 
    
    <h3>隐藏的宝石</h3>
    
    <p>资源 api 组件说明了从招聘到终止的员工生命周期。想要更全面的了解,请探索十八罗汉!</p>
    
    <h2>最后的想法</h2>
    
    <p>最初,我计划提供一个 <a style="color:#f60; text-decoration:underline;" href="https://www.php.cn/zt/16009.html" target="_blank">golang</a> 实现,但不幸的是,golang 的 rsocket 缺乏开放的路由 api,导致实现这一目标不切实际。然而,一线希望依然存在:jeff 很快就会让它们变得可用。</p>
    
    <p>我发现使用 rust 和 <a style="color:#f60; text-decoration:underline;" href="https://www.php.cn/zt/15723.html" target="_blank">nodejs</a> 等替代语言来演示这一点很有趣。也许我什至会就此主题撰写一系列文章。</p>
    
    <p>顺便说一下,这个演示的源代码可以在 <a style="color:#f60; text-decoration:underline;" href="https://www.php.cn/zt/15841.html" target="_blank">git</a>hub 上找到。</p>
    
    
    

以上就是在几分钟内保护您的 API:使用 JWT 的基于令牌的 RSocket的详细内容,更多请关注其它相关文章!