1.9.虚之墙:安全

木落如飞鸟,山平疑澹烟。灯残挥手去,曳杖听流泉。——陈继儒《同印空夜坐凭虚阁》

「壹」基本开发

    本文介绍Zero中关于安全部分的开发,该部分开发主要使用配置实现,而且Zero框架中所有和认证授权相关的内容都写在AOP层,AOP全称为Aspect Oriented Program,面向切面编程,一般通过预编译方式和运行时动态代理实现程序功能的统一维护技术,它属于OOP的延续。

1.1. 基本配置

    Zero框架中的配置文件结构如下:

    上述结构中注意两个根节点:

    由于项目原因,目前Zero只支持比较主流的jwt类型,zero-rbac中提供的默认安全实现也是基于jwt的,为了强化jwt,旧版本Zero中的basicmongo 两种类型目前都不考虑,如果您想要自己实现,可参考下边两个类去自定义:

  • io.vertx.up.secure.handler.JwtOstium(AuthHandler子接口)

  • io.vertx.up.secure.provider.authenticate.JwtAuth(AuthProvider子接口)

1.2. 跨域

    跨域配置是Zero和Zero-UI前后端集成时的核心配置,该配置同样采用第三方集成配置,文件结构参考上图,文件内容如下:

cors:
  credentials: true
  methods:
    - DELETE
    - GET
    - POST
    - PUT
    - OPTIONS
  headers:
    - Authorization
    - Accept
    - Content-Disposition
    - Content-Encoding
    - Content-Length
    - Content-Type
    - X-App-Id
    - X-App-Key
    - X-Sigma
    - X-Lang
  origin: "http://ox.server.cn:4000"

这个配置就没有必要解释了,读者一看就明白。

1.3. @Wall

    言归正传,接下来看看Zero中安全部分的核心开发,在看这部分之前先区分几个基本工作流:

  1. 登录流程:根据用户账号和口令生成令牌(Token),该令牌会在后续请求过程中追加到Authorization请求头中。

  2. 认证流程:(401)触发系统的安全框架验证Authorization请求头中提供的令牌Token是否合法。

  3. 授权流程:(403)认证成功后,触发系统安全框架中的核心授权逻辑。

    综上,您开发的任何一个系统前两个流程是可以标准化的,比如JWT算法生成令牌、基于OAuth的三步骤交换令牌、或者在Basic认证中检查用户名和密码一步到位等等,而最后一个授权流程就根据系统需求有所差异了,您可以基于RBAC模型来设计系统的授权流程(后续教程会带您分析zero-rbac 子模块的授权系统)。

1.3.1. Security接口

    io.vertx.up.secure.Security接口是Zero配合认证授权设计的核心接口,它的代码定义如下:

package io.vertx.up.secure;

import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;

public interface Security {
    /*
     * 该方法在登录成功后调用,属于登录成功的callback回调函数,用来
     * 关联登录账号的安全信息存储介质——如Redis、Database、Etcd等都
     * 可透过该方法关联某种或多种,默认情况什么都不做。
     **/
    default Future<JsonObject> store(final JsonObject data) {   
        return Future.succeededFuture(data);
    }
    /*
     * 核心令牌(Token)校验专用接口,校验时会验证Authorization头是否
     * 合法,不合法返回401错误,其中不合法包括:
     * - 1)提供的令牌Token不匹配。
     * - 2)Token过期,需执行Refresh流程。
     * - 3)数字签名不合法,sig错误,不可调用。
     **/
    Future<Boolean> verify(JsonObject data);
    /*
     * 认证过后的核心授权专用接口,用来执行403校验,默认访问任何API
     * 都是已经授权的情况。
     **/
    default Future<Boolean> access(final JsonObject user) {
        return Future.succeededFuture(Boolean.TRUE);
    }
}

1.3.2. 开发步骤

    Zero使用了vertx-auth-common 框架执行安全流程,并根据Vert.x的原始代码进行重新设计以及开发,创建了自定义可配置的安全模块,从前文可知,Zero重写了AuthProviderAuthHandler,重写过后支持下边功能:

  1. 和Security接口连接,实现标准化的认证、授权流程。

  2. 将权限认证转移到切面代码中,使用Annotation注解技术实现AOP层的认证。

  3. 让Zero可启用插件模块对授权进行强定制(zero-rbac中数据域实现)。

    先看一个基本示例:

Agent代码

@EndPoint
@Path("/api")
public class SecAgent {
    @GET
    @Path("/hi/secure")
    public String sendOneWay(
            @BodyParam final JsonObject json) {
        return "Hello World";
    }
}

Security实现类

package cn.vertxup.secure;

import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import io.vertx.up.secure.Security;

public class SecStub implements Security {

    @Override
    public Future<Boolean> verify(final JsonObject data) {
        return Future.succeededFuture(Boolean.TRUE);
    }
}

    注意:上边代码主要是为了测试,所以只重写了认证的方法,并没有提供其他实现。

@Wall主类

package cn.vertxup.secure;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.handler.AuthHandler;
import io.vertx.up.annotations.Authenticate;
import io.vertx.up.annotations.Wall;
import io.vertx.up.secure.Security;
import io.vertx.up.secure.handler.JwtOstium;
import io.vertx.up.secure.provider.authenticate.JwtAuth;

import javax.inject.Inject;

// 墙相关配置,类型和路径
@Wall(value = "jwt", path = "/api/*")
public class SecWall {
    // Zero中支持JSR330依赖注入代码
    @Inject
    private transient Security security;

    // 主方法,用来创建AuthHandler和AuthProvider
    @Authenticate
    public AuthHandler authenticate(final Vertx vertx,
                                    final JsonObject config) {
        return JwtOstium.create(JwtAuth.create(vertx, new JWTAuthOptions(config))
            .bind(() -> this.security));
    }
}

    此处的基本思路和vertx-auth-common模块扩展思路一样,都是创建自定义的AuthHandlerAuthProvider ,而Zero只是将这几部分内容通过轻量级的方式编连到一起,上述代码的内部实现处于切面层,所以它们的存在不影响Agent/Worker结构。

    @Wall注解,单词翻译为“墙”,它是Zero中打开安全模块的注解,内部定义的注解方法如下:

    运行上述案例代码,任意发送到/api/*下的请求都会收到如下响应:

{
    "code": -60012,
    "message": "[ERR-60012] (JwtPhylum) Web Exception occurs: (401) - ..."
}

    并且在后台会看到如下信息:

io.vertx.tp.error._401UnauthorizedException: [ERR-60012] (JwtPhylum) Web Exception \ 
occurs: (401) - (Security) Unauthorized request met in request.
	at io.vertx.up.secure.handler.AuthPhylum.<init>(AuthPhylum.java:34)
	at io.vertx.up.secure.handler.AuthPhylum.<init>(AuthPhylum.java:40)

    如何将请求发送成功呢?需在客户端增加Authorization的HTTP请求头重新发送请求,这个例子中由于Security类的verity方法直接返回了ture,任意格式合法的JwtToken都可以得到最终的正确请求:

{
    "data": "Hello World"
}

1.3.3. 工具类

    为了方便用户将信息存储到JWT的令牌并执行提取,Zero提供了核心工具类Ux.Jwt,先看下边代码:

Agent代码

@EndPoint
@Path("/hi/jwt")
public class JwtAgent {
    @GET
    @Path("/generate")
    public String hiJwt(
            @QueryParam("name") final String name,
            @QueryParam("email") final String email) {
        // Encoding
        final JsonObject tokenJson = new JsonObject()
                .put("name", name)
                .put("email", email);
        return Ux.Jwt.token(tokenJson);
    }
}

    上述代码请求会返回一个当前环境中的令牌值:

{
    "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....."
}

    运行上述代码需在您的环境中做一定的准备:

  1. 安全配置文件中的配置如下:

    secure:
      jwt:
        type: jwt
        config:
          jwtOptions:
            algorithm: HS256
          keyStore:
            type: jceks
            path: keys/keystore.jceks
            password: zeroup
  2. 在该配置下,需在src/main/resources中追加keys/keystore.jceks文件,该文件为自己生成,如果读者嫌麻烦可直接下载代码找到该文件。

    若不做上边三个准备,会遇到下边的错误:

{
    "code": -60007,
    "message": "[ERR-60007] (Envelop) Web Exception ... Algorithm not supported."
}

    如代码中所示,Zero中提供的工具函数如下:

// 将JsonObject对象存储在token中并生成新的token
Ux.Jwt.token(JsonObject);
// 将token还原得到原始的JsonObject对象
Ux.Jwt.extract(String);

    上述准备代码就是为工具类服务的,主要目的如下:

  1. 生成Jwt的Token时,保证算法的基本条件满足,这部分内容可参考Jwt的Token算法相关资料,如此才可保证Token可正确生成。

  2. 两个工具类执行的底层Jwt配置必须一致,如果算法不一致就会导致前边Algorithm not supported错。

有了工具类,您就很容易将数据存储到Token中,并且很容易从Token中提取数据。

「贰」插件扩展

    Zero中的认证授权逻辑主要基于Security接口,本章节抛开第一节提及的@Wall开发细节,转移到两个核心数据插件中,这两个插件可帮助您完成如下任务:

  1. 支持授权过后的数据域操作。

  2. 支持Auditor责任数据的切面注入。

    配置扩展插件的代码如下:

extension:
  region:
    component: io.vertx.tp.rbac.extension.DataRegion
    config:
      prefix: /api/
  auditor:
    component: io.vertx.tp.rbac.extension.AuditorPin
    config:
      include:
      exclude: 

    两个插件对流程的影响如下:

2.1. 数据域插件

    数据域插件的实现可参考io.vertx.tp.rbac.extension.DataRegion源代码,它的基本结构如下:

// 未显示完整代码
import io.vertx.up.extension.PlugRegion;

public class DataRegion implements PlugRegion {
    @Override
    public Future<Envelop> before(final RoutingContext context, 
                                  final Envelop request){
        // 请求处理:修改Worker组件中收到的Envelop对象
        return request;
    }
    @Override
    public Future<Envelop> after(final RoutingContext context, 
                                 final Envelop response){
        // 响应处理:修改Worker组件执行完成后的响应对象
        return response;
    }
}

    我在开发过程中提供的实现是基于zero-rbac模块的,数据域插件主要完成:

  1. 根据配置文件中的prefix判断哪些请求需执行数据域功能。

  2. before流程中,对请求的修改

    • 修改查询条件,根据权限定义缩小查询范围(行筛选)。

    • 修改列过滤信息,根据权限定义缩小查询范围(列筛选)。

  3. after流程中,对响应再次修改

    • 行二次过滤(区域筛选、单行筛选、动态筛选)。

    • 列二次过滤。

    • 提供模型中合法的acl信息(动态计算),创建多状态表单和多状态列表

    上述几点只是目前zero-rbac模块中已实现的基础数据域的功能,也只能作为您的一个参考,而位于切面层的代码让您更容易对请求和响应数据执行修改以及限制,zero-rbac 的详细功能后边我会专程写一篇文章来讲解,包括配置步骤,动态资源访问器和界面配置等。

2.2. Auditor插件

    Auditor插件的实现可参考io.vertx.tp.rbac.extension.AuditorPin源代码,它的基本结构如下:

// 未显示完整代码部分
import io.vertx.up.extension.PlugAuditor

public class AuditorPin implements PlugAuditor {
    @Override
    public PlugAuditor bind(final JsonObject config) {
        // 绑定当前插件的配置定义
        return this;
    }
    @Override
    public Future<Envelop> audit(final RoutingContext context,
                                 final Envelop request){
        // 请求处理:修改Worker组件中收到的Envelop对象
        return request;
    }
}

    Auditor插件主要用于修改请求数据(不处理响应),在CRUD模型中,只有新增更新 会使用该插件,该插件负责让用户在系统中留下操作痕迹,至于痕迹如何存储就在于您如何设计这两个插件的内置逻辑。

    zero-rbac中提供了最简单的Auditor数据如下:

    执行了该插件过后,上述四个属性会被直接追加到Envelop的统一模型中,当然若您不使用zero-rbac ,也可以自己去实现,插件对请求和响应的修改是AOP模式,若不提供这两个插件,不影响正常数据流的运行。

「叄」小结

    到这里,Zero安全部分的开发就告一段落,虽然部分内容和安全没有直接关系,但这些东西都是实战过程中有用的部分,可提供给您参考。若您使用了zero-rbac 扩展模块,无疑可以直接忽略掉系统的认证、授权并拥有它所有的功能,包括在微服务环境下提供的认证授权服务器的功能。

    最后,提供一段zero-rbac的@Wall完整代码(不包含服务类),让您对本章内容有所回顾。

package cn.vertxup.rbac.wall;

import cn.vertxup.rbac.service.accredit.AccreditStub;
import cn.vertxup.rbac.service.jwt.JwtStub;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.handler.AuthHandler;
import io.vertx.tp.rbac.cv.AuthMsg;
import io.vertx.tp.rbac.refine.Sc;
import io.vertx.up.annotations.Authenticate;
import io.vertx.up.annotations.Wall;
import io.vertx.up.log.Annal;
import io.vertx.up.secure.Security;
import io.vertx.up.secure.handler.JwtOstium;
import io.vertx.up.secure.provider.authenticate.JwtAuth;
import io.vertx.up.unity.Ux;

import javax.inject.Inject;

@Wall(value = "jwt", path = "/api/*")
public class JwtWall implements Security {
    private static final Annal LOGGER = Annal.get(JwtWall.class);
    @Inject
    private transient JwtStub jwtStub;
    @Inject
    private transient AccreditStub accredit;

    @Authenticate
    public AuthHandler authenticate(final Vertx vertx,
                                    final JsonObject config) {
        return JwtOstium.create(JwtAuth.create(vertx, new JWTAuthOptions(config))
            .bind(() -> this));
    }

    @Override
    public Future<JsonObject> store(final JsonObject data) {
        final String userKey = data.getString("user");
        Sc.infoAuth(LOGGER, AuthMsg.TOKEN_STORE, userKey);
        return jwtStub.store(userKey, data);
    }

    @Override
    public Future<Boolean> verify(final JsonObject data) {
        final String token = data.getString("jwt");
        final JsonObject extracted = Ux.Jwt.extract(data);
        Sc.infoAuth(LOGGER, AuthMsg.TOKEN_INPUT, token, extracted.encode());
        return jwtStub.verify(extracted.getString("user"), token);
    }

    @Override
    public Future<Boolean> access(final JsonObject data) {
        /*
         * User defined accessor
         */
        return accredit.authorize(data);
    }
}

Last updated